Skip to content

Commit 5d85fd5

Browse files
committed
test: add IsolatedScanner test suite
Add comprehensive tests for isolated scanning mode that spawns separate JVM processes for each addon scan. Tests cover: - Full fixture JAR scanning with process isolation - Error handling for missing/corrupt files - Scan result metadata verification - Timeout configuration validation
1 parent 1eda865 commit 5d85fd5

1 file changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package com.cope.addonparser;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import com.cope.addonparser.model.JarScanResult;
9+
import com.cope.addonparser.scanner.IsolatedScanner;
10+
import java.io.IOException;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.util.ArrayList;
14+
import java.util.Comparator;
15+
import java.util.List;
16+
import java.util.Set;
17+
import java.util.stream.Collectors;
18+
import java.util.stream.Stream;
19+
import org.junit.jupiter.api.Test;
20+
import org.junit.jupiter.api.Timeout;
21+
22+
/**
23+
* Tests for {@link IsolatedScanner} that verify scanning fixture JARs in isolated JVM processes.
24+
* This test class mirrors {@link FixtureScanTest} but uses the isolated scanning mode.
25+
*/
26+
public class IsolatedScannerTest {
27+
private static final long TEST_TIMEOUT_SECONDS = 180;
28+
29+
@Test
30+
@Timeout(value = 300, unit = java.util.concurrent.TimeUnit.SECONDS)
31+
void scansAllFixtureJars() throws Exception {
32+
Path jarDir = Path.of(System.getProperty("addonparser.fixtureJarsDir", "fixtures/addons/jars"));
33+
assertTrue(
34+
Files.isDirectory(jarDir), "Fixture jar directory missing: " + jarDir.toAbsolutePath());
35+
36+
List<Path> jars =
37+
Files.list(jarDir)
38+
.filter(p -> Files.isRegularFile(p) && p.toString().toLowerCase().endsWith(".jar"))
39+
.sorted(Comparator.comparing(p -> p.getFileName().toString().toLowerCase()))
40+
.toList();
41+
42+
assertFalse(jars.isEmpty(), "No fixture jars found in " + jarDir.toAbsolutePath());
43+
44+
Set<String> jarNames =
45+
jars.stream().map(path -> path.getFileName().toString()).collect(Collectors.toSet());
46+
47+
// Verify expected fixture jars exist
48+
assertTrue(
49+
jarNames.stream().anyMatch(name -> name.startsWith("Baritone-Controller--")),
50+
"Missing fixture jar for Baritone-Controller");
51+
assertTrue(
52+
jarNames.stream().anyMatch(name -> name.startsWith("mc-games--")),
53+
"Missing fixture jar for mc-games");
54+
assertTrue(
55+
jarNames.stream().anyMatch(name -> name.startsWith("meteor-satellite-addon--")),
56+
"Missing fixture jar for meteor-satellite-addon");
57+
assertTrue(
58+
jarNames.stream().anyMatch(name -> name.startsWith("meteor-translation-addon--")),
59+
"Missing fixture jar for meteor-translation-addon");
60+
assertTrue(
61+
jarNames.stream().anyMatch(name -> name.startsWith("Trouser-Streak--")),
62+
"Missing fixture jar for Trouser-Streak");
63+
64+
// Known addon side-effect artifacts that may be created during scanning.
65+
List<Path> sideEffectPaths =
66+
List.of(
67+
Path.of("TrouserStreak"),
68+
Path.of("nora-tweaks"),
69+
Path.of("nora-tweaks-categories.json"));
70+
Set<Path> initiallyMissingSideEffects =
71+
sideEffectPaths.stream().filter(path -> !Files.exists(path)).collect(Collectors.toSet());
72+
73+
List<String> failures = new ArrayList<>();
74+
try (IsolatedScanner scanner = new IsolatedScanner(TEST_TIMEOUT_SECONDS)) {
75+
for (Path jar : jars) {
76+
JarScanResult result = scanner.scan(jar);
77+
if (!result.success) {
78+
failures.add(jar.getFileName() + " => " + String.join(" | ", result.errors));
79+
}
80+
}
81+
} finally {
82+
// Clean up any addon side-effect artifacts created during the test
83+
for (Path sideEffectPath : initiallyMissingSideEffects) {
84+
deleteRecursively(sideEffectPath);
85+
}
86+
}
87+
88+
assertTrue(failures.isEmpty(), "Isolated scan failures:\n" + String.join("\n", failures));
89+
}
90+
91+
@Test
92+
void scanNonExistentJarReturnsFailure() throws Exception {
93+
try (IsolatedScanner scanner = new IsolatedScanner(10)) {
94+
Path nonExistent = Path.of("non-existent-jar-" + System.currentTimeMillis() + ".jar");
95+
JarScanResult result = scanner.scan(nonExistent);
96+
97+
assertFalse(result.success, "Scan of non-existent jar should fail");
98+
assertFalse(result.errors.isEmpty(), "Should have error messages");
99+
}
100+
}
101+
102+
@Test
103+
void scanInvalidFileReturnsFailure() throws Exception {
104+
try (IsolatedScanner scanner = new IsolatedScanner(10)) {
105+
// Create a temporary file that is not a valid JAR
106+
Path tempFile = Files.createTempFile("invalid-", ".jar");
107+
try {
108+
Files.writeString(tempFile, "This is not a valid JAR file");
109+
JarScanResult result = scanner.scan(tempFile);
110+
111+
assertFalse(result.success, "Scan of invalid JAR should fail");
112+
assertFalse(result.errors.isEmpty(), "Should have error messages");
113+
} finally {
114+
Files.deleteIfExists(tempFile);
115+
}
116+
}
117+
}
118+
119+
@Test
120+
void scanResultContainsExpectedMetadata() throws Exception {
121+
Path jarDir = Path.of(System.getProperty("addonparser.fixtureJarsDir", "fixtures/addons/jars"));
122+
if (!Files.isDirectory(jarDir)) {
123+
// Skip if fixtures not available
124+
return;
125+
}
126+
127+
// Find a known fixture jar with predictable metadata
128+
List<Path> jars =
129+
Files.list(jarDir)
130+
.filter(p -> p.getFileName().toString().startsWith("meteor-satellite-addon--"))
131+
.toList();
132+
133+
if (jars.isEmpty()) {
134+
return; // Skip if fixture not available
135+
}
136+
137+
try (IsolatedScanner scanner = new IsolatedScanner(TEST_TIMEOUT_SECONDS)) {
138+
JarScanResult result = scanner.scan(jars.get(0));
139+
140+
assertTrue(result.success, "Scan should succeed: " + String.join(", ", result.errors));
141+
assertNotNull(result.jarName, "jarName should be set");
142+
assertNotNull(result.jarPath, "jarPath should be set");
143+
assertTrue(result.jarName.startsWith("meteor-satellite-addon--"), "Unexpected jar name: " + result.jarName);
144+
145+
// Verify addon metadata is populated
146+
assertFalse(result.addons.isEmpty(), "Should have at least one addon");
147+
assertFalse(result.modules.isEmpty(), "Should have at least one module");
148+
149+
// Verify addon structure
150+
var addon = result.addons.get(0);
151+
assertNotNull(addon.name, "Addon name should be set");
152+
assertNotNull(addon.packageName, "Addon packageName should be set");
153+
assertNotNull(addon.entrypoint, "Addon entrypoint should be set");
154+
155+
// Verify module structure
156+
var module = result.modules.get(0);
157+
assertNotNull(module.name, "Module name should be set");
158+
assertNotNull(module.category, "Module category should be set");
159+
}
160+
}
161+
162+
@Test
163+
void defaultTimeoutIsUsed() {
164+
// Verify default constructor uses expected timeout
165+
IsolatedScanner defaultScanner = new IsolatedScanner();
166+
// The scanner doesn't expose the timeout, but we can verify it doesn't throw
167+
assertNotNull(defaultScanner);
168+
defaultScanner.close();
169+
}
170+
171+
@Test
172+
void customTimeoutIsAccepted() {
173+
// Verify custom timeout constructor works
174+
IsolatedScanner customScanner = new IsolatedScanner(60);
175+
assertNotNull(customScanner);
176+
customScanner.close();
177+
}
178+
179+
private static void deleteRecursively(Path root) {
180+
if (root == null || !Files.exists(root)) return;
181+
try (Stream<Path> stream = Files.walk(root)) {
182+
List<Path> toDelete = stream.sorted(Comparator.reverseOrder()).toList();
183+
for (Path path : toDelete) {
184+
try {
185+
Files.deleteIfExists(path);
186+
} catch (IOException e) {
187+
// Best-effort cleanup in test
188+
}
189+
}
190+
} catch (IOException e) {
191+
// Walk failure during test cleanup - non-fatal
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)