GH-31 - Support for optimized test execution.

Initital prototype to support optimized test execution based on the Spring Modulith application module model. The change introduces a new artifact spring-modulith-junit that extends JUnit's test execution lifecycle. It obtains the ApplicationModules model for the application and potentially skips test classes for execution in case the changes made to the application reside in modules the current test case's module does not depend on.

Co-authored-by: Lukas Dohmen <l.dohmen@yahoo.de>
Co-authored-by: David Bilge <david.bilge@gmail.com>
This commit is contained in:
Lukas Dohmen
2024-02-22 20:50:47 +01:00
committed by Oliver Drotbohm
parent db4ab8f55d
commit 8cf3920af9
19 changed files with 630 additions and 13 deletions

View File

@@ -29,7 +29,8 @@
<module>spring-modulith-runtime</module>
<module>spring-modulith-starters</module>
<module>spring-modulith-test</module>
</modules>
<module>spring-modulith-junit</module>
</modules>
<properties>

View File

@@ -63,7 +63,12 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-junit</artifactId>
<version>1.3.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -16,7 +16,6 @@
package example.order;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.test.ApplicationModuleTest;
import org.springframework.modulith.test.Scenario;
@@ -28,16 +27,16 @@ import org.springframework.modulith.test.Scenario;
@RequiredArgsConstructor
class OrderIntegrationTests {
private final OrderManagement orders;
private final OrderManagement orders;
@Test
void publishesOrderCompletion(Scenario scenario) {
@Test
void publishesOrderCompletion(Scenario scenario) {
// this is a change
var reference = new Order();
var reference = new Order();
scenario.stimulate(() -> orders.complete(reference))
.andWaitForEventOfType(OrderCompleted.class)
.matchingMappedValue(OrderCompleted::orderId, reference.getId())
.toArrive();
}
scenario.stimulate(() -> orders.complete(reference))
.andWaitForEventOfType(OrderCompleted.class)
.matchingMappedValue(OrderCompleted::orderId, reference.getId())
.toArrive();
}
}

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith</artifactId>
<version>1.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Spring Modulith - Test Junit</name>
<artifactId>spring-modulith-junit</artifactId>
<properties>
<module.name>org.springframework.modulith.junit</module.name>
</properties>
<dependencies>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.8.0.202311291450-r</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,9 @@
package org.springframework.modulith.junit;
sealed interface Change {
record JavaClassChange(String fullyQualifiedClassName) implements Change {}
record JavaTestClassChange(String fullyQualifiedClassName) implements Change {}
record OtherFileChange(String path) implements Change {}
}

View File

@@ -0,0 +1,51 @@
package org.springframework.modulith.junit;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.modulith.junit.Change.JavaClassChange;
import org.springframework.modulith.junit.Change.JavaTestClassChange;
import org.springframework.modulith.junit.Change.OtherFileChange;
import org.springframework.modulith.junit.diff.ModifiedFilePath;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
final class Changes {
private static final String STANDARD_SOURCE_DIRECTORY = "src/main/java";
private static final String STANDARD_TEST_SOURCE_DIRECTORY = "src/test/java";
private Changes() {}
static Set<Change> toChanges(Set<ModifiedFilePath> modifiedFilePaths) {
return modifiedFilePaths.stream().map(Changes::toChange).collect(Collectors.toSet());
}
static Change toChange(ModifiedFilePath modifiedFilePath) {
if ("java".equalsIgnoreCase(StringUtils.getFilenameExtension(modifiedFilePath.path()))) {
String withoutExtension = StringUtils.stripFilenameExtension(modifiedFilePath.path());
int startOfMainDir = withoutExtension.indexOf(STANDARD_SOURCE_DIRECTORY);
int startOfTestDir = withoutExtension.indexOf(STANDARD_TEST_SOURCE_DIRECTORY);
if (startOfTestDir > 0 && (startOfMainDir < 0 || startOfTestDir < startOfMainDir)) {
String fullyQualifiedClassName = ClassUtils.convertResourcePathToClassName(
withoutExtension.substring(startOfTestDir + STANDARD_TEST_SOURCE_DIRECTORY.length() + 1));
return new JavaTestClassChange(fullyQualifiedClassName);
} else if (startOfMainDir > 0 && (startOfTestDir < 0 || startOfMainDir < startOfTestDir)) {
String fullyQualifiedClassName = ClassUtils.convertResourcePathToClassName(
withoutExtension.substring(startOfMainDir + STANDARD_SOURCE_DIRECTORY.length() + 1));
return new JavaClassChange(fullyQualifiedClassName);
} else {
// This is unusual, fall back to just assume that the full path is the package -> TODO At least log this
String fullyQualifiedClassName = ClassUtils.convertResourcePathToClassName(withoutExtension);
return new JavaClassChange(fullyQualifiedClassName);
}
} else {
// TODO Do these need to be relative to the module root (i.e. where src/main/java etc. reside)?
return new OtherFileChange(modifiedFilePath.path());
}
}
}

