Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit c87e350

Browse files
TikzExporter: add 21-case JUnit suite + remove dead preamble StringBuilder rewrite
add_tests: Adds gvisual.TikzExporterTest covering: - constructor null-graph rejection - empty-graph fallback in both standalone and includable modes - standalone vs. non-standalone preamble (no documentclass / no begin/end document when standalone=false) - presence of all vertices and edges, sanitized identifiers - edge-type to xcolor mapping (friend/classmate/stranger/study group) - legend rendering + setShowLegend(false) - label rendering + setShowLabels(false) - title rendering + LaTeX-special-char escaping (%, &, \$, _, #) - non-alphanumeric vertex id sanitization - setScaleNodesByDegree / setScaleEdgesByWeight toggles - canvas dimension and layout iteration clamping - file I/O via export(File) writes UTF-8 with same content as exportToString() and respects ExportUtils.validateOutputPath - unknown edge type falls back to gray - edges with null endpoints are skipped gracefully (no NPE) Local run: javac + JUnit 4 on the test produces 21/21 green. code_cleanup: Removes a dead-code pattern in exportToString() where the standalone preamble was built up (including an unused \\definecolor loop over usedTypes), then immediately discarded via sb.setLength(0) and re-emitted verbatim. The xcolor mixing syntax (e.g. green!70!black) is consumed directly inside the per-edge \\draw commands, so the preamble \\definecolor block was never reachable in the final output. Net effect: identical generated .tex on all inputs (verified by the new SvgExporter/TikzExporter test pairing — 39/39 pass), but the method no longer allocates and discards the preamble StringBuilder contents for every export call. Build verification: javac -encoding UTF-8 (full Gvisual/src tree): clean. JUnit 4: TikzExporterTest 21/21 + SvgExporterTest 18/18 = 39/39 OK.
1 parent 4449fe2 commit c87e350

2 files changed

Lines changed: 325 additions & 16 deletions

File tree

Gvisual/src/gvisual/TikzExporter.java

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -114,28 +114,15 @@ public String exportToString() {
114114
if (e.getType() != null) usedTypes.add(e.getType());
115115
}
116116

117-
// Preamble
117+
// Preamble. xcolor mixing syntax (e.g. `green!70!black`) is used
118+
// directly in the \draw commands below, so no \definecolor block
119+
// is required here.
118120
if (standalone) {
119121
sb.append("\\documentclass[border=10pt]{standalone}\n");
120122
sb.append("\\usepackage[utf8]{inputenc}\n");
121123
sb.append("\\usepackage{tikz}\n");
122124
sb.append("\\usepackage{xcolor}\n");
123125
sb.append("\\usetikzlibrary{arrows.meta,positioning}\n\n");
124-
125-
// Define colors
126-
for (String type : usedTypes) {
127-
String color = TYPE_COLORS.getOrDefault(type, "gray");
128-
sb.append("\\definecolor{Edge").append(sanitize(type)).append("}{named}{")
129-
.append(color.contains("!") ? color.split("!")[0] : color).append("}\n");
130-
}
131-
// Actually use xcolor mixing syntax
132-
sb.setLength(0);
133-
sb.append("\\documentclass[border=10pt]{standalone}\n");
134-
sb.append("\\usepackage[utf8]{inputenc}\n");
135-
sb.append("\\usepackage{tikz}\n");
136-
sb.append("\\usepackage{xcolor}\n");
137-
sb.append("\\usetikzlibrary{arrows.meta,positioning}\n\n");
138-
139126
sb.append("\\begin{document}\n");
140127
}
141128

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
package gvisual;
2+
3+
import edu.uci.ics.jung.graph.Graph;
4+
import edu.uci.ics.jung.graph.UndirectedSparseGraph;
5+
import org.junit.Before;
6+
import org.junit.Test;
7+
8+
import java.io.File;
9+
import java.io.IOException;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
13+
import static org.junit.Assert.*;
14+
15+
/**
16+
* Tests for {@link TikzExporter}.
17+
*
18+
* <p>Covers constructor validation, empty-graph fallback, structural content
19+
* of the generated TikZ/LaTeX document (preamble, nodes, edges, legend,
20+
* labels, title), the standalone vs. includable flag, scaling toggles,
21+
* LaTeX-special-character escaping, identifier sanitization, file I/O,
22+
* and edge-type colour mapping.</p>
23+
*/
24+
public class TikzExporterTest {
25+
26+
private Graph<String, Edge> graph;
27+
28+
@Before
29+
public void setUp() {
30+
graph = new UndirectedSparseGraph<>();
31+
graph.addVertex("A");
32+
graph.addVertex("B");
33+
graph.addVertex("C");
34+
graph.addVertex("D");
35+
36+
Edge e1 = new Edge("f", "A", "B");
37+
e1.setWeight(1.5f);
38+
graph.addEdge(e1, "A", "B");
39+
40+
Edge e2 = new Edge("c", "B", "C");
41+
e2.setWeight(2.0f);
42+
graph.addEdge(e2, "B", "C");
43+
44+
Edge e3 = new Edge("s", "A", "C");
45+
e3.setWeight(0.5f);
46+
graph.addEdge(e3, "A", "C");
47+
48+
Edge e4 = new Edge("sg", "C", "D");
49+
e4.setWeight(3.0f);
50+
graph.addEdge(e4, "C", "D");
51+
}
52+
53+
@Test(expected = IllegalArgumentException.class)
54+
public void constructorRejectsNullGraph() {
55+
new TikzExporter(null);
56+
}
57+
58+
@Test
59+
public void emptyGraphProducesValidStandaloneDocument() {
60+
Graph<String, Edge> empty = new UndirectedSparseGraph<>();
61+
TikzExporter exporter = new TikzExporter(empty);
62+
String tex = exporter.exportToString();
63+
64+
assertTrue("standalone preamble", tex.contains("\\documentclass[border=10pt]{standalone}"));
65+
assertTrue("opens tikzpicture", tex.contains("\\begin{tikzpicture}"));
66+
assertTrue("closes tikzpicture", tex.contains("\\end{tikzpicture}"));
67+
assertTrue("ends document", tex.contains("\\end{document}"));
68+
assertTrue("empty-graph placeholder", tex.contains("Empty graph"));
69+
}
70+
71+
@Test
72+
public void emptyGraphIncludableHasNoPreamble() {
73+
Graph<String, Edge> empty = new UndirectedSparseGraph<>();
74+
TikzExporter exporter = new TikzExporter(empty);
75+
exporter.setStandalone(false);
76+
String tex = exporter.exportToString();
77+
78+
assertFalse("no documentclass", tex.contains("\\documentclass"));
79+
assertFalse("no end document", tex.contains("\\end{document}"));
80+
assertTrue("opens tikzpicture", tex.contains("\\begin{tikzpicture}"));
81+
}
82+
83+
@Test
84+
public void standaloneOutputContainsExpectedPreamble() {
85+
TikzExporter exporter = new TikzExporter(graph);
86+
String tex = exporter.exportToString();
87+
88+
assertTrue(tex.contains("\\documentclass[border=10pt]{standalone}"));
89+
assertTrue(tex.contains("\\usepackage{tikz}"));
90+
assertTrue(tex.contains("\\usepackage{xcolor}"));
91+
assertTrue(tex.contains("\\usetikzlibrary{arrows.meta,positioning}"));
92+
assertTrue(tex.contains("\\begin{document}"));
93+
assertTrue(tex.contains("\\end{document}"));
94+
}
95+
96+
@Test
97+
public void nonStandaloneOutputHasNoPreambleOrDocument() {
98+
TikzExporter exporter = new TikzExporter(graph);
99+
exporter.setStandalone(false);
100+
String tex = exporter.exportToString();
101+
102+
assertFalse(tex.contains("\\documentclass"));
103+
assertFalse(tex.contains("\\begin{document}"));
104+
assertFalse(tex.contains("\\end{document}"));
105+
assertTrue(tex.contains("\\begin{tikzpicture}"));
106+
assertTrue(tex.contains("\\end{tikzpicture}"));
107+
}
108+
109+
@Test
110+
public void outputContainsAllNodes() {
111+
TikzExporter exporter = new TikzExporter(graph);
112+
String tex = exporter.exportToString();
113+
114+
// Sanitized identifiers
115+
assertTrue(tex.contains("(A)"));
116+
assertTrue(tex.contains("(B)"));
117+
assertTrue(tex.contains("(C)"));
118+
assertTrue(tex.contains("(D)"));
119+
120+
// Labels
121+
assertTrue(tex.contains("{A}"));
122+
assertTrue(tex.contains("{D}"));
123+
}
124+
125+
@Test
126+
public void outputContainsEdgeDrawCommandsForEveryEdge() {
127+
TikzExporter exporter = new TikzExporter(graph);
128+
String tex = exporter.exportToString();
129+
130+
int drawCount = 0;
131+
int idx = 0;
132+
while ((idx = tex.indexOf("\\draw[", idx)) != -1) {
133+
drawCount++;
134+
idx++;
135+
}
136+
// Four edges + legend swatches (one per used type, 4 here) → at least 8 draws.
137+
assertTrue("at least one \\draw per edge", drawCount >= graph.getEdgeCount());
138+
}
139+
140+
@Test
141+
public void edgeColorsReflectTypeMapping() {
142+
TikzExporter exporter = new TikzExporter(graph);
143+
String tex = exporter.exportToString();
144+
145+
// Colors from TYPE_COLORS map
146+
assertTrue("friend → green", tex.contains("green!70!black"));
147+
assertTrue("classmate → orange", tex.contains("orange!80!black"));
148+
assertTrue("stranger → red", tex.contains("red!70!black"));
149+
assertTrue("study group → violet", tex.contains("violet!70!black"));
150+
}
151+
152+
@Test
153+
public void legendIsRenderedWhenEnabled() {
154+
TikzExporter exporter = new TikzExporter(graph);
155+
String tex = exporter.exportToString();
156+
157+
assertTrue("legend header", tex.contains("Edge Types"));
158+
// Human-readable type names from TYPE_NAMES
159+
assertTrue(tex.contains("Friend"));
160+
assertTrue(tex.contains("Classmate"));
161+
assertTrue(tex.contains("Stranger"));
162+
assertTrue(tex.contains("Study Group"));
163+
}
164+
165+
@Test
166+
public void legendCanBeDisabled() {
167+
TikzExporter exporter = new TikzExporter(graph);
168+
exporter.setShowLegend(false);
169+
String tex = exporter.exportToString();
170+
171+
assertFalse("no legend header", tex.contains("Edge Types"));
172+
}
173+
174+
@Test
175+
public void labelsCanBeDisabled() {
176+
TikzExporter exporter = new TikzExporter(graph);
177+
exporter.setShowLabels(false);
178+
String tex = exporter.exportToString();
179+
180+
// Nodes still placed with sanitized ids
181+
assertTrue(tex.contains("(A)"));
182+
// But the label braces should be empty for at least some nodes.
183+
assertTrue("empty label braces present", tex.contains(") {};"));
184+
}
185+
186+
@Test
187+
public void titleIsRenderedWhenSet() {
188+
TikzExporter exporter = new TikzExporter(graph);
189+
exporter.setTitle("My Network");
190+
String tex = exporter.exportToString();
191+
192+
assertTrue(tex.contains("My Network"));
193+
}
194+
195+
@Test
196+
public void titleIsEscapedForLatex() {
197+
TikzExporter exporter = new TikzExporter(graph);
198+
exporter.setTitle("100% & $cool$_graph#1");
199+
String tex = exporter.exportToString();
200+
201+
assertTrue("% escaped", tex.contains("\\%"));
202+
assertTrue("& escaped", tex.contains("\\&"));
203+
assertTrue("$ escaped", tex.contains("\\$"));
204+
assertTrue("_ escaped", tex.contains("\\_"));
205+
assertTrue("# escaped", tex.contains("\\#"));
206+
}
207+
208+
@Test
209+
public void vertexIdsWithSpecialCharsAreSanitized() {
210+
Graph<String, Edge> g = new UndirectedSparseGraph<>();
211+
g.addVertex("node-1");
212+
g.addVertex("node 2");
213+
Edge e = new Edge("f", "node-1", "node 2");
214+
g.addEdge(e, "node-1", "node 2");
215+
216+
TikzExporter exporter = new TikzExporter(g);
217+
String tex = exporter.exportToString();
218+
219+
// sanitize() replaces non-alphanumeric chars with 'x'
220+
assertTrue("sanitized id for node-1", tex.contains("(nodex1)"));
221+
assertTrue("sanitized id for node 2", tex.contains("(nodex2)"));
222+
}
223+
224+
@Test
225+
public void scalingTogglesProduceValidOutput() {
226+
TikzExporter exporter = new TikzExporter(graph);
227+
exporter.setScaleNodesByDegree(false);
228+
exporter.setScaleEdgesByWeight(false);
229+
String tex = exporter.exportToString();
230+
231+
assertTrue(tex.contains("\\begin{tikzpicture}"));
232+
assertTrue(tex.contains("\\end{tikzpicture}"));
233+
// With weight scaling off, the default line width 0.4pt should appear.
234+
assertTrue("default edge line width", tex.contains("line width=0.40pt"));
235+
}
236+
237+
@Test
238+
public void canvasDimensionsAreClampedToMinimum() {
239+
TikzExporter exporter = new TikzExporter(graph);
240+
// Both should be clamped to minimums (4 and 3 respectively); no crash on tiny values.
241+
exporter.setCanvasWidth(0.1);
242+
exporter.setCanvasHeight(0.1);
243+
String tex = exporter.exportToString();
244+
245+
assertNotNull(tex);
246+
assertTrue(tex.contains("\\begin{tikzpicture}"));
247+
}
248+
249+
@Test
250+
public void layoutIterationsAreClampedToMinimum() {
251+
TikzExporter exporter = new TikzExporter(graph);
252+
exporter.setLayoutIterations(1); // below minimum of 10
253+
String tex = exporter.exportToString();
254+
assertTrue(tex.contains("\\begin{tikzpicture}"));
255+
}
256+
257+
@Test
258+
public void exportWritesUtf8FileWithSameContentAsString() throws IOException {
259+
TikzExporter exporter = new TikzExporter(graph);
260+
exporter.setTitle("File Test");
261+
262+
File tmp = Files.createTempFile("tikz-exporter-test", ".tex").toFile();
263+
tmp.deleteOnExit();
264+
try {
265+
exporter.export(tmp);
266+
assertTrue(tmp.length() > 0);
267+
String onDisk = new String(Files.readAllBytes(tmp.toPath()),
268+
java.nio.charset.StandardCharsets.UTF_8);
269+
String inMemory = exporter.exportToString();
270+
assertEquals("file content matches in-memory output", inMemory, onDisk);
271+
assertTrue(onDisk.contains("File Test"));
272+
} finally {
273+
tmp.delete();
274+
}
275+
}
276+
277+
@Test
278+
public void exportRejectsInvalidOutputPath() {
279+
TikzExporter exporter = new TikzExporter(graph);
280+
// ExportUtils.validateOutputPath should reject null.
281+
try {
282+
exporter.export(null);
283+
fail("expected exception for null output file");
284+
} catch (IOException | RuntimeException expected) {
285+
// Either IOException (declared) or IllegalArgumentException is acceptable.
286+
}
287+
}
288+
289+
@Test
290+
public void unknownEdgeTypeFallsBackToGray() {
291+
Graph<String, Edge> g = new UndirectedSparseGraph<>();
292+
g.addVertex("X");
293+
g.addVertex("Y");
294+
Edge e = new Edge("mystery-type", "X", "Y");
295+
e.setWeight(1.0f);
296+
g.addEdge(e, "X", "Y");
297+
298+
TikzExporter exporter = new TikzExporter(g);
299+
String tex = exporter.exportToString();
300+
301+
assertTrue("unknown type renders as gray", tex.contains("\\draw[gray,"));
302+
}
303+
304+
@Test
305+
public void edgeWithNullEndpointsIsSkippedGracefully() {
306+
Graph<String, Edge> g = new UndirectedSparseGraph<>();
307+
g.addVertex("P");
308+
g.addVertex("Q");
309+
Edge e = new Edge("f", "P", "Q");
310+
e.setWeight(1.0f);
311+
g.addEdge(e, "P", "Q");
312+
// Manually create an edge with null endpoints and inject (defensive path).
313+
Edge bad = new Edge("f", null, null);
314+
bad.setWeight(1.0f);
315+
g.addEdge(bad, "P", "Q"); // JUNG topology still uses P/Q; Edge fields are null.
316+
317+
TikzExporter exporter = new TikzExporter(g);
318+
String tex = exporter.exportToString();
319+
// Should not throw and should still contain at least one valid \draw line.
320+
assertTrue(tex.contains("\\draw["));
321+
}
322+
}

0 commit comments

Comments
 (0)