Skip to content

Commit

Permalink
spring-projectsGH-31 Check module dependencies for changes
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasdo committed Sep 2, 2024
1 parent 861bfef commit eb5ca99
Showing 1 changed file with 87 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
package org.springframework.modulith;

import static org.springframework.modulith.FileModificationDetector.CLASS_FILE_SUFFIX;
import static org.springframework.modulith.FileModificationDetector.PACKAGE_PREFIX;

import java.io.IOException;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import com.tngtech.archunit.core.domain.JavaClass;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;
Expand All @@ -23,12 +16,22 @@
import org.springframework.core.env.StandardEnvironment;
import org.springframework.lang.NonNull;
import org.springframework.modulith.core.ApplicationModule;
import org.springframework.modulith.core.ApplicationModuleDependency;
import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.git.DiffDetector;
import org.springframework.modulith.git.UnpushedGitChangesDetector;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static org.springframework.modulith.FileModificationDetector.CLASS_FILE_SUFFIX;
import static org.springframework.modulith.FileModificationDetector.PACKAGE_PREFIX;

// add logging to explain what happens (and why)

/**
Expand All @@ -45,10 +48,11 @@ public class ModulithExecutionExtension implements ExecutionCondition {

@Override
public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
if (context.getTestMethod().isPresent()) { // Is there something similar like @TestInstance(TestInstance.Lifecycle.PER_CLASS) for Extensions?
if (context.getTestMethod().isPresent()) {
// Is there something similar liken @TestInstance(TestInstance.Lifecycle.PER_CLASS) for Extensions?
return ConditionEvaluationResult.enabled("Enabled, only evaluating per class");
}
// if there is no applicationContext present, this is probably a unit test -> always execute those, for now

ExtensionContext.Store store = context.getRoot().getStore(Namespace.create(ModulithExecutionExtension.class));
this.writeChangedFilesToStore(store);
Exception e = store.get(PROJECT_ERROR, Exception.class);
Expand All @@ -71,34 +75,44 @@ public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext con
Class<?> mainClass = this.spaClassFinder.findFromClass(testClass.get());

if (mainClass == null) {// TODO:: Try with @ApplicationModuleTest -> main class
return ConditionEvaluationResult.enabled(
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<?> modifiedFile : modifiedFiles) {

Optional<ApplicationModule> optionalApplicationModule = applicationModules.getModuleForPackage(packageName);
if (optionalApplicationModule.isPresent()) {
if (optionalApplicationModule.get().contains(modifiedFile)) {
return ConditionEvaluationResult.enabled(
"ModulithExecutionExtension: Changes in module detected, Executing tests");
}

for (Class<?> modifiedFile : modifiedFiles) {
if (optionalApplicationModule.get().contains(modifiedFile)) {
return ConditionEvaluationResult.enabled("ModulithExecutionExtension: Changes in module detected, Executing tests");
}
}
if (dependentClasses.stream()
.anyMatch(applicationModule -> applicationModule.isEquivalentTo(modifiedFile))) {
return ConditionEvaluationResult.enabled(
"ModulithExecutionExtension: Changes in dependent module detected, Executing tests");
}
}
}
}

return ConditionEvaluationResult.disabled("ModulithExtension: No Changes detected in current module, executing tests");
return ConditionEvaluationResult.disabled(
"ModulithExtension: No Changes detected in current module, executing tests");
}


public void writeChangedFilesToStore(ExtensionContext.Store store) {

var environment = new StandardEnvironment();
ConfigDataEnvironmentPostProcessor.applyTo(environment);
var environment = new StandardEnvironment();
ConfigDataEnvironmentPostProcessor.applyTo(environment);

var strategy = loadGitProviderStrategy(environment);

Expand All @@ -108,14 +122,13 @@ public void writeChangedFilesToStore(ExtensionContext.Store store) {
Set<ModifiedFilePath> modifiedFiles = strategy.getModifiedFiles(environment);

Set<String> changedClassNames = modifiedFiles.stream()

.map(ModifiedFilePath::path)
.map(ClassUtils::convertResourcePathToClassName)
.filter(path -> path.contains(PACKAGE_PREFIX))
.filter(path -> path.endsWith(CLASS_FILE_SUFFIX))
.map(path -> path.substring(path.lastIndexOf(PACKAGE_PREFIX) + PACKAGE_PREFIX.length() + 1,
path.length() - CLASS_FILE_SUFFIX.length()))
.collect(Collectors.toSet());
.map(ModifiedFilePath::path)
.map(ClassUtils::convertResourcePathToClassName)
.filter(path -> path.contains(PACKAGE_PREFIX))
.filter(path -> path.endsWith(CLASS_FILE_SUFFIX))
.map(path -> path.substring(path.lastIndexOf(PACKAGE_PREFIX) + PACKAGE_PREFIX.length() + 1,
path.length() - CLASS_FILE_SUFFIX.length()))
.collect(Collectors.toSet());

for (String className : changedClassNames) {
try {
Expand All @@ -138,24 +151,53 @@ private FileModificationDetector loadGitProviderStrategy(@NonNull PropertyResolv
var property = propertyResolver.getProperty(CONFIG_PROPERTY_PREFIX + ".changed-files-strategy");
var referenceCommit = propertyResolver.getProperty(CONFIG_PROPERTY_PREFIX + ".reference-commit");

if(StringUtils.hasText(property)) {
try {
if (StringUtils.hasText(property)) {
try {

var strategyType = ClassUtils.forName(property, ModulithExecutionExtension.class.getClassLoader());
log.info("Strategy for finding changed files is '{}'", strategyType.getName());
return BeanUtils.instantiateClass(strategyType, FileModificationDetector.class);
var strategyType = ClassUtils.forName(property, ModulithExecutionExtension.class.getClassLoader());
log.info("Strategy for finding changed files is '{}'", strategyType.getName());
return BeanUtils.instantiateClass(strategyType, FileModificationDetector.class);

} catch (ClassNotFoundException | LinkageError o_O) {
throw new IllegalStateException(o_O);
}
}
if(StringUtils.hasText(referenceCommit)) {
log.info("Strategy for finding changed files is '{}'", DiffDetector.class.getName());
return new DiffDetector();
}
} catch (ClassNotFoundException | LinkageError o_O) {
throw new IllegalStateException(o_O);
}
}
if (StringUtils.hasText(referenceCommit)) {
log.info("Strategy for finding changed files is '{}'", DiffDetector.class.getName());
return new DiffDetector();
}

log.info("Strategy for finding changed files is '{}'", UnpushedGitChangesDetector.class.getName());
return new UnpushedGitChangesDetector();
}
return new UnpushedGitChangesDetector();
}

private Set<JavaClass> getAllDependentClasses(ApplicationModule applicationModule,
ApplicationModules applicationModules) {

Set<ApplicationModule> dependentModules = Set.of(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);
}
}
}

}

0 comments on commit eb5ca99

Please sign in to comment.