View File

@@ -0,0 +1,122 @@
package org.springframework.modulith.junit;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.AnnotatedClassFinder;
import org.springframework.modulith.core.ApplicationModule;
import org.springframework.modulith.core.ApplicationModuleDependency;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.util.ClassUtils;
import com.tngtech.archunit.core.domain.JavaClass;
// add logging to explain what happens (and why)
/**
* Junit Extension to skip test execution if no changes happened in the module that the test belongs to.
*
* @author Lukas Dohmen, David Bilge
*/
public class ModulithExecutionExtension implements ExecutionCondition {
public static final String CONFIG_PROPERTY_PREFIX = "spring.modulith.test";
final AnnotatedClassFinder spaClassFinder = new AnnotatedClassFinder(SpringBootApplication.class);
private static final Logger log = LoggerFactory.getLogger(ModulithExecutionExtension.class);
@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
if (context.getTestMethod().isPresent()) {
// Is there something similar liken @TestInstance(TestInstance.Lifecycle.PER_CLASS) for Extensions?
return ConditionEvaluationResult.enabled("Enabled, only evaluating per class");
}
StateStore stateStore = new StateStore(context);
Set<Class<?>> changedClasses = stateStore.getChangedClasses();
if (changedClasses.isEmpty()) {
log.trace("No class changes found, running tests");
return ConditionEvaluationResult.enabled("ModulithExecutionExtension: No changes detected");
}
log.trace("Found following changed classes {}", changedClasses);
Optional<Class<?>> testClass = context.getTestClass();
if (testClass.isPresent()) {
if (changedClasses.contains(testClass.get())) {
return ConditionEvaluationResult.enabled("ModulithExecutionExtension: Change in test class detected");
}
Class<?> mainClass = this.spaClassFinder.findFromClass(testClass.get());
if (mainClass == null) {// TODO:: Try with @ApplicationModuleTest -> main class
return ConditionEvaluationResult.enabled(
"ModulithExecutionExtension: Unable to locate SpringBootApplication Class");
}
ApplicationModules applicationModules = ApplicationModules.of(mainClass);
String packageName = ClassUtils.getPackageName(testClass.get());
// always run test if one of whitelisted files is modified (ant matching)
Optional<ApplicationModule> optionalApplicationModule = applicationModules.getModuleForPackage(packageName);
if (optionalApplicationModule.isPresent()) {
Set<JavaClass> dependentClasses = getAllDependentClasses(optionalApplicationModule.get(),
applicationModules);
for (Class<?> changedClass : changedClasses) {
if (optionalApplicationModule.get().contains(changedClass)) {
return ConditionEvaluationResult.enabled(
"ModulithExecutionExtension: Changes in module detected, Executing tests");
}
if (dependentClasses.stream()
.anyMatch(applicationModule -> applicationModule.isEquivalentTo(changedClass))) {
return ConditionEvaluationResult.enabled(
"ModulithExecutionExtension: Changes in dependent module detected, Executing tests");
}
}
}
}
return ConditionEvaluationResult.disabled(
"ModulithExtension: No Changes detected in current module, executing tests");
}
private Set<JavaClass> getAllDependentClasses(ApplicationModule applicationModule,
ApplicationModules applicationModules) {
Set<ApplicationModule> dependentModules = new HashSet<>();
dependentModules.add(applicationModule);
this.getDependentModules(applicationModule, applicationModules, dependentModules);
return dependentModules.stream()
.map(appModule -> appModule.getDependencies(applicationModules))
.flatMap(applicationModuleDependencies -> applicationModuleDependencies.stream()
.map(ApplicationModuleDependency::getTargetType))
.collect(Collectors.toSet());
}
private void getDependentModules(ApplicationModule applicationModule, ApplicationModules applicationModules,
Set<ApplicationModule> modules) {
Set<ApplicationModule> applicationModuleDependencies = applicationModule.getDependencies(applicationModules)
.stream()
.map(ApplicationModuleDependency::getTargetModule)
.collect(Collectors.toSet());
modules.addAll(applicationModuleDependencies);
if (!applicationModuleDependencies.isEmpty()) {
for (ApplicationModule applicationModuleDependency : applicationModuleDependencies) {
this.getDependentModules(applicationModuleDependency, applicationModules, modules);
}
}
}
}

