Skip to content

Commit 36628a3

Browse files
test: test for the agent added (#17)
* tests: adding java agent tests * tests on empty annotattion or sbom added * fix * Update .gitignore Co-authored-by: Aman Sharma <[email protected]> * Update classport-agent/pom.xml Co-authored-by: Aman Sharma <[email protected]> * cross-platform compatibility fixed * fix dependency issue * made writeSBOM public * change name of getSBOM -> mockSBOM * names refactoring * test Analyser * test for static analyser completed * fix in boolean variable --------- Co-authored-by: Aman Sharma <[email protected]>
1 parent 7df77a2 commit 36628a3

File tree

15 files changed

+322
-7
lines changed

15 files changed

+322
-7
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
target/
2-
*.class
32
.DS_Store
43

54
# jdtls/eclipse stuff

classport-agent/pom.xml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,24 @@
2929
<artifactId>classport-commons</artifactId>
3030
<version>${project.parent.version}</version>
3131
</dependency>
32+
<dependency>
33+
<groupId>org.junit.jupiter</groupId>
34+
<artifactId>junit-jupiter-api</artifactId>
35+
<scope>test</scope>
36+
</dependency>
37+
<dependency>
38+
<groupId>org.junit.jupiter</groupId>
39+
<artifactId>junit-jupiter-engine</artifactId>
40+
<scope>test</scope>
41+
</dependency>
42+
<dependency>
43+
<groupId>org.mockito</groupId>
44+
<artifactId>mockito-core</artifactId>
45+
<scope>test</scope>
46+
</dependency>
47+
3248
</dependencies>
49+
3350

3451
<build>
3552
<plugins>

classport-agent/src/main/java/io/github/chains_project/classport/agent/ClassportAgent.java

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,16 @@
44
import java.io.File;
55
import java.io.FileWriter;
66
import java.io.IOException;
7-
import java.io.OutputStreamWriter;
8-
import java.io.Writer;
9-
import java.lang.instrument.*;
7+
import java.lang.instrument.ClassFileTransformer;
8+
import java.lang.instrument.Instrumentation;
109
import java.security.ProtectionDomain;
1110
import java.util.ArrayList;
1211
import java.util.HashMap;
1312
import java.util.Map;
1413

14+
import io.github.chains_project.classport.commons.AnnotationReader;
1515
import io.github.chains_project.classport.commons.ClassportInfo;
1616
import io.github.chains_project.classport.commons.ClassportProject;
17-
import io.github.chains_project.classport.commons.AnnotationReader;
1817

1918
/**
2019
* A classport agent, ready to check the classports of any and all incoming
@@ -25,7 +24,7 @@ public class ClassportAgent {
2524
private static final ArrayList<String> noAnnotations = new ArrayList<>();
2625

2726
// TODO: Output in a useful format (JSON?)
28-
private static void writeSBOM(Map<String, ClassportInfo> sbom) throws IOException {
27+
public static void writeSBOM(Map<String, ClassportInfo> sbom) throws IOException {
2928
File treeOutputFile = new File("classport-deps-tree");
3029
File listOutputFile = new File("classport-deps-list");
3130

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import java.io.File;
2+
import java.io.FileInputStream;
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.lang.annotation.Annotation;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.util.HashMap;
9+
10+
import static org.junit.jupiter.api.Assertions.assertAll;
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
import static org.junit.jupiter.api.Assertions.assertNull;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
import org.junit.jupiter.api.Test;
15+
16+
import io.github.chains_project.classport.agent.ClassportAgent;
17+
import io.github.chains_project.classport.commons.AnnotationReader;
18+
import io.github.chains_project.classport.commons.ClassportInfo;
19+
20+
public class ClassportAgentTest {
21+
22+
private static final Path CLASSPORT_TREE_PATH = Path.of("src/test/resources/classport-deps/classport-deps-tree");
23+
private static final Path CLASSPORT_LIST_PATH = Path.of("src/test/resources/classport-deps/classport-deps-list");
24+
private static final Path ANNOTATED_CLASS_PATH = Path.of("src/test/resources/annotated-classes/StringUtils.class");
25+
private static final Path NOT_ANNOTATED_CLASS_PATH = Path.of("src/test/resources/not-annotated-classes/StringUtils.class");
26+
27+
@Test
28+
void shouldGenerateDependencyListAndTreeFiles() throws Exception {
29+
HashMap<String, ClassportInfo> sbom = mockSBOM();
30+
31+
ClassportAgent.writeSBOM(sbom);
32+
33+
File actualTreeFile = new File(System.getProperty("user.dir"), "classport-deps-tree");
34+
File actualListFile = new File(System.getProperty("user.dir"), "classport-deps-list");
35+
36+
assertAll(
37+
() -> assertTrue(actualTreeFile.exists(), "Tree output file should be created"),
38+
() -> assertTrue(actualListFile.exists(), "List output file should be created"),
39+
() -> assertEquals(
40+
Files.readString(CLASSPORT_TREE_PATH),
41+
Files.readString(actualTreeFile.toPath()),
42+
"Tree files should be identical"),
43+
() -> assertEquals(
44+
Files.readString(CLASSPORT_LIST_PATH),
45+
Files.readString(actualListFile.toPath()),
46+
"List files should be identical")
47+
);
48+
49+
actualTreeFile.delete();
50+
actualListFile.delete();
51+
}
52+
53+
54+
@Test
55+
void shouldHandleEmptySBOM() throws Exception {
56+
HashMap<String, ClassportInfo> emptySBOM = new HashMap<>();
57+
58+
ClassportAgent.writeSBOM(emptySBOM);
59+
60+
File treeFile = new File(System.getProperty("user.dir"), "classport-deps-tree");
61+
File listFile = new File(System.getProperty("user.dir"), "classport-deps-list");
62+
63+
assertTrue(treeFile.exists(), "Tree output file should be created even if SBOM is empty");
64+
assertTrue(listFile.exists(), "List output file should be created even if SBOM is empty");
65+
66+
String treeContent = new String(Files.readAllBytes(treeFile.toPath()));
67+
String expectedPlaceholder = "<Unknown> (parent-only artefact)";
68+
69+
assertTrue(treeContent.contains(expectedPlaceholder), "Tree file should contain placeholder text when SBOM is empty");
70+
assertTrue(listFile.length() == 0, "List file should be empty when SBOM is empty");
71+
72+
// Clean up after the test
73+
treeFile.delete();
74+
listFile.delete();
75+
}
76+
77+
@Test
78+
void shouldAnnotationBeCorrectlyRead() throws IOException {
79+
File classFile = ANNOTATED_CLASS_PATH.toFile();
80+
byte[] buffer = loadClassFromFile(classFile);
81+
ClassportInfo actualAnnotation = AnnotationReader.getAnnotationValues(buffer);
82+
83+
assertAll(
84+
() -> assertEquals("commons-lang3", actualAnnotation.artefact()),
85+
() -> assertEquals("org.apache.commons", actualAnnotation.group()),
86+
() -> assertEquals("org.apache.commons:commons-lang3:jar:3.17.0", actualAnnotation.id()),
87+
() -> assertTrue(actualAnnotation.isDirectDependency()),
88+
() -> assertEquals("com.example:test-agent-app:jar:1.0-SNAPSHOT", actualAnnotation.sourceProjectId()),
89+
() -> assertEquals("3.17.0", actualAnnotation.version()),
90+
() -> assertEquals("org.apache.commons:commons-text:jar:1.12.0", actualAnnotation.childIds()[0])
91+
);
92+
}
93+
94+
@Test
95+
void shouldReturnNonAnnotatedClassesNull() throws Exception {
96+
byte[] nonAnnotatedClassBytes = loadClassFromFile(NOT_ANNOTATED_CLASS_PATH.toFile());
97+
ClassportInfo actualAnnotation = AnnotationReader.getAnnotationValues(nonAnnotatedClassBytes);
98+
assertNull(actualAnnotation);
99+
}
100+
101+
102+
private byte[] loadClassFromFile(File classFile) throws IOException {
103+
try (InputStream inputStream = new FileInputStream(classFile)) {
104+
return inputStream.readAllBytes();
105+
}
106+
}
107+
108+
109+
private HashMap<String, ClassportInfo> mockSBOM() {
110+
HashMap<String, ClassportInfo> sbom = new HashMap<>();
111+
sbom.put("com.example:test-agent-app:jar:1.0-SNAPSHOT",
112+
createClassportInfo("com.example", "1.0-SNAPSHOT",
113+
"com.example:test-agent-app:jar:1.0-SNAPSHOT",
114+
"test-agent-app", false,
115+
"com.example:test-agent-app:jar:1.0-SNAPSHOT",
116+
new String[]{"com.google.code.gson:gson:jar:2.11.0",
117+
"org.apache.commons:commons-lang3:jar:3.17.0"}));
118+
sbom.put("com.google.code.gson:gson:jar:2.11.0",
119+
createClassportInfo("com.google.code.gson", "2.11.0",
120+
"com.google.code.gson:gson:jar:2.11.0",
121+
"gson", true,
122+
"com.example:test-agent-app:jar:1.0-SNAPSHOT",
123+
new String[]{"com.google.errorprone:error_prone_annotations:jar:2.27.0"}));
124+
sbom.put("org.apache.commons:commons-lang3:jar:3.17.0",
125+
createClassportInfo("org.apache.commons", "3.17.0",
126+
"org.apache.commons:commons-lang3:jar:3.17.0",
127+
"commons-lang3", true,
128+
"com.example:test-agent-app:jar:1.0-SNAPSHOT",
129+
new String[]{"org.apache.commons:commons-text:jar:1.12.0"}));
130+
return sbom;
131+
}
132+
133+
private ClassportInfo createClassportInfo(String group, String version, String id,
134+
String artefact, boolean isDirectDependency,
135+
String sourceProjectId, String[] childIds) {
136+
return new ClassportInfo() {
137+
@Override public String group() { return group; }
138+
@Override public String version() { return version; }
139+
@Override public String id() { return id; }
140+
@Override public String artefact() { return artefact; }
141+
@Override public boolean isDirectDependency() { return isDirectDependency; }
142+
@Override public String sourceProjectId() { return sourceProjectId; }
143+
@Override public String[] childIds() { return childIds; }
144+
@Override public Class<? extends Annotation> annotationType() { return ClassportInfo.class; }
145+
};
146+
}
147+
}
Binary file not shown.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
com.google.code.gson:gson:jar:2.11.0
2+
org.apache.commons:commons-lang3:jar:3.17.0
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
com.example:test-agent-app:jar:1.0-SNAPSHOT
2+
+- com.google.code.gson:gson:jar:2.11.0
3+
\- org.apache.commons:commons-lang3:jar:3.17.0
Binary file not shown.

classport-analyser/pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@
5050
<artifactId>asm-util</artifactId>
5151
<version>9.6</version>
5252
</dependency>
53+
<dependency>
54+
<groupId>org.junit.jupiter</groupId>
55+
<artifactId>junit-jupiter-api</artifactId>
56+
<scope>test</scope>
57+
</dependency>
58+
<dependency>
59+
<groupId>org.junit.jupiter</groupId>
60+
<artifactId>junit-jupiter-engine</artifactId>
61+
<scope>test</scope>
62+
</dependency>
63+
<dependency>
64+
<groupId>org.mockito</groupId>
65+
<artifactId>mockito-core</artifactId>
66+
<scope>test</scope>
67+
</dependency>
5368
</dependencies>
5469

5570
<build>

classport-analyser/src/main/java/io/github/chains_project/classport/analyser/Analyser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class Analyser {
3030
// and use this from there.
3131
private static final byte[] magicBytes = new byte[] { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
3232

33-
private static HashMap<String, ClassportInfo> getSBOM(JarFile jar) {
33+
public static HashMap<String, ClassportInfo> getSBOM(JarFile jar) {
3434
HashMap<String, ClassportInfo> sbom = new HashMap<>();
3535
HashMap<String, Integer> noAnnotations = new HashMap<>();
3636
try {
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
2+
import java.io.ByteArrayOutputStream;
3+
import java.io.IOException;
4+
import java.io.PrintStream;
5+
import java.lang.reflect.InvocationTargetException;
6+
import java.nio.file.Files;
7+
import java.nio.file.Path;
8+
import java.util.HashMap;
9+
import java.util.jar.JarFile;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
import static org.junit.jupiter.api.Assertions.assertNotNull;
13+
import static org.junit.jupiter.api.Assertions.assertTrue;
14+
import org.junit.jupiter.api.Test;
15+
import org.objectweb.asm.ClassReader;
16+
import org.objectweb.asm.ClassVisitor;
17+
import org.objectweb.asm.MethodVisitor;
18+
import org.objectweb.asm.Opcodes;
19+
20+
import io.github.chains_project.classport.analyser.Analyser;
21+
import io.github.chains_project.classport.analyser.ClassLoadingAdder;
22+
import io.github.chains_project.classport.commons.ClassportInfo;
23+
24+
class AnalyserTest {
25+
26+
private final Path annotatedJarPath = Path.of("src/test/resources/annotated-classes/test-agent-app-1.0-SNAPSHOT.jar");
27+
private final Path notAnnotatedJarPath = Path.of("src/test/resources/not-annotated-classes/test-agent-app-1.0-SNAPSHOT.jar");
28+
private final String addedClassName = "__classportForceClassLoading";
29+
private final String addedClassDescriptor = "()V";
30+
private final Path annotatedClassPath = Path.of("src/test/resources/annotated-classes/Main.class");
31+
private boolean methodPresent;
32+
private boolean methodInvoked;
33+
34+
@Test
35+
void testGetSBOM_withAnnotatedClasses() throws IOException {
36+
JarFile jar = new JarFile(annotatedJarPath.toFile());
37+
HashMap<String, ClassportInfo> actualSbom = Analyser.getSBOM(jar);
38+
39+
assertTrue(!actualSbom.isEmpty());
40+
assertEquals(4, actualSbom.size(), "SBOM should contain 4 annotated classes");
41+
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");
42+
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");
43+
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");
44+
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");
45+
}
46+
47+
@Test
48+
void testGetSBOM_withNonAnnotatedClasses() throws IOException {
49+
ByteArrayOutputStream errContent = new ByteArrayOutputStream();
50+
System.setErr(new PrintStream(errContent));
51+
52+
JarFile jar = new JarFile(notAnnotatedJarPath.toFile());
53+
HashMap<String, ClassportInfo> actualSbom = Analyser.getSBOM(jar);
54+
55+
assertTrue(actualSbom.isEmpty(),"The sbom should be empty if the jar file does not contain annotated classes");
56+
String actualMessage = errContent.toString();
57+
assertTrue(actualMessage.contains("[Warning]"), "The warning message should match the expected pattern");
58+
}
59+
60+
@Test
61+
void testForceClassLoading_withAnnotation() throws IOException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
62+
Path path = annotatedClassPath;
63+
byte[] originalMainClass = Files.readAllBytes(path);
64+
65+
String[] classesToLoad = {"com/google/errorprone/annotations/MustBeClosed", "com/google/gson/ExclusionStrategy", "org/apache/commons/lang3/BooleanUtils"};
66+
byte[] modifiedClass = ClassLoadingAdder.forceClassLoading(originalMainClass, classesToLoad);
67+
68+
assertNotNull(modifiedClass, "Modified class byte array should not be null");
69+
assertTrue(modifiedClass.length > 0, "Modified class should have content");
70+
71+
ClassReader classReader = new ClassReader(modifiedClass);
72+
assertTrue(isAdditionalMethodPresent(classReader), "The additional method __classportForceClassLoading should be present");
73+
assertTrue(isAdditionalMethodInvoked(classReader), "The additional method __classportForceClassLoading should be invoked");
74+
75+
}
76+
77+
private boolean isAdditionalMethodPresent(ClassReader classReader) {
78+
methodPresent = false;
79+
classReader.accept(new ClassVisitor(Opcodes.ASM9) {
80+
81+
@Override
82+
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
83+
if (name.equals(addedClassName) && descriptor.equals(addedClassDescriptor)) {
84+
methodPresent = true;
85+
}
86+
return super.visitMethod(access, name, descriptor, signature, exceptions);
87+
}
88+
}, 0);
89+
return methodPresent;
90+
}
91+
92+
private boolean isAdditionalMethodInvoked(ClassReader classReader) {
93+
methodInvoked = false;
94+
classReader.accept(new ClassVisitor(Opcodes.ASM9) {
95+
@Override
96+
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
97+
return new MethodVisitor(Opcodes.ASM9) {
98+
@Override
99+
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
100+
if (name.equals(addedClassName) && descriptor.equals(addedClassDescriptor)) {
101+
methodInvoked = true;
102+
}
103+
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
104+
}
105+
};
106+
}
107+
108+
}, 0);
109+
return methodInvoked;
110+
}
111+
}
112+
Binary file not shown.

pom.xml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<url>https://github.com/chains-project/classport</url>
1313
<inceptionYear>2024</inceptionYear>
1414

15+
1516
<!-- TODO: Set variables for url, version etc. that are inherited in child POMs -->
1617

1718
<licenses>
@@ -48,6 +49,26 @@
4849
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
4950
</properties>
5051

52+
<dependencyManagement>
53+
<dependencies>
54+
<dependency>
55+
<groupId>org.junit.jupiter</groupId>
56+
<artifactId>junit-jupiter-api</artifactId>
57+
<version>5.11.3</version>
58+
</dependency>
59+
<dependency>
60+
<groupId>org.junit.jupiter</groupId>
61+
<artifactId>junit-jupiter-engine</artifactId>
62+
<version>5.11.3</version>
63+
</dependency>
64+
<dependency>
65+
<groupId>org.mockito</groupId>
66+
<artifactId>mockito-core</artifactId>
67+
<version>5.14.2</version>
68+
</dependency>
69+
</dependencies>
70+
</dependencyManagement>
71+
5172
<build>
5273
<pluginManagement>
5374
<plugins>

0 commit comments

Comments
 (0)