Analyze cloned repos from source without running Maven or Gradle on the target project.
Parse build files statically for module wiring, use the exporter's bundled Spring classpath for type bindings, optionally include pre-built generated main sources, and exclude test output and test-scoped dependencies from analysis. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -161,7 +161,7 @@ public class RegressionTest {
|
||||
if (scenario.inputPath() == null) System.out.println("inputPath is NULL");
|
||||
if (tempDir == null) System.out.println("tempDir is NULL");
|
||||
|
||||
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
|
||||
exportService.runExporter(scenario.inputPath(), tempDir, List.of("puml", "dot", "scxml", "json"), true, scenario.activeProfiles(), null, null, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
||||
|
||||
// Find the generated directory (it might be named with FQN)
|
||||
List<Path> generatedDirs;
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class GeneratedSourceDiscoveryTest {
|
||||
|
||||
private final GeneratedSourceDiscovery discovery = new GeneratedSourceDiscovery();
|
||||
|
||||
@Nested
|
||||
class MavenTarget {
|
||||
|
||||
@Test
|
||||
void shouldDiscoverMapStructLikeSourcesUnderTarget(@TempDir Path tempDir) throws IOException {
|
||||
Path module = tempDir.resolve("service");
|
||||
Path generated = module.resolve("target/generated-sources/annotations/com/acme/mapper");
|
||||
Files.createDirectories(generated);
|
||||
Files.writeString(generated.resolve("OrderMapperImpl.java"), """
|
||||
package com.acme.mapper;
|
||||
public class OrderMapperImpl implements OrderMapper {}
|
||||
""");
|
||||
|
||||
assertThat(discovery.discoverSourceRoots(module))
|
||||
.containsExactly(module.resolve("target").normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreCompiledClassesUnderTarget(@TempDir Path tempDir) throws IOException {
|
||||
Path module = tempDir.resolve("service");
|
||||
Path classes = module.resolve("target/classes/com/acme");
|
||||
Files.createDirectories(classes);
|
||||
Files.writeString(classes.resolve("Accidental.java"), "class Accidental {}");
|
||||
|
||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
||||
assertThat(GeneratedSourceDiscovery.isExcludedBuildPath(module, classes.resolve("Accidental.java")))
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GradleBuild {
|
||||
|
||||
@Test
|
||||
void shouldDiscoverAnnotationProcessorOutput(@TempDir Path tempDir) throws IOException {
|
||||
Path module = tempDir.resolve("app");
|
||||
Path generated = module.resolve(
|
||||
"build/generated/sources/annotationProcessor/java/main/com/acme/config");
|
||||
Files.createDirectories(generated);
|
||||
Files.writeString(generated.resolve("ApplicationConfigImpl.java"), """
|
||||
package com.acme.config;
|
||||
public class ApplicationConfigImpl {}
|
||||
""");
|
||||
|
||||
assertThat(discovery.discoverSourceRoots(module))
|
||||
.containsExactly(module.resolve("build").normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreGradleBuildClasses(@TempDir Path tempDir) throws IOException {
|
||||
Path module = tempDir.resolve("app");
|
||||
Path classes = module.resolve("build/classes/java/main/com/acme");
|
||||
Files.createDirectories(classes);
|
||||
Files.writeString(classes.resolve("ShouldSkip.java"), "class ShouldSkip {}");
|
||||
|
||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreGradleTestAnnotationProcessorOutput(@TempDir Path tempDir) throws IOException {
|
||||
Path module = tempDir.resolve("app");
|
||||
Path generated = module.resolve(
|
||||
"build/generated/sources/annotationProcessor/java/test/com/acme/testsupport");
|
||||
Files.createDirectories(generated);
|
||||
Files.writeString(generated.resolve("TestSupportImpl.java"), "class TestSupportImpl {}");
|
||||
|
||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class TestOutputExclusion {
|
||||
|
||||
@Test
|
||||
void shouldIgnoreMavenGeneratedTestSources(@TempDir Path tempDir) throws IOException {
|
||||
Path module = tempDir.resolve("service");
|
||||
Path generated = module.resolve("target/generated-test-sources/test-annotations/com/acme");
|
||||
Files.createDirectories(generated);
|
||||
Files.writeString(generated.resolve("TestMapperImpl.java"), "class TestMapperImpl {}");
|
||||
|
||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreTestClassesUnderTarget(@TempDir Path tempDir) throws IOException {
|
||||
Path module = tempDir.resolve("service");
|
||||
Path testClasses = module.resolve("target/test-classes/com/acme");
|
||||
Files.createDirectories(testClasses);
|
||||
Files.writeString(testClasses.resolve("OrderServiceTest.java"), "class OrderServiceTest {}");
|
||||
|
||||
assertThat(discovery.discoverSourceRoots(module)).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Integration {
|
||||
|
||||
@Test
|
||||
void shouldAddGeneratedRootsToProjectScanPaths(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("monorepo");
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
Files.writeString(root.resolve("settings.gradle"), "include 'api'");
|
||||
Path api = root.resolve("api");
|
||||
Files.createDirectories(api);
|
||||
Files.writeString(api.resolve("build.gradle"), "");
|
||||
Path generated = api.resolve("target/generated-sources/annotations/com/example");
|
||||
Files.createDirectories(generated);
|
||||
Files.writeString(generated.resolve("GeneratedBean.java"), """
|
||||
package com.example;
|
||||
public class GeneratedBean {}
|
||||
""");
|
||||
|
||||
StaticBuildFileAnalyzer analyzer = new StaticBuildFileAnalyzer();
|
||||
ProjectModuleGraph graph = analyzer.analyze(root);
|
||||
|
||||
assertThat(graph.resolveScanPaths(root)).contains(api.resolve("target").normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSkipGeneratedRootsWhenDisabled(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("monorepo");
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
Files.writeString(root.resolve("settings.gradle"), "include 'api'");
|
||||
Path api = root.resolve("api");
|
||||
Files.createDirectories(api);
|
||||
Files.writeString(api.resolve("build.gradle"), "");
|
||||
Path generated = api.resolve("target/generated-sources/annotations/com/example");
|
||||
Files.createDirectories(generated);
|
||||
Files.writeString(generated.resolve("GeneratedBean.java"), "package com.example; class GeneratedBean {}");
|
||||
|
||||
ProjectModuleGraph graph = new StaticBuildFileAnalyzer().analyze(root);
|
||||
|
||||
assertThat(graph.resolveScanPaths(root, false)).doesNotContain(api.resolve("target").normalize());
|
||||
assertThat(graph.resolveScanPaths(root, true)).contains(api.resolve("target").normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIndexGeneratedTypesWhenScanningModule() throws IOException {
|
||||
Path module = Files.createTempDirectory("sme-gen-src").resolve("service");
|
||||
Path generated = module.resolve("target/generated-sources/annotations/com/acme");
|
||||
Files.createDirectories(generated);
|
||||
Files.writeString(generated.resolve("GeneratedEvent.java"), """
|
||||
package com.acme;
|
||||
public enum GeneratedEvent { PAY, SHIP }
|
||||
""");
|
||||
|
||||
CodebaseContext context = new CodebaseContext();
|
||||
context.scan(Set.of(module), Set.of());
|
||||
|
||||
assertThat(context.getCompilationUnits()).hasSize(1);
|
||||
assertThat(context.getEnumValues("GeneratedEvent"))
|
||||
.contains("com.acme.GeneratedEvent.PAY", "com.acme.GeneratedEvent.SHIP");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.resolver;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class StaticBuildFileAnalyzerTest {
|
||||
|
||||
private final StaticBuildFileAnalyzer analyzer = new StaticBuildFileAnalyzer();
|
||||
|
||||
@Nested
|
||||
class GradleIncludes {
|
||||
|
||||
@Test
|
||||
void shouldParseSettingsGradleIncludeList(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("monorepo");
|
||||
Files.createDirectories(root);
|
||||
Files.writeString(root.resolve("settings.gradle"), """
|
||||
rootProject.name = 'monorepo'
|
||||
include 'module-a', 'module-b', 'module-c'
|
||||
""");
|
||||
createGradleModule(root, "module-a");
|
||||
createGradleModule(root, "module-b");
|
||||
createGradleModule(root, "module-c");
|
||||
|
||||
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
|
||||
|
||||
assertThat(includes).hasSize(3);
|
||||
assertThat(includes.get("module-a")).isEqualTo(root.resolve("module-a").normalize());
|
||||
assertThat(includes.get("module-b")).isEqualTo(root.resolve("module-b").normalize());
|
||||
assertThat(includes.get("module-c")).isEqualTo(root.resolve("module-c").normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseSettingsGradleKtsIncludeCall(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("monorepo");
|
||||
Files.createDirectories(root);
|
||||
Files.writeString(root.resolve("settings.gradle.kts"), """
|
||||
include(":api", ":core")
|
||||
""");
|
||||
createGradleModule(root, "api");
|
||||
createGradleModule(root, "core");
|
||||
|
||||
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
|
||||
|
||||
assertThat(includes).containsKeys("api", "core");
|
||||
assertThat(includes.get("api")).isEqualTo(root.resolve("api").normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveNestedIncludePaths(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("monorepo");
|
||||
Path nested = root.resolve("state_machines/multi/api");
|
||||
Files.createDirectories(nested);
|
||||
Files.writeString(root.resolve("settings.gradle"), """
|
||||
include ':state_machines:multi:api', ':state_machines:multi:core'
|
||||
""");
|
||||
createGradleModule(root, "state_machines/multi/api");
|
||||
createGradleModule(root, "state_machines/multi/core");
|
||||
|
||||
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
|
||||
|
||||
assertThat(includes.get("state_machines:multi:api"))
|
||||
.isEqualTo(root.resolve("state_machines/multi/api").normalize());
|
||||
assertThat(includes.get("state_machines:multi:core"))
|
||||
.isEqualTo(root.resolve("state_machines/multi/core").normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHonorCustomProjectDirMapping(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("monorepo");
|
||||
Path custom = root.resolve("custom-location/service");
|
||||
Files.createDirectories(custom);
|
||||
Files.writeString(root.resolve("settings.gradle"), """
|
||||
include ':service'
|
||||
project(':service').projectDir = file('custom-location/service')
|
||||
""");
|
||||
createGradleModule(custom, ".");
|
||||
|
||||
Map<String, Path> includes = analyzer.parseGradleIncludes(root);
|
||||
|
||||
assertThat(includes.get("service")).isEqualTo(custom.normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDiscoverAllIncludedModulesWhenScanningFromRoot(@TempDir Path tempDir) throws IOException {
|
||||
Path root = writeGradleMonorepo(tempDir);
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
|
||||
Set<Path> scanPaths = graph.resolveScanPaths(root);
|
||||
|
||||
assertThat(scanPaths).containsExactlyInAnyOrder(
|
||||
root.normalize(),
|
||||
root.resolve("module-a").normalize(),
|
||||
root.resolve("module-b").normalize(),
|
||||
root.resolve("module-c").normalize());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class GradleDependencyGraph {
|
||||
|
||||
@Test
|
||||
void shouldBuildTransitiveProjectDependencyGraph(@TempDir Path tempDir) throws IOException {
|
||||
Path root = writeGradleMonorepo(tempDir);
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
|
||||
|
||||
List<ProjectModuleGraph.ModuleDependency> localDeps = graph.dependencies().stream()
|
||||
.filter(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT)
|
||||
.toList();
|
||||
|
||||
assertThat(localDeps).anyMatch(dep ->
|
||||
dep.fromModuleId().contains("module-a") && dep.toModuleId().contains("module-b"));
|
||||
assertThat(localDeps).anyMatch(dep ->
|
||||
dep.fromModuleId().contains("module-b") && dep.toModuleId().contains("module-c"));
|
||||
|
||||
Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(graph, root.resolve("module-a"));
|
||||
assertThat(siblings).containsExactlyInAnyOrder(
|
||||
root.resolve("module-b").normalize(),
|
||||
root.resolve("module-c").normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldParseBuildGradleKtsProjectDependencies(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("monorepo");
|
||||
Files.createDirectories(root);
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
Files.writeString(root.resolve("settings.gradle.kts"), "include(\":app\", \":lib\")");
|
||||
Files.createDirectories(root.resolve("app"));
|
||||
Files.writeString(root.resolve("app/build.gradle.kts"), """
|
||||
dependencies {
|
||||
implementation(project(":lib"))
|
||||
}
|
||||
""");
|
||||
Files.createDirectories(root.resolve("lib"));
|
||||
Files.writeString(root.resolve("lib/build.gradle.kts"), "");
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
|
||||
boolean hasLibDep = graph.dependencies().stream()
|
||||
.anyMatch(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT
|
||||
&& dep.fromModuleId().contains("app")
|
||||
&& dep.toModuleId().contains("lib"));
|
||||
assertThat(hasLibDep).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreTestImplementationProjectDependencies(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("monorepo");
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
Files.writeString(root.resolve("settings.gradle"), "include 'app', 'lib', 'test-lib'");
|
||||
createGradleModule(root, "app");
|
||||
createGradleModule(root, "lib");
|
||||
createGradleModule(root, "test-lib");
|
||||
Files.writeString(root.resolve("app/build.gradle"), """
|
||||
dependencies {
|
||||
implementation project(':lib')
|
||||
testImplementation project(':test-lib')
|
||||
}
|
||||
""");
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
|
||||
List<ProjectModuleGraph.ModuleDependency> localDeps = graph.dependencies().stream()
|
||||
.filter(dep -> dep.kind() == ProjectModuleGraph.DependencyKind.LOCAL_PROJECT)
|
||||
.toList();
|
||||
|
||||
assertThat(localDeps).anyMatch(dep -> dep.toModuleId().contains("lib"));
|
||||
assertThat(localDeps).noneMatch(dep -> dep.toModuleId().contains("test-lib"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class MavenCoordinates {
|
||||
|
||||
@Test
|
||||
void shouldIndexModulesByGroupIdAndArtifactId(@TempDir Path tempDir) throws IOException {
|
||||
Path root = writeMavenMultiModule(tempDir);
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("module-a"));
|
||||
|
||||
assertThat(graph.modulesByCoordinates()).containsKeys(
|
||||
new ProjectModuleGraph.MavenCoordinates("com.acme", "module-a"),
|
||||
new ProjectModuleGraph.MavenCoordinates("com.acme", "module-b"),
|
||||
new ProjectModuleGraph.MavenCoordinates("com.acme", "module-c"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDistinguishSameArtifactIdUnderDifferentGroups(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("repo");
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
writePom(root.resolve("pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.repo</groupId>
|
||||
<artifactId>parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>team-a/api</module>
|
||||
<module>team-b/api</module>
|
||||
<module>consumer</module>
|
||||
</modules>
|
||||
</project>
|
||||
""");
|
||||
writePom(root.resolve("team-a/api/pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.team.a</groupId>
|
||||
<artifactId>api</artifactId>
|
||||
</project>
|
||||
""");
|
||||
writePom(root.resolve("team-b/api/pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.team.b</groupId>
|
||||
<artifactId>api</artifactId>
|
||||
</project>
|
||||
""");
|
||||
writePom(root.resolve("consumer/pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.consumer</groupId>
|
||||
<artifactId>consumer</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.team.a</groupId>
|
||||
<artifactId>api</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
""");
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("consumer"));
|
||||
|
||||
List<ProjectModuleGraph.ModuleDependency> deps = graph.localDependenciesOf(
|
||||
graph.modulesByCoordinates()
|
||||
.get(new ProjectModuleGraph.MavenCoordinates("com.consumer", "consumer"))
|
||||
.id());
|
||||
|
||||
assertThat(deps).hasSize(1);
|
||||
assertThat(deps.get(0).toModuleId()).isEqualTo(
|
||||
graph.modulesByCoordinates().get(new ProjectModuleGraph.MavenCoordinates("com.team.a", "api")).id());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMarkExternalLibraryWhenCoordinatesAreNotInRepo(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("repo");
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
writePom(root.resolve("service/pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.acme</groupId>
|
||||
<artifactId>service</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
<version>3.2.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
""");
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("service"));
|
||||
String serviceId = graph.modulesByCoordinates()
|
||||
.get(new ProjectModuleGraph.MavenCoordinates("com.acme", "service")).id();
|
||||
|
||||
assertThat(graph.dependencies()).anyMatch(dep ->
|
||||
dep.fromModuleId().equals(serviceId)
|
||||
&& dep.kind() == ProjectModuleGraph.DependencyKind.EXTERNAL_LIBRARY
|
||||
&& dep.externalCoordinates().key().equals("org.springframework.boot:spring-boot-starter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreTestScopedMavenDependencies(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("repo");
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
writePom(root.resolve("pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.repo</groupId>
|
||||
<artifactId>parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<modules><module>app</module><module>test-support</module></modules>
|
||||
</project>
|
||||
""");
|
||||
writePom(root.resolve("app/pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.repo</groupId>
|
||||
<artifactId>app</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.repo</groupId>
|
||||
<artifactId>test-support</artifactId>
|
||||
<version>1.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
""");
|
||||
writePom(root.resolve("test-support/pom.xml"), """
|
||||
<project><groupId>com.repo</groupId><artifactId>test-support</artifactId></project>
|
||||
""");
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(root.resolve("app"));
|
||||
String appId = graph.modulesByCoordinates()
|
||||
.get(new ProjectModuleGraph.MavenCoordinates("com.repo", "app")).id();
|
||||
|
||||
assertThat(graph.localDependenciesOf(appId)).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class MavenModules {
|
||||
|
||||
@Test
|
||||
void shouldDiscoverAggregatorModules(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("aggregator");
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
writePom(root.resolve("pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.acme</groupId>
|
||||
<artifactId>root</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>service-x</module>
|
||||
<module>service-y</module>
|
||||
</modules>
|
||||
</project>
|
||||
""");
|
||||
writePom(root.resolve("service-x/pom.xml"), """
|
||||
<project><groupId>com.acme</groupId><artifactId>service-x</artifactId></project>
|
||||
""");
|
||||
writePom(root.resolve("service-y/pom.xml"), """
|
||||
<project><groupId>com.acme</groupId><artifactId>service-y</artifactId></project>
|
||||
""");
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(root);
|
||||
Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(graph, root);
|
||||
|
||||
assertThat(siblings).containsExactlyInAnyOrder(
|
||||
root.resolve("service-x").normalize(),
|
||||
root.resolve("service-y").normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveTransitiveMavenModuleDependencies(@TempDir Path tempDir) throws IOException {
|
||||
Path root = writeMavenMultiModule(tempDir);
|
||||
|
||||
Set<Path> siblings = new SiblingDependencyResolver().resolveSiblings(root.resolve("module-a"));
|
||||
|
||||
assertThat(siblings).containsExactlyInAnyOrder(
|
||||
root.resolve("module-b").normalize(),
|
||||
root.resolve("module-c").normalize(),
|
||||
root.normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveParentViaRelativePath(@TempDir Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("root");
|
||||
Path child = tempDir.resolve("child");
|
||||
Files.createDirectories(root);
|
||||
Files.createDirectories(child);
|
||||
writePom(root.resolve("pom.xml"), """
|
||||
<project><groupId>com.acme</groupId><artifactId>root</artifactId></project>
|
||||
""");
|
||||
writePom(child.resolve("pom.xml"), """
|
||||
<project>
|
||||
<parent>
|
||||
<relativePath>../root/pom.xml</relativePath>
|
||||
</parent>
|
||||
<artifactId>child</artifactId>
|
||||
</project>
|
||||
""");
|
||||
|
||||
Set<Path> scanPaths = analyzer.analyze(child).resolveScanPaths(child);
|
||||
|
||||
assertThat(scanPaths).contains(root.toAbsolutePath().normalize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPreferNestedMavenAggregatorOverOuterGradleRoot(@TempDir Path tempDir) throws IOException {
|
||||
Path outerGradle = tempDir.resolve("outer");
|
||||
Path nestedMaven = outerGradle.resolve("nested-maven");
|
||||
Files.createDirectories(outerGradle.resolve(".git"));
|
||||
Files.writeString(outerGradle.resolve("settings.gradle"), "rootProject.name = 'outer'");
|
||||
writePom(nestedMaven.resolve("pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.nested</groupId>
|
||||
<artifactId>parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<modules><module>api</module></modules>
|
||||
</project>
|
||||
""");
|
||||
writePom(nestedMaven.resolve("api/pom.xml"), """
|
||||
<project><groupId>com.nested</groupId><artifactId>api</artifactId></project>
|
||||
""");
|
||||
|
||||
ProjectModuleGraph graph = analyzer.analyze(nestedMaven.resolve("api"));
|
||||
|
||||
assertThat(graph.workspaceRoot()).isEqualTo(nestedMaven.normalize());
|
||||
assertThat(graph.allModuleDirectories()).containsExactlyInAnyOrder(
|
||||
nestedMaven.resolve("api").normalize(),
|
||||
nestedMaven.normalize());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ExtractHelpers {
|
||||
|
||||
@Test
|
||||
void shouldExtractGradleIncludeTokensFromMixedSyntax() {
|
||||
String settings = """
|
||||
pluginManagement {}
|
||||
include ':api'
|
||||
include(":core", ":infra")
|
||||
include 'web', 'worker'
|
||||
""";
|
||||
|
||||
assertThat(StaticBuildFileAnalyzer.extractGradleIncludePaths(settings))
|
||||
.containsExactlyInAnyOrder("api", "core", "infra", "web", "worker");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExtractOwnMavenCoordinatesSkippingParentSection() {
|
||||
String pom = """
|
||||
<project>
|
||||
<parent>
|
||||
<groupId>com.parent</groupId>
|
||||
<artifactId>parent</artifactId>
|
||||
</parent>
|
||||
<artifactId>child</artifactId>
|
||||
<groupId>com.child</groupId>
|
||||
</project>
|
||||
""";
|
||||
|
||||
assertThat(StaticBuildFileAnalyzer.extractOwnMavenCoordinates(pom))
|
||||
.contains(new ProjectModuleGraph.MavenCoordinates("com.child", "child"));
|
||||
}
|
||||
}
|
||||
|
||||
private static Path writeGradleMonorepo(Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("my-root-project");
|
||||
Files.createDirectories(root);
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
Files.writeString(root.resolve("settings.gradle"), "include 'module-a', 'module-b', 'module-c'");
|
||||
createGradleModule(root, "module-a");
|
||||
createGradleModule(root, "module-b");
|
||||
createGradleModule(root, "module-c");
|
||||
Files.writeString(root.resolve("module-a/build.gradle"), "dependencies { implementation project(':module-b') }");
|
||||
Files.writeString(root.resolve("module-b/build.gradle"), "dependencies { implementation project(':module-c') }");
|
||||
return root;
|
||||
}
|
||||
|
||||
private static Path writeMavenMultiModule(Path tempDir) throws IOException {
|
||||
Path root = tempDir.resolve("my-maven-root");
|
||||
Files.createDirectories(root.resolve(".git"));
|
||||
writePom(root.resolve("pom.xml"), """
|
||||
<project><groupId>com.acme</groupId><artifactId>root</artifactId></project>
|
||||
""");
|
||||
writePom(root.resolve("module-a/pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.acme</groupId>
|
||||
<artifactId>module-a</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.acme</groupId>
|
||||
<artifactId>module-b</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
""");
|
||||
writePom(root.resolve("module-b/pom.xml"), """
|
||||
<project>
|
||||
<groupId>com.acme</groupId>
|
||||
<artifactId>module-b</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.acme</groupId>
|
||||
<artifactId>module-c</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
""");
|
||||
writePom(root.resolve("module-c/pom.xml"), """
|
||||
<project><groupId>com.acme</groupId><artifactId>module-c</artifactId></project>
|
||||
""");
|
||||
return root;
|
||||
}
|
||||
|
||||
private static void createGradleModule(Path root, String modulePath) throws IOException {
|
||||
Path moduleDir = ".".equals(modulePath) ? root : root.resolve(modulePath);
|
||||
Files.createDirectories(moduleDir);
|
||||
Path buildFile = moduleDir.resolve("build.gradle");
|
||||
if (!Files.exists(buildFile)) {
|
||||
Files.writeString(buildFile, "");
|
||||
}
|
||||
}
|
||||
|
||||
private static void writePom(Path pomFile, String content) throws IOException {
|
||||
Files.createDirectories(pomFile.getParent());
|
||||
Files.writeString(pomFile, content);
|
||||
}
|
||||
}
|
||||
@@ -1,78 +1,31 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.service;
|
||||
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Tag("dynamic_classpath_resolver")
|
||||
class DynamicClasspathResolverTest {
|
||||
|
||||
@Test
|
||||
void shouldResolveMavenClasspathDynamically(@TempDir Path tempDir) throws IOException {
|
||||
// Create dummy pom.xml
|
||||
void shouldNeverResolveExternalJarsEvenWhenMavenProject(@TempDir Path tempDir) throws Exception {
|
||||
Files.createFile(tempDir.resolve("pom.xml"));
|
||||
|
||||
// Mock the resolver so it doesn't actually run maven, but writes the expected output file
|
||||
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
|
||||
@Override
|
||||
protected void runProcess(ProcessBuilder pb) throws IOException {
|
||||
// Pretend maven executed and generated the cp.txt
|
||||
String cp = "/fake/m2/repo/spring-core.jar" + File.pathSeparator + "/fake/m2/repo/spring-context.jar";
|
||||
|
||||
String outputFileArg = pb.command().stream()
|
||||
.filter(arg -> arg.startsWith("-Dmdep.outputFile="))
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
Path outputFile = pb.directory().toPath().resolve(outputFileArg.substring("-Dmdep.outputFile=".length()));
|
||||
|
||||
Files.writeString(outputFile, cp);
|
||||
}
|
||||
};
|
||||
|
||||
List<String> classpath = resolver.resolveClasspath(tempDir);
|
||||
|
||||
assertThat(classpath).containsExactly(
|
||||
"/fake/m2/repo/spring-core.jar",
|
||||
"/fake/m2/repo/spring-context.jar"
|
||||
);
|
||||
assertThat(new DynamicClasspathResolver().resolveClasspath(tempDir)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveGradleClasspathDynamically(@TempDir Path tempDir) throws IOException {
|
||||
// Create dummy build.gradle
|
||||
void shouldNeverResolveExternalJarsEvenWhenGradleProject(@TempDir Path tempDir) throws Exception {
|
||||
Files.createFile(tempDir.resolve("build.gradle"));
|
||||
|
||||
DynamicClasspathResolver resolver = new DynamicClasspathResolver() {
|
||||
@Override
|
||||
protected String runProcessAndGetOutput(ProcessBuilder pb) {
|
||||
// Pretend gradle executed and printed the marker
|
||||
String cp = "/fake/gradle/caches/spring-core.jar" + File.pathSeparator + "/fake/gradle/caches/spring-context.jar";
|
||||
return "Some noisy gradle output\n" +
|
||||
"SME_CLASSPATH_MARKER:" + cp + "\n" +
|
||||
"BUILD SUCCESSFUL\n";
|
||||
}
|
||||
};
|
||||
|
||||
List<String> classpath = resolver.resolveClasspath(tempDir);
|
||||
|
||||
assertThat(classpath).containsExactly(
|
||||
"/fake/gradle/caches/spring-core.jar",
|
||||
"/fake/gradle/caches/spring-context.jar"
|
||||
);
|
||||
assertThat(new DynamicClasspathResolver().resolveClasspath(tempDir)).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnEmptyListIfNoBuildFileFound(@TempDir Path tempDir) {
|
||||
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
|
||||
List<String> classpath = resolver.resolveClasspath(tempDir);
|
||||
List<String> classpath = new DynamicClasspathResolver().resolveClasspath(tempDir);
|
||||
assertThat(classpath).isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import click.kamil.springstatemachineexporter.analysis.spring.InjectionPointAnal
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringBeanRegistry;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringContextScanner;
|
||||
import click.kamil.springstatemachineexporter.analysis.spring.SpringDependencyResolver;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ToolingClasspath;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -33,12 +33,7 @@ class JdtCallGraphEngineIntegrationTest {
|
||||
context = new CodebaseContext();
|
||||
context.setProjectRoot(projectRoot);
|
||||
context.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
|
||||
|
||||
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
|
||||
List<String> cp = resolver.resolveClasspath(projectRoot);
|
||||
if (!cp.isEmpty()) {
|
||||
context.setClasspath(cp);
|
||||
}
|
||||
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
|
||||
context.setResolveBindings(true);
|
||||
|
||||
context.scan(Set.of(projectRoot), Collections.emptySet());
|
||||
@@ -290,12 +285,7 @@ class JdtCallGraphEngineIntegrationTest {
|
||||
CodebaseContext ctx = new CodebaseContext();
|
||||
ctx.setProjectRoot(projectRoot);
|
||||
ctx.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
|
||||
|
||||
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
|
||||
List<String> cp = resolver.resolveClasspath(projectRoot);
|
||||
if (!cp.isEmpty()) {
|
||||
ctx.setClasspath(cp);
|
||||
}
|
||||
ctx.setClasspath(ToolingClasspath.currentJvmJarEntries());
|
||||
ctx.setResolveBindings(true);
|
||||
ctx.scan(Set.of(projectRoot), Collections.emptySet());
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ class LayeredDispatcherSampleTest {
|
||||
ExportService exportService = new ExportService(List.of(new JsonExporter()));
|
||||
exportService.runExporter(sampleRoot, tempDir, List.of("json"), true, List.of(), null, null,
|
||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn,
|
||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn, true);
|
||||
click.kamil.springstatemachineexporter.exporter.EnumFormat.fn);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode orderMachine = readMachineJson(tempDir, "StandardOrderStateMachineConfiguration", mapper);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package click.kamil.springstatemachineexporter.analysis.spring;
|
||||
|
||||
import click.kamil.springstatemachineexporter.analysis.service.ToolingClasspath;
|
||||
import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
|
||||
import click.kamil.springstatemachineexporter.analysis.service.DynamicClasspathResolver;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -25,12 +25,7 @@ class SpringContextScannerIntegrationTest {
|
||||
context = new CodebaseContext();
|
||||
context.setProjectRoot(projectRoot);
|
||||
context.setSourcepath(List.of(projectRoot.resolve("src/main/java").toString()));
|
||||
|
||||
DynamicClasspathResolver resolver = new DynamicClasspathResolver();
|
||||
List<String> cp = resolver.resolveClasspath(projectRoot);
|
||||
if (!cp.isEmpty()) {
|
||||
context.setClasspath(cp);
|
||||
}
|
||||
context.setClasspath(ToolingClasspath.currentJvmJarEntries());
|
||||
context.setResolveBindings(true);
|
||||
|
||||
context.scan(Set.of(projectRoot), Collections.emptySet());
|
||||
|
||||
Reference in New Issue
Block a user