View File

@@ -0,0 +1,71 @@
package org.springframework.modulith.junit;
import java.util.HashSet;
import java.util.Set;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.modulith.junit.Change.JavaClassChange;
import org.springframework.modulith.junit.Change.JavaTestClassChange;
import org.springframework.modulith.junit.Change.OtherFileChange;
import org.springframework.modulith.junit.diff.FileModificationDetector;
import org.springframework.modulith.junit.diff.ModifiedFilePath;
import org.springframework.util.ClassUtils;
class StateStore {
private static final Logger log = LoggerFactory.getLogger(StateStore.class);
private final ExtensionContext.Store store;
StateStore(ExtensionContext context) {
store = context.getRoot().getStore(Namespace.create(ModulithExecutionExtension.class));
}
Set<Class<?>> getChangedClasses() {
// noinspection unchecked
return (Set<Class<?>>) store.getOrComputeIfAbsent("changed-files", s -> {
var environment = new StandardEnvironment();
ConfigDataEnvironmentPostProcessor.applyTo(environment);
var detector = FileModificationDetector.loadFileModificationDetector(environment);
try {
Set<ModifiedFilePath> modifiedFiles = detector.getModifiedFiles(environment);
Set<Change> changes = Changes.toChanges(modifiedFiles);
return toChangedClasses(changes);
} catch (Exception e) {
log.error("ModulithExecutionExtension: Unable to fetch changed files, executing all tests", e);
return Set.of();
}
});
}
private static Set<Class<?>> toChangedClasses(Set<Change> changes) {
Set<Class<?>> changedClasses = new HashSet<>();
for (Change change : changes) {
if (change instanceof OtherFileChange) {
continue;
}
String className;
if (change instanceof JavaClassChange jcc) {
className = jcc.fullyQualifiedClassName();
} else if (change instanceof JavaTestClassChange jtcc) {
className = jtcc.fullyQualifiedClassName();
} else {
throw new IllegalStateException("Unexpected change type: " + change.getClass());
}
try {
Class<?> aClass = ClassUtils.forName(className, null);
changedClasses.add(aClass);
} catch (ClassNotFoundException e) {
log.trace("ModulithExecutionExtension: Unable to find class \"{}\"", className);
}
}
return changedClasses;
}
}

View File

@@ -0,0 +1,49 @@
package org.springframework.modulith.junit.diff;
import static org.springframework.modulith.junit.ModulithExecutionExtension.*;
import java.io.IOException;
import java.util.Set;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.env.PropertyResolver;
import org.springframework.lang.NonNull;
import org.springframework.modulith.junit.ModulithExecutionExtension;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
public interface FileModificationDetector {
Logger log = LoggerFactory.getLogger(FileModificationDetector.class);
String CLASS_FILE_SUFFIX = ".java";
String PACKAGE_PREFIX = "src.main.java";
Set<ModifiedFilePath> getModifiedFiles(@NonNull PropertyResolver propertyResolver)
throws IOException, GitAPIException;
static FileModificationDetector loadFileModificationDetector(@NonNull PropertyResolver propertyResolver) {
var detectorClassName = propertyResolver.getProperty(CONFIG_PROPERTY_PREFIX + ".file-modification-detector");
var referenceCommit = ReferenceCommitDetector.getReferenceCommitProperty(propertyResolver);
if (StringUtils.hasText(detectorClassName)) {
try {
var strategyType = ClassUtils.forName(detectorClassName, ModulithExecutionExtension.class.getClassLoader());
log.info("Found request via property for file modification detector '{}'", detectorClassName);
return BeanUtils.instantiateClass(strategyType, FileModificationDetector.class);
} catch (ClassNotFoundException | LinkageError o_O) {
throw new IllegalStateException(o_O);
}
} else if (StringUtils.hasText(referenceCommit)) {
log.info("Found reference commit property. Using file modification detector '{}'",
ReferenceCommitDetector.class.getName());
return new ReferenceCommitDetector();
}
log.info("Using default file modification detector '{}'", UnpushedCommitsDetector.class.getName());
return new UnpushedCommitsDetector();
}
}

