|
| 1 | +package io.github.randomcodespace.iq.e2e; |
| 2 | + |
| 3 | +import io.github.randomcodespace.iq.analyzer.AnalysisResult; |
| 4 | +import io.github.randomcodespace.iq.analyzer.Analyzer; |
| 5 | +import io.github.randomcodespace.iq.analyzer.FileDiscovery; |
| 6 | +import io.github.randomcodespace.iq.analyzer.LayerClassifier; |
| 7 | +import io.github.randomcodespace.iq.analyzer.StructuredParser; |
| 8 | +import io.github.randomcodespace.iq.analyzer.linker.EntityLinker; |
| 9 | +import io.github.randomcodespace.iq.analyzer.linker.Linker; |
| 10 | +import io.github.randomcodespace.iq.analyzer.linker.ModuleContainmentLinker; |
| 11 | +import io.github.randomcodespace.iq.analyzer.linker.TopicLinker; |
| 12 | +import io.github.randomcodespace.iq.config.CodeIqConfig; |
| 13 | +import io.github.randomcodespace.iq.detector.Detector; |
| 14 | +import io.github.randomcodespace.iq.detector.DetectorRegistry; |
| 15 | +import io.github.randomcodespace.iq.model.CodeNode; |
| 16 | +import io.github.randomcodespace.iq.model.NodeKind; |
| 17 | +import org.junit.jupiter.api.Tag; |
| 18 | +import org.junit.jupiter.api.Test; |
| 19 | +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; |
| 20 | +import org.springframework.beans.factory.config.BeanDefinition; |
| 21 | +import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; |
| 22 | +import org.springframework.core.type.filter.AssignableTypeFilter; |
| 23 | + |
| 24 | +import java.nio.file.Files; |
| 25 | +import java.nio.file.Path; |
| 26 | +import java.util.ArrayList; |
| 27 | +import java.util.List; |
| 28 | +import java.util.Set; |
| 29 | +import java.util.stream.Collectors; |
| 30 | + |
| 31 | +import static org.junit.jupiter.api.Assertions.*; |
| 32 | + |
| 33 | +/** |
| 34 | + * Integration smoke tests that validate the analysis pipeline against |
| 35 | + * diverse real-world test repositories. Verifies no crashes, reasonable |
| 36 | + * node/edge counts, correct language detection, and basic sanity. |
| 37 | + * |
| 38 | + * <p>Enabled when {@code INTEGRATION_TEST_DIR} env var points to a directory |
| 39 | + * containing cloned test repos (terraform-vpc, kit, fastapi, kafka, nest, spring-boot). |
| 40 | + * |
| 41 | + * <pre> |
| 42 | + * INTEGRATION_TEST_DIR=/home/sandbox/projects/testDir \ |
| 43 | + * mvn test -Dtest="IntegrationSmokeTest" -pl . |
| 44 | + * </pre> |
| 45 | + */ |
| 46 | +@Tag("integration") |
| 47 | +@EnabledIfEnvironmentVariable(named = "INTEGRATION_TEST_DIR", matches = ".+") |
| 48 | +class IntegrationSmokeTest { |
| 49 | + |
| 50 | + private static Analyzer buildAnalyzer() { |
| 51 | + List<Detector> detectors = discoverDetectors(); |
| 52 | + var registry = new DetectorRegistry(detectors); |
| 53 | + var parser = new StructuredParser(); |
| 54 | + var config = new CodeIqConfig(); |
| 55 | + var fileDiscovery = new FileDiscovery(config); |
| 56 | + var layerClassifier = new LayerClassifier(); |
| 57 | + List<Linker> linkers = List.of( |
| 58 | + new EntityLinker(), |
| 59 | + new ModuleContainmentLinker(), |
| 60 | + new TopicLinker() |
| 61 | + ); |
| 62 | + return new Analyzer(registry, parser, fileDiscovery, layerClassifier, linkers, config); |
| 63 | + } |
| 64 | + |
| 65 | + @SuppressWarnings("unchecked") |
| 66 | + private static List<Detector> discoverDetectors() { |
| 67 | + var scanner = new ClassPathScanningCandidateComponentProvider(false); |
| 68 | + scanner.addIncludeFilter(new AssignableTypeFilter(Detector.class)); |
| 69 | + List<Detector> detectors = new ArrayList<>(); |
| 70 | + for (BeanDefinition bd : scanner.findCandidateComponents("io.github.randomcodespace.iq.detector")) { |
| 71 | + try { |
| 72 | + Class<? extends Detector> clazz = (Class<? extends Detector>) Class.forName(bd.getBeanClassName()); |
| 73 | + if (!clazz.isInterface() && !java.lang.reflect.Modifier.isAbstract(clazz.getModifiers())) { |
| 74 | + detectors.add(clazz.getDeclaredConstructor().newInstance()); |
| 75 | + } |
| 76 | + } catch (Exception e) { |
| 77 | + // Skip detectors that can't be instantiated |
| 78 | + } |
| 79 | + } |
| 80 | + return detectors; |
| 81 | + } |
| 82 | + |
| 83 | + private Path repoDir(String name) { |
| 84 | + Path base = Path.of(System.getenv("INTEGRATION_TEST_DIR")); |
| 85 | + Path repo = base.resolve(name); |
| 86 | + assertTrue(Files.isDirectory(repo), "Test repo not found: " + repo); |
| 87 | + return repo; |
| 88 | + } |
| 89 | + |
| 90 | + private AnalysisResult analyze(String repoName) { |
| 91 | + Analyzer analyzer = buildAnalyzer(); |
| 92 | + return analyzer.run(repoDir(repoName), null); |
| 93 | + } |
| 94 | + |
| 95 | + // --- Small: terraform-vpc (HCL) --- |
| 96 | + |
| 97 | + @Test |
| 98 | + void terraformVpc_producesNodes() { |
| 99 | + AnalysisResult result = analyze("terraform-vpc"); |
| 100 | + assertNotNull(result); |
| 101 | + assertTrue(result.nodeCount() >= 5, |
| 102 | + "terraform-vpc should produce at least 5 nodes, got " + result.nodeCount()); |
| 103 | + |
| 104 | + // Should detect node kinds |
| 105 | + assertNotNull(result.nodes()); |
| 106 | + Set<NodeKind> kinds = result.nodes().stream() |
| 107 | + .map(CodeNode::getKind) |
| 108 | + .collect(Collectors.toSet()); |
| 109 | + assertFalse(kinds.isEmpty(), "Should detect multiple node kinds"); |
| 110 | + } |
| 111 | + |
| 112 | + // --- Medium: kit (Go) --- |
| 113 | + |
| 114 | + @Test |
| 115 | + void kit_producesGoNodes() { |
| 116 | + AnalysisResult result = analyze("kit"); |
| 117 | + assertNotNull(result); |
| 118 | + assertTrue(result.nodeCount() >= 50, |
| 119 | + "kit (Go) should produce at least 50 nodes, got " + result.nodeCount()); |
| 120 | + |
| 121 | + // Should detect Go via language breakdown |
| 122 | + assertNotNull(result.languageBreakdown()); |
| 123 | + assertTrue(result.languageBreakdown().containsKey("go"), |
| 124 | + "kit should detect Go language, found: " + result.languageBreakdown().keySet()); |
| 125 | + } |
| 126 | + |
| 127 | + // --- Medium: fastapi (Python) --- |
| 128 | + |
| 129 | + @Test |
| 130 | + void fastapi_producesPythonNodes() { |
| 131 | + AnalysisResult result = analyze("fastapi"); |
| 132 | + assertNotNull(result); |
| 133 | + assertTrue(result.nodeCount() >= 100, |
| 134 | + "fastapi should produce at least 100 nodes, got " + result.nodeCount()); |
| 135 | + |
| 136 | + assertNotNull(result.languageBreakdown()); |
| 137 | + assertTrue(result.languageBreakdown().containsKey("python"), |
| 138 | + "fastapi should detect Python, found: " + result.languageBreakdown().keySet()); |
| 139 | + } |
| 140 | + |
| 141 | + // --- Large: kafka (Java/Scala) --- |
| 142 | + |
| 143 | + @Test |
| 144 | + void kafka_producesJavaNodes() { |
| 145 | + AnalysisResult result = analyze("kafka"); |
| 146 | + assertNotNull(result); |
| 147 | + assertTrue(result.nodeCount() >= 500, |
| 148 | + "kafka should produce at least 500 nodes, got " + result.nodeCount()); |
| 149 | + |
| 150 | + assertNotNull(result.languageBreakdown()); |
| 151 | + boolean hasJavaOrScala = result.languageBreakdown().containsKey("java") |
| 152 | + || result.languageBreakdown().containsKey("scala"); |
| 153 | + assertTrue(hasJavaOrScala, |
| 154 | + "kafka should detect Java or Scala, found: " + result.languageBreakdown().keySet()); |
| 155 | + } |
| 156 | + |
| 157 | + // --- Large: nest (TypeScript) --- |
| 158 | + |
| 159 | + @Test |
| 160 | + void nest_producesTypeScriptNodes() { |
| 161 | + AnalysisResult result = analyze("nest"); |
| 162 | + assertNotNull(result); |
| 163 | + assertTrue(result.nodeCount() >= 200, |
| 164 | + "nest should produce at least 200 nodes, got " + result.nodeCount()); |
| 165 | + |
| 166 | + assertNotNull(result.languageBreakdown()); |
| 167 | + assertTrue(result.languageBreakdown().containsKey("typescript"), |
| 168 | + "nest should detect TypeScript, found: " + result.languageBreakdown().keySet()); |
| 169 | + } |
| 170 | + |
| 171 | + // --- Monorepo: spring-boot (Java) --- |
| 172 | + |
| 173 | + @Test |
| 174 | + void springBoot_producesLargeGraph() { |
| 175 | + AnalysisResult result = analyze("spring-boot"); |
| 176 | + assertNotNull(result); |
| 177 | + assertTrue(result.nodeCount() >= 1000, |
| 178 | + "spring-boot monorepo should produce at least 1000 nodes, got " + result.nodeCount()); |
| 179 | + |
| 180 | + // Should have edges |
| 181 | + assertTrue(result.edgeCount() > 0, |
| 182 | + "spring-boot should produce edges"); |
| 183 | + |
| 184 | + // Should detect Java |
| 185 | + assertNotNull(result.languageBreakdown()); |
| 186 | + assertTrue(result.languageBreakdown().containsKey("java"), |
| 187 | + "spring-boot should detect Java, found: " + result.languageBreakdown().keySet()); |
| 188 | + } |
| 189 | + |
| 190 | + // --- Cross-cutting: edge sanity --- |
| 191 | + |
| 192 | + @Test |
| 193 | + void smallRepos_edgesReferenceValidNodes() { |
| 194 | + for (String repo : List.of("terraform-vpc", "kit")) { |
| 195 | + AnalysisResult result = analyze(repo); |
| 196 | + assertNotNull(result.nodes(), repo + " should return nodes list"); |
| 197 | + |
| 198 | + Set<String> nodeIds = result.nodes().stream() |
| 199 | + .map(CodeNode::getId) |
| 200 | + .collect(Collectors.toSet()); |
| 201 | + |
| 202 | + // Verify edges on nodes reference valid targets |
| 203 | + for (CodeNode node : result.nodes()) { |
| 204 | + for (var edge : node.getEdges()) { |
| 205 | + assertTrue(nodeIds.contains(edge.getSourceId()), |
| 206 | + repo + ": Edge source " + edge.getSourceId() + " not found in nodes"); |
| 207 | + if (edge.getTarget() != null) { |
| 208 | + assertTrue(nodeIds.contains(edge.getTarget().getId()), |
| 209 | + repo + ": Edge target " + edge.getTarget().getId() + " not found in nodes"); |
| 210 | + } |
| 211 | + } |
| 212 | + } |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + // --- Determinism --- |
| 217 | + |
| 218 | + @Test |
| 219 | + void terraformVpc_deterministicOutput() { |
| 220 | + AnalysisResult first = analyze("terraform-vpc"); |
| 221 | + AnalysisResult second = analyze("terraform-vpc"); |
| 222 | + |
| 223 | + assertEquals(first.nodeCount(), second.nodeCount(), |
| 224 | + "Node count should be deterministic"); |
| 225 | + assertEquals(first.edgeCount(), second.edgeCount(), |
| 226 | + "Edge count should be deterministic"); |
| 227 | + |
| 228 | + assertNotNull(first.nodes()); |
| 229 | + assertNotNull(second.nodes()); |
| 230 | + List<String> firstIds = first.nodes().stream() |
| 231 | + .map(CodeNode::getId).sorted().toList(); |
| 232 | + List<String> secondIds = second.nodes().stream() |
| 233 | + .map(CodeNode::getId).sorted().toList(); |
| 234 | + assertEquals(firstIds, secondIds, |
| 235 | + "Node IDs should be identical across runs"); |
| 236 | + } |
| 237 | +} |
0 commit comments