Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: test for the agent added #17

Merged
merged 14 commits into from
Nov 29, 2024
Merged
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
target/
*.class
.DS_Store

# jdtls/eclipse stuff
Expand Down
17 changes: 17 additions & 0 deletions classport-agent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,24 @@
<artifactId>classport-commons</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

</dependencies>


<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -25,7 +24,7 @@ public class ClassportAgent {
private static final ArrayList<String> noAnnotations = new ArrayList<>();

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

Expand Down
147 changes: 147 additions & 0 deletions classport-agent/src/test/java/ClassportAgentTest.java
Original file line number Diff line number Diff line change
@@ -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<String, ClassportInfo> 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<String, ClassportInfo> 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 = "<Unknown> (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<String, ClassportInfo> mockSBOM() {
HashMap<String, ClassportInfo> 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; }
};
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
com.google.code.gson:gson:jar:2.11.0
org.apache.commons:commons-lang3:jar:3.17.0
Original file line number Diff line number Diff line change
@@ -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
Binary file not shown.
15 changes: 15 additions & 0 deletions classport-analyser/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@
<artifactId>asm-util</artifactId>
<version>9.6</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, ClassportInfo> getSBOM(JarFile jar) {
public static HashMap<String, ClassportInfo> getSBOM(JarFile jar) {
HashMap<String, ClassportInfo> sbom = new HashMap<>();
HashMap<String, Integer> noAnnotations = new HashMap<>();
try {
Expand Down
112 changes: 112 additions & 0 deletions classport-analyser/src/test/java/AnalyserTest.java
Original file line number Diff line number Diff line change
@@ -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<String, ClassportInfo> 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<String, ClassportInfo> 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

@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;
}
}

Binary file not shown.
Binary file not shown.
Binary file not shown.
21 changes: 21 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<url>https://github.com/chains-project/classport</url>
<inceptionYear>2024</inceptionYear>


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

<licenses>
Expand Down Expand Up @@ -48,6 +49,26 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.3</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.11.3</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.14.2</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<pluginManagement>
<plugins>
Expand Down