View File

@@ -0,0 +1,61 @@
package org.springframework.modulith.junit.diff;
import java.io.IOException;
import java.util.stream.Stream;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
/**
* Utility to contain re-used JGit operations. For internal use only.
*/
final class JGitUtil {
private JGitUtil() {}
static Stream<ModifiedFilePath> convertDiffEntriesToFileChanges(Stream<DiffEntry> diffEntries) {
return diffEntries.flatMap(
entry -> Stream.of(new ModifiedFilePath(entry.getNewPath()), new ModifiedFilePath(entry.getOldPath())))
.filter(change -> !change.path().equals("/dev/null"));
}
static Repository buildRepository() throws IOException {
return new FileRepositoryBuilder().findGitDir().build();
}
static Stream<DiffEntry> diffRefs(Repository repository, String oldRef, String newRef) throws IOException {
try (Git git = new Git(repository)) {
AbstractTreeIterator oldTreeParser = prepareTreeParser(repository, oldRef);
AbstractTreeIterator newTreeParser = prepareTreeParser(repository, newRef);
return git.diff().setOldTree(oldTreeParser).setNewTree(newTreeParser).call().stream();
} catch (GitAPIException e) {
throw new IOException("Unable to find diff between refs '%s' and '%s'".formatted(oldRef, newRef), e);
}
}
private static AbstractTreeIterator prepareTreeParser(Repository repository, String ref) throws IOException {
ObjectId commitId = repository.resolve(ref);
try (RevWalk walk = new RevWalk(repository)) {
RevCommit commit = walk.parseCommit(commitId);
RevTree tree = walk.parseTree(commit.getTree().getId());
CanonicalTreeParser treeParser = new CanonicalTreeParser();
try (ObjectReader reader = repository.newObjectReader()) {
treeParser.reset(reader, tree.getId());
}
return treeParser;
}
}
}

View File

@@ -0,0 +1,3 @@
package org.springframework.modulith.junit.diff;
public record ModifiedFilePath(String path) {}

View File

@@ -0,0 +1,48 @@
package org.springframework.modulith.junit.diff;
import static org.springframework.modulith.junit.ModulithExecutionExtension.*;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.diff.DiffEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.PropertyResolver;
import org.springframework.lang.NonNull;
/**
* Implementation to get changes between HEAD and a complete or abbreviated SHA-1 or other revision, like
* <code>HEAD~2</code>. See {@link org.eclipse.jgit.lib.Repository#resolve(String)} for more information.
*/
public class ReferenceCommitDetector implements FileModificationDetector {
private static final Logger log = LoggerFactory.getLogger(ReferenceCommitDetector.class);
@Override
public @NonNull Set<ModifiedFilePath> getModifiedFiles(@NonNull PropertyResolver propertyResolver)
throws IOException {
String commitIdToCompareTo = getReferenceCommitProperty(propertyResolver);
try (var repo = JGitUtil.buildRepository()) {
String compareTo;
if (commitIdToCompareTo == null || commitIdToCompareTo.isEmpty()) {
log.warn("No reference-commit configured, comparing to HEAD~1.");
compareTo = "HEAD~1";
} else {
log.info("Comparing to git commit #{}", commitIdToCompareTo);
compareTo = commitIdToCompareTo;
}
String localBranch = repo.getFullBranch();
Stream<DiffEntry> diffs = JGitUtil.diffRefs(repo, compareTo, localBranch);
return JGitUtil.convertDiffEntriesToFileChanges(diffs).collect(Collectors.toSet());
}
}
public static String getReferenceCommitProperty(PropertyResolver propertyResolver) {
return propertyResolver.getProperty(CONFIG_PROPERTY_PREFIX + ".reference-commit");
}
}

View File

@@ -0,0 +1,45 @@
package org.springframework.modulith.junit.diff;
import java.io.IOException;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.springframework.core.env.PropertyResolver;
import org.springframework.lang.NonNull;
import com.tngtech.archunit.thirdparty.com.google.common.collect.Streams;
/**
* Implementation to get latest local file changes.
*
* @author Lukas Dohmen
*/
public class UncommittedChangesDetector implements FileModificationDetector {
@Override
public @NonNull Set<ModifiedFilePath> getModifiedFiles(@NonNull PropertyResolver propertyResolver)
throws IOException {
try (var repo = new FileRepositoryBuilder().findGitDir().build()) {
return findUncommittedChanges(repo).collect(Collectors.toSet());
}
}
private static Stream<ModifiedFilePath> findUncommittedChanges(Repository repository) throws IOException {
try (Git git = new Git(repository)) {
Status status = git.status().call();
return Streams.concat(status.getUncommittedChanges().stream(), status.getUntracked().stream())
.map(ModifiedFilePath::new);
} catch (GitAPIException e) {
throw new IOException("Unable to find uncommitted changes", e);
}
}
}

View File

@@ -0,0 +1,41 @@
package org.springframework.modulith.junit.diff;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.lib.BranchConfig;
import org.springframework.core.env.PropertyResolver;
import org.springframework.lang.NonNull;
/**
* <p>
* Find all changes that have not been pushed to the remote branch yet.
* <p>
* To be precise, this finds the diff between the local HEAD and its tracking branch and the uncommitted and untracked
* changes. <em>Note:</em> This will not fetch from the remote first!
*/
public class UnpushedCommitsDetector implements FileModificationDetector {
@Override
public @NonNull Set<ModifiedFilePath> getModifiedFiles(@NonNull PropertyResolver propertyResolver)
throws IOException {
try (var repo = JGitUtil.buildRepository()) {
String localBranch = repo.getFullBranch();
String trackingBranch = new BranchConfig(repo.getConfig(), repo.getBranch()).getTrackingBranch();
Stream<DiffEntry> diff = localBranch != null && trackingBranch != null
? JGitUtil.diffRefs(repo, localBranch, trackingBranch)
: Stream.empty();
HashSet<ModifiedFilePath> result = new HashSet<>();
result.addAll(new UncommittedChangesDetector().getModifiedFiles(propertyResolver));
result.addAll(JGitUtil.convertDiffEntriesToFileChanges(diff).collect(Collectors.toSet()));
return result;
}
}
}

View File

@@ -0,0 +1,17 @@
{
"groups": [
{
"name": "spring.modulith.test",
"description": "Properties configuring the execution of modulith-specific tests."
}
],
"properties": [
{
"name": "spring.modulith.test.changed-files-strategy",
"type": "java.lang.String",
"defaultValue": "org.springframework.modulith.UncommittedChangesStrategy",
"description": "A strategy to determine the list of changed files to consider for test execution. This should be a fully qualified class name of a registered implementation."
}
],
"hints": []
}

View File

@@ -0,0 +1 @@
org.springframework.modulith.junit.ModulithExecutionExtension

View File

@@ -0,0 +1,3 @@
org.springframework.modulith.git.UncommittedChangesDetector
org.springframework.modulith.git.UnpushedGitChangesDetector
org.springframework.modulith.git.DiffDetector

View File

@@ -0,0 +1 @@
junit.jupiter.extensions.autodetection.enabled=true

View File

@@ -0,0 +1,33 @@
package org.springframework.modulith.junit;
import static org.assertj.core.api.Assertions.*;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.junit.Change.JavaClassChange;
import org.springframework.modulith.junit.Change.JavaTestClassChange;
import org.springframework.modulith.junit.Change.OtherFileChange;
import org.springframework.modulith.junit.diff.ModifiedFilePath;
class ChangesTest {
@Test
void shouldInterpredModifiedFilePathsCorrectly() {
// given
Set<ModifiedFilePath> modifiedFilePaths = Set.of(
new ModifiedFilePath("spring-modulith-junit/src/main/java/org/springframework/modulith/Changes.java"),
new ModifiedFilePath("spring-modulith-junit/src/test/java/org/springframework/modulith/ChangesTest.java"),
new ModifiedFilePath(
"spring-modulith-junit/src/main/resources/META-INF/additional-spring-configuration-metadata.json"));
// when
Set<Change> result = Changes.toChanges(modifiedFilePaths);
// then
assertThat(result).containsExactlyInAnyOrder(
new JavaClassChange("org.springframework.modulith.Changes"),
new JavaTestClassChange("org.springframework.modulith.ChangesTest"),
new OtherFileChange(
"spring-modulith-junit/src/main/resources/META-INF/additional-spring-configuration-metadata.json"));
}
}