ArchUnit
ArchUnit copied to clipboard
Analyze a path with @AnalyzeClasses
The ClassFileImporter offers the method "importPath()" to import classes in a specific path instead of a specific package. Is there an equivalent for @AnalyzeClasses? Something like
@AnalyzeClasses(path = "src/main")
Our project uses a lof of generated code that uses the same package structure as non-generated code and we would like to exclude the generated code from being scanned.
You can use the LocationProvider
API for that. E.g. create a custom location provider
class PathLocationProvider implements LocationProvider {
@Override
public Set<Location> get(Class<?> testClass) {
return Collections.singleton(Location.of(Paths.get("/some/path")));
}
}
Then use it via
@AnalyzeClasses(locations = PathLocationProvider.class)
If you need it reusable you could introduce a custom path annotation
class PathLocationProvider implements LocationProvider {
@Override
public Set<Location> get(Class<?> testClass) {
if (!testClass.isAnnotationPresent(SelectedPath.class)) {
throw new IllegalArgumentException(
String.format("%s can only be used on classes annotated with @%s",
getClass().getSimpleName(), SelectedPath.class.getSimpleName()));
}
String path = testClass.getAnnotation(SelectedPath.class).value();
return Collections.singleton(Location.of(Paths.get(path)));
}
}
@Retention(RUNTIME)
@interface SelectedPath {
String value();
}
Then use it via
@SelectedPath("/some/path")
@AnalyzeClasses(locations = PathLocationProvider.class)
Does that help you? Maybe we should support this use case out of the box, since I also had the need already several times in the past :thinking: But file paths are always a little tricky with respect to the "current directory" set by the runtime and I wasn't sure how projects in general would want to use such an API :man_shrugging:
This did, in fact, help me - although it was a bit more complicated then I initially assumed.
We're using a Maven project with basically the following structure (standard maven):
src
main
java
de.foo.bar
Foo.java
src-gen (generated code)
main
java
de.foo.bar
Bar.java
We only want to scan Foo.java
, but not Bar.java
. Initially, I thought it would be as easy as this:
class PathLocationProvider implements LocationProvider {
@Override
public Set<Location> get(Class<?> testClass) {
return Collections.singleton(Location.of(Paths.get("src/main/java")));
}
}
As it turns out, ArchUnit seems to expect the LocationProvider to return the paths to *.class files, not to *.java files. Maven, however, will compile the above structure to
target
classes
de.foo.bar
Foo.class
Bar.class
This makes a differentiation between manually written code from src
and automatically generated code from src-gen
a tad more complicated. What we ended up with is the following, which seems to work for our use case:
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;
import com.tngtech.archunit.core.importer.Location;
import com.tngtech.archunit.junit.LocationProvider;
public class OnlyScanSrcFolderLocationProvider implements LocationProvider {
private static final String SOURCE_ROOT = "src/main/java";
private static final String CLASS_ROOT = "target/classes";
@Override
public Set<Location> get(Class<?> testClass) {
Path rootPath = Paths.get(SOURCE_ROOT);
Set<Location> result = new HashSet<>();
try (Stream<Path> pathStream = Files.walk(rootPath)) {
pathStream.filter(Files::isReadable)
.filter(Files::isRegularFile)
.filter((path -> path.getFileName()
.toString()
.endsWith(".java")))
.forEach(path -> {
Path classFilePath = Paths.get(CLASS_ROOT,
path.subpath(rootPath.getNameCount(), path.getNameCount() - 1).toString(),
path.getFileName()
.toString()
.replaceAll("\\.java$", ".class"));
result.add(Location.of(classFilePath));
});
} catch (IOException e) {
throw new RuntimeException(e);
}
return result;
}
}
I can't imagine that we are the only one with the use case "do not scan generated code", so if someone has a more elegant solution to this that I don't know about, please let me know :)
Yes, ArchUnit works on bytecode base, so even if you do importPackages("foo.bar")
it will internally ask the ClassLoader
"please give me the resource path of the compiled classes" and import all the .class
files. For me the root problem is that your Maven configuration throws generated and non-generated classes together in the same output path. If the build tool would do this cleaner, you might have target/classes
and target/gen-classes
or something like this and all your logic wouldn't be necessary. But of course then all the environments would need to be configured to put both these file paths on the classpath.
Given that all you have is one folder where all compiled classes are thrown together I also don't know of a better solution than the one you already found :thinking:
Other other typical solution would be to configure the source generator to put something like generated
into the package name, then you can also simply exclude */generated/*
from your import and you can simply import packages again :man_shrugging: