diff --git a/.gitignore b/.gitignore
index f10a528..5dcf37b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,4 @@
target/
-*.class
.DS_Store
# jdtls/eclipse stuff
diff --git a/classport-agent/pom.xml b/classport-agent/pom.xml
index ba4c6f1..493d1f9 100644
--- a/classport-agent/pom.xml
+++ b/classport-agent/pom.xml
@@ -29,7 +29,24 @@
classport-commons
${project.parent.version}
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
diff --git a/classport-agent/src/main/java/io/github/chains_project/classport/agent/ClassportAgent.java b/classport-agent/src/main/java/io/github/chains_project/classport/agent/ClassportAgent.java
index 235a75d..9ad01e8 100644
--- a/classport-agent/src/main/java/io/github/chains_project/classport/agent/ClassportAgent.java
+++ b/classport-agent/src/main/java/io/github/chains_project/classport/agent/ClassportAgent.java
@@ -4,17 +4,16 @@
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.lang.instrument.*;
+import java.lang.instrument.ClassFileTransformer;
+import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
+import io.github.chains_project.classport.commons.AnnotationReader;
import io.github.chains_project.classport.commons.ClassportInfo;
import io.github.chains_project.classport.commons.ClassportProject;
-import io.github.chains_project.classport.commons.AnnotationReader;
/**
* A classport agent, ready to check the classports of any and all incoming
@@ -25,7 +24,7 @@ public class ClassportAgent {
private static final ArrayList noAnnotations = new ArrayList<>();
// TODO: Output in a useful format (JSON?)
- private static void writeSBOM(Map sbom) throws IOException {
+ public static void writeSBOM(Map sbom) throws IOException {
File treeOutputFile = new File("classport-deps-tree");
File listOutputFile = new File("classport-deps-list");
diff --git a/classport-agent/src/test/java/ClassportAgentTest.java b/classport-agent/src/test/java/ClassportAgentTest.java
new file mode 100644
index 0000000..7812290
--- /dev/null
+++ b/classport-agent/src/test/java/ClassportAgentTest.java
@@ -0,0 +1,147 @@
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+
+import io.github.chains_project.classport.agent.ClassportAgent;
+import io.github.chains_project.classport.commons.AnnotationReader;
+import io.github.chains_project.classport.commons.ClassportInfo;
+
+public class ClassportAgentTest {
+
+ private static final Path CLASSPORT_TREE_PATH = Path.of("src/test/resources/classport-deps/classport-deps-tree");
+ private static final Path CLASSPORT_LIST_PATH = Path.of("src/test/resources/classport-deps/classport-deps-list");
+ private static final Path ANNOTATED_CLASS_PATH = Path.of("src/test/resources/annotated-classes/StringUtils.class");
+ private static final Path NOT_ANNOTATED_CLASS_PATH = Path.of("src/test/resources/not-annotated-classes/StringUtils.class");
+
+ @Test
+ void shouldGenerateDependencyListAndTreeFiles() throws Exception {
+ HashMap sbom = mockSBOM();
+
+ ClassportAgent.writeSBOM(sbom);
+
+ File actualTreeFile = new File(System.getProperty("user.dir"), "classport-deps-tree");
+ File actualListFile = new File(System.getProperty("user.dir"), "classport-deps-list");
+
+ assertAll(
+ () -> assertTrue(actualTreeFile.exists(), "Tree output file should be created"),
+ () -> assertTrue(actualListFile.exists(), "List output file should be created"),
+ () -> assertEquals(
+ Files.readString(CLASSPORT_TREE_PATH),
+ Files.readString(actualTreeFile.toPath()),
+ "Tree files should be identical"),
+ () -> assertEquals(
+ Files.readString(CLASSPORT_LIST_PATH),
+ Files.readString(actualListFile.toPath()),
+ "List files should be identical")
+ );
+
+ actualTreeFile.delete();
+ actualListFile.delete();
+ }
+
+
+ @Test
+ void shouldHandleEmptySBOM() throws Exception {
+ HashMap emptySBOM = new HashMap<>();
+
+ ClassportAgent.writeSBOM(emptySBOM);
+
+ File treeFile = new File(System.getProperty("user.dir"), "classport-deps-tree");
+ File listFile = new File(System.getProperty("user.dir"), "classport-deps-list");
+
+ assertTrue(treeFile.exists(), "Tree output file should be created even if SBOM is empty");
+ assertTrue(listFile.exists(), "List output file should be created even if SBOM is empty");
+
+ String treeContent = new String(Files.readAllBytes(treeFile.toPath()));
+ String expectedPlaceholder = " (parent-only artefact)";
+
+ assertTrue(treeContent.contains(expectedPlaceholder), "Tree file should contain placeholder text when SBOM is empty");
+ assertTrue(listFile.length() == 0, "List file should be empty when SBOM is empty");
+
+ // Clean up after the test
+ treeFile.delete();
+ listFile.delete();
+ }
+
+ @Test
+ void shouldAnnotationBeCorrectlyRead() throws IOException {
+ File classFile = ANNOTATED_CLASS_PATH.toFile();
+ byte[] buffer = loadClassFromFile(classFile);
+ ClassportInfo actualAnnotation = AnnotationReader.getAnnotationValues(buffer);
+
+ assertAll(
+ () -> assertEquals("commons-lang3", actualAnnotation.artefact()),
+ () -> assertEquals("org.apache.commons", actualAnnotation.group()),
+ () -> assertEquals("org.apache.commons:commons-lang3:jar:3.17.0", actualAnnotation.id()),
+ () -> assertTrue(actualAnnotation.isDirectDependency()),
+ () -> assertEquals("com.example:test-agent-app:jar:1.0-SNAPSHOT", actualAnnotation.sourceProjectId()),
+ () -> assertEquals("3.17.0", actualAnnotation.version()),
+ () -> assertEquals("org.apache.commons:commons-text:jar:1.12.0", actualAnnotation.childIds()[0])
+ );
+ }
+
+ @Test
+ void shouldReturnNonAnnotatedClassesNull() throws Exception {
+ byte[] nonAnnotatedClassBytes = loadClassFromFile(NOT_ANNOTATED_CLASS_PATH.toFile());
+ ClassportInfo actualAnnotation = AnnotationReader.getAnnotationValues(nonAnnotatedClassBytes);
+ assertNull(actualAnnotation);
+ }
+
+
+ private byte[] loadClassFromFile(File classFile) throws IOException {
+ try (InputStream inputStream = new FileInputStream(classFile)) {
+ return inputStream.readAllBytes();
+ }
+ }
+
+
+ private HashMap mockSBOM() {
+ HashMap sbom = new HashMap<>();
+ sbom.put("com.example:test-agent-app:jar:1.0-SNAPSHOT",
+ createClassportInfo("com.example", "1.0-SNAPSHOT",
+ "com.example:test-agent-app:jar:1.0-SNAPSHOT",
+ "test-agent-app", false,
+ "com.example:test-agent-app:jar:1.0-SNAPSHOT",
+ new String[]{"com.google.code.gson:gson:jar:2.11.0",
+ "org.apache.commons:commons-lang3:jar:3.17.0"}));
+ sbom.put("com.google.code.gson:gson:jar:2.11.0",
+ createClassportInfo("com.google.code.gson", "2.11.0",
+ "com.google.code.gson:gson:jar:2.11.0",
+ "gson", true,
+ "com.example:test-agent-app:jar:1.0-SNAPSHOT",
+ new String[]{"com.google.errorprone:error_prone_annotations:jar:2.27.0"}));
+ sbom.put("org.apache.commons:commons-lang3:jar:3.17.0",
+ createClassportInfo("org.apache.commons", "3.17.0",
+ "org.apache.commons:commons-lang3:jar:3.17.0",
+ "commons-lang3", true,
+ "com.example:test-agent-app:jar:1.0-SNAPSHOT",
+ new String[]{"org.apache.commons:commons-text:jar:1.12.0"}));
+ return sbom;
+ }
+
+ private ClassportInfo createClassportInfo(String group, String version, String id,
+ String artefact, boolean isDirectDependency,
+ String sourceProjectId, String[] childIds) {
+ return new ClassportInfo() {
+ @Override public String group() { return group; }
+ @Override public String version() { return version; }
+ @Override public String id() { return id; }
+ @Override public String artefact() { return artefact; }
+ @Override public boolean isDirectDependency() { return isDirectDependency; }
+ @Override public String sourceProjectId() { return sourceProjectId; }
+ @Override public String[] childIds() { return childIds; }
+ @Override public Class extends Annotation> annotationType() { return ClassportInfo.class; }
+ };
+ }
+}
diff --git a/classport-agent/src/test/resources/annotated-classes/StringUtils.class b/classport-agent/src/test/resources/annotated-classes/StringUtils.class
new file mode 100644
index 0000000..0b27790
Binary files /dev/null and b/classport-agent/src/test/resources/annotated-classes/StringUtils.class differ
diff --git a/classport-agent/src/test/resources/classport-deps/classport-deps-list b/classport-agent/src/test/resources/classport-deps/classport-deps-list
new file mode 100644
index 0000000..41751cf
--- /dev/null
+++ b/classport-agent/src/test/resources/classport-deps/classport-deps-list
@@ -0,0 +1,2 @@
+com.google.code.gson:gson:jar:2.11.0
+org.apache.commons:commons-lang3:jar:3.17.0
diff --git a/classport-agent/src/test/resources/classport-deps/classport-deps-tree b/classport-agent/src/test/resources/classport-deps/classport-deps-tree
new file mode 100644
index 0000000..3ef7a72
--- /dev/null
+++ b/classport-agent/src/test/resources/classport-deps/classport-deps-tree
@@ -0,0 +1,3 @@
+com.example:test-agent-app:jar:1.0-SNAPSHOT
++- com.google.code.gson:gson:jar:2.11.0
+\- org.apache.commons:commons-lang3:jar:3.17.0
diff --git a/classport-agent/src/test/resources/not-annotated-classes/StringUtils.class b/classport-agent/src/test/resources/not-annotated-classes/StringUtils.class
new file mode 100644
index 0000000..136013c
Binary files /dev/null and b/classport-agent/src/test/resources/not-annotated-classes/StringUtils.class differ
diff --git a/classport-analyser/pom.xml b/classport-analyser/pom.xml
index 93acc88..2b80344 100644
--- a/classport-analyser/pom.xml
+++ b/classport-analyser/pom.xml
@@ -50,6 +50,21 @@
asm-util
9.6
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
diff --git a/classport-analyser/src/main/java/io/github/chains_project/classport/analyser/Analyser.java b/classport-analyser/src/main/java/io/github/chains_project/classport/analyser/Analyser.java
index 41bde43..8d13e7a 100644
--- a/classport-analyser/src/main/java/io/github/chains_project/classport/analyser/Analyser.java
+++ b/classport-analyser/src/main/java/io/github/chains_project/classport/analyser/Analyser.java
@@ -30,7 +30,7 @@ public class Analyser {
// and use this from there.
private static final byte[] magicBytes = new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
- private static HashMap getSBOM(JarFile jar) {
+ public static HashMap getSBOM(JarFile jar) {
HashMap sbom = new HashMap<>();
HashMap noAnnotations = new HashMap<>();
try {
diff --git a/classport-analyser/src/test/java/AnalyserTest.java b/classport-analyser/src/test/java/AnalyserTest.java
new file mode 100644
index 0000000..18fcb09
--- /dev/null
+++ b/classport-analyser/src/test/java/AnalyserTest.java
@@ -0,0 +1,112 @@
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.jar.JarFile;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+import io.github.chains_project.classport.analyser.Analyser;
+import io.github.chains_project.classport.analyser.ClassLoadingAdder;
+import io.github.chains_project.classport.commons.ClassportInfo;
+
+class AnalyserTest {
+
+ private final Path annotatedJarPath = Path.of("src/test/resources/annotated-classes/test-agent-app-1.0-SNAPSHOT.jar");
+ private final Path notAnnotatedJarPath = Path.of("src/test/resources/not-annotated-classes/test-agent-app-1.0-SNAPSHOT.jar");
+ private final String addedClassName = "__classportForceClassLoading";
+ private final String addedClassDescriptor = "()V";
+ private final Path annotatedClassPath = Path.of("src/test/resources/annotated-classes/Main.class");
+ private boolean methodPresent;
+ private boolean methodInvoked;
+
+ @Test
+ void testGetSBOM_withAnnotatedClasses() throws IOException {
+ JarFile jar = new JarFile(annotatedJarPath.toFile());
+ HashMap actualSbom = Analyser.getSBOM(jar);
+
+ assertTrue(!actualSbom.isEmpty());
+ assertEquals(4, actualSbom.size(), "SBOM should contain 4 annotated classes");
+ assertTrue(actualSbom.containsKey("com.example:test-agent-app:jar:1.0-SNAPSHOT"), "SBOM should contain class com.example:test-agent-app:jar:1.0-SNAPSHOT");
+ assertTrue(actualSbom.containsKey("com.google.errorprone:error_prone_annotations:jar:2.27.0"), "SBOM should contain class com.google.errorprone:error_prone_annotations:jar:2.27.0");
+ assertTrue(actualSbom.containsKey("com.google.code.gson:gson:jar:2.11.0"), "SBOM should contain class com.google.code.gson:gson:jar:2.11.0");
+ assertTrue(actualSbom.containsKey("org.apache.commons:commons-lang3:jar:3.17.0"), "SBOM should contain class org.apache.commons:commons-lang3:jar:3.17.0");
+ }
+
+ @Test
+ void testGetSBOM_withNonAnnotatedClasses() throws IOException {
+ ByteArrayOutputStream errContent = new ByteArrayOutputStream();
+ System.setErr(new PrintStream(errContent));
+
+ JarFile jar = new JarFile(notAnnotatedJarPath.toFile());
+ HashMap actualSbom = Analyser.getSBOM(jar);
+
+ assertTrue(actualSbom.isEmpty(),"The sbom should be empty if the jar file does not contain annotated classes");
+ String actualMessage = errContent.toString();
+ assertTrue(actualMessage.contains("[Warning]"), "The warning message should match the expected pattern");
+ }
+
+ @Test
+ void testForceClassLoading_withAnnotation() throws IOException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
+ Path path = annotatedClassPath;
+ byte[] originalMainClass = Files.readAllBytes(path);
+
+ String[] classesToLoad = {"com/google/errorprone/annotations/MustBeClosed", "com/google/gson/ExclusionStrategy", "org/apache/commons/lang3/BooleanUtils"};
+ byte[] modifiedClass = ClassLoadingAdder.forceClassLoading(originalMainClass, classesToLoad);
+
+ assertNotNull(modifiedClass, "Modified class byte array should not be null");
+ assertTrue(modifiedClass.length > 0, "Modified class should have content");
+
+ ClassReader classReader = new ClassReader(modifiedClass);
+ assertTrue(isAdditionalMethodPresent(classReader), "The additional method __classportForceClassLoading should be present");
+ assertTrue(isAdditionalMethodInvoked(classReader), "The additional method __classportForceClassLoading should be invoked");
+
+ }
+
+ private boolean isAdditionalMethodPresent(ClassReader classReader) {
+ methodPresent = false;
+ classReader.accept(new ClassVisitor(Opcodes.ASM9) {
+
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ if (name.equals(addedClassName) && descriptor.equals(addedClassDescriptor)) {
+ methodPresent = true;
+ }
+ return super.visitMethod(access, name, descriptor, signature, exceptions);
+ }
+ }, 0);
+ return methodPresent;
+ }
+
+ private boolean isAdditionalMethodInvoked(ClassReader classReader) {
+ methodInvoked = false;
+ classReader.accept(new ClassVisitor(Opcodes.ASM9) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
+ return new MethodVisitor(Opcodes.ASM9) {
+ @Override
+ public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
+ if (name.equals(addedClassName) && descriptor.equals(addedClassDescriptor)) {
+ methodInvoked = true;
+ }
+ super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
+ }
+ };
+ }
+
+ }, 0);
+ return methodInvoked;
+ }
+}
+
diff --git a/classport-analyser/src/test/resources/annotated-classes/Main.class b/classport-analyser/src/test/resources/annotated-classes/Main.class
new file mode 100644
index 0000000..be9efc0
Binary files /dev/null and b/classport-analyser/src/test/resources/annotated-classes/Main.class differ
diff --git a/classport-analyser/src/test/resources/annotated-classes/test-agent-app-1.0-SNAPSHOT.jar b/classport-analyser/src/test/resources/annotated-classes/test-agent-app-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..7bb707a
Binary files /dev/null and b/classport-analyser/src/test/resources/annotated-classes/test-agent-app-1.0-SNAPSHOT.jar differ
diff --git a/classport-analyser/src/test/resources/not-annotated-classes/test-agent-app-1.0-SNAPSHOT.jar b/classport-analyser/src/test/resources/not-annotated-classes/test-agent-app-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..4ea9617
Binary files /dev/null and b/classport-analyser/src/test/resources/not-annotated-classes/test-agent-app-1.0-SNAPSHOT.jar differ
diff --git a/pom.xml b/pom.xml
index f4119c5..682c95c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,6 +12,7 @@
https://github.com/chains-project/classport
2024
+
@@ -48,6 +49,26 @@
UTF-8
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.11.3
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.11.3
+
+
+ org.mockito
+ mockito-core
+ 5.14.2
+
+
+
+