diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/ByteCodeLinesResolver.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/ByteCodeLinesResolver.java index c77a1f92e5d..46c043455ef 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/ByteCodeLinesResolver.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/ByteCodeLinesResolver.java @@ -66,6 +66,10 @@ public static ClassMethodLines parse(Class> clazz) { try { ClassMethodLines classMethodLines = new ClassMethodLines(); try (InputStream classStream = Utils.getClassStream(clazz)) { + if (classStream == null) { + log.debug("Could not get input stream for class {}", clazz.getName()); + return classMethodLines; + } ClassReader classReader = new ClassReader(classStream); MethodLocator methodLocator = new MethodLocator(classMethodLines); classReader.accept(methodLocator, ClassReader.SKIP_FRAMES); diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/ByteCodeLinesResolverTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/ByteCodeLinesResolverTest.groovy deleted file mode 100644 index 0960a2a40ab..00000000000 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/ByteCodeLinesResolverTest.groovy +++ /dev/null @@ -1,85 +0,0 @@ -package datadog.trace.civisibility.source - -import org.spockframework.util.IoUtil -import spock.lang.Specification - -class ByteCodeLinesResolverTest extends Specification { - - def "test method lines resolution"() { - setup: - def aTestMethod = NestedClass.getDeclaredMethod("aTestMethod") - - when: - def linesResolver = new ByteCodeLinesResolver() - def methodLines = linesResolver.getMethodLines(aTestMethod) - - then: - methodLines.isValid() - methodLines.startLineNumber > 0 - methodLines.endLineNumber > methodLines.startLineNumber - } - - def "test always invalid class lines resolution" () { - when: - def linesResolver = new ByteCodeLinesResolver() - def classLines = linesResolver.getClassLines(NestedClass) - - then: - !classLines.isValid() - } - - def "test invalid method lines resolution"() { - setup: - def aTestMethod = NestedClass.getDeclaredMethod("abstractMethod") - - when: - def linesResolver = new ByteCodeLinesResolver() - def methodLines = linesResolver.getMethodLines(aTestMethod) - - then: - !methodLines.isValid() - } - - def "test returns empty method lines when class cannot be loaded"() { - setup: - def misbehavingClassLoader = new MisbehavingClassLoader() - - Utils.getClassStream(NestedClass).withCloseable { stream -> - def baos = new ByteArrayOutputStream() - IoUtil.copyStream(stream, baos) - misbehavingClassLoader.putClass(NestedClass.name, baos.toByteArray()) - } - - def misbehavingClass = misbehavingClassLoader.loadClass(NestedClass.name) - def misbehavingMethod = misbehavingClass.getDeclaredMethod("aTestMethod") - - when: - def linesResolver = new ByteCodeLinesResolver() - def methodLines = linesResolver.getMethodLines(misbehavingMethod) - - then: - !methodLines.isValid() - } - - def "test returns empty method lines when unknown method is attempted to be resolved"() { - setup: - def aTestMethod = NestedClass.getDeclaredMethod("abstractMethod") - def classMethodLines = new ByteCodeLinesResolver.ClassMethodLines() - - when: - def methodLines = classMethodLines.get(aTestMethod) - - then: - !methodLines.isValid() - } - - private static abstract class NestedClass { - static double aTestMethod() { - def random = Math.random() - return random - } - - abstract void abstractMethod() - } -} - diff --git a/dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/source/ByteCodeLinesResolverTest.java b/dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/source/ByteCodeLinesResolverTest.java new file mode 100644 index 00000000000..7423af078cf --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/source/ByteCodeLinesResolverTest.java @@ -0,0 +1,115 @@ +package datadog.trace.civisibility.source; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.civisibility.source.LinesResolver.Lines; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import org.junit.jupiter.api.Test; + +class ByteCodeLinesResolverTest { + + @Test + void testMethodLinesResolution() throws NoSuchMethodException { + Method aTestMethod = NestedClass.class.getDeclaredMethod("aTestMethod"); + + ByteCodeLinesResolver linesResolver = new ByteCodeLinesResolver(); + Lines methodLines = linesResolver.getMethodLines(aTestMethod); + + assertTrue(methodLines.isValid()); + assertTrue(methodLines.getStartLineNumber() > 0); + assertTrue(methodLines.getEndLineNumber() > methodLines.getStartLineNumber()); + } + + @Test + void testAlwaysInvalidClassLinesResolution() { + ByteCodeLinesResolver linesResolver = new ByteCodeLinesResolver(); + Lines classLines = linesResolver.getClassLines(NestedClass.class); + + assertFalse(classLines.isValid()); + } + + @Test + void testInvalidMethodLinesResolution() throws NoSuchMethodException { + Method abstractMethod = NestedClass.class.getDeclaredMethod("abstractMethod"); + + ByteCodeLinesResolver linesResolver = new ByteCodeLinesResolver(); + Lines methodLines = linesResolver.getMethodLines(abstractMethod); + + assertFalse(methodLines.isValid()); + } + + @Test + void testReturnsEmptyMethodLinesWhenClassCannotBeLoaded() + throws IOException, ClassNotFoundException, NoSuchMethodException { + MisbehavingClassLoader misbehavingClassLoader = new MisbehavingClassLoader(); + + try (InputStream stream = Utils.getClassStream(NestedClass.class)) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = stream.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + misbehavingClassLoader.putClass(NestedClass.class.getName(), baos.toByteArray()); + } + + Class> misbehavingClass = misbehavingClassLoader.loadClass(NestedClass.class.getName()); + Method misbehavingMethod = misbehavingClass.getDeclaredMethod("aTestMethod"); + + ByteCodeLinesResolver linesResolver = new ByteCodeLinesResolver(); + Lines methodLines = linesResolver.getMethodLines(misbehavingMethod); + + assertFalse(methodLines.isValid()); + } + + @Test + void testReturnsEmptyMethodLinesWhenClassResourceIsMissing() + throws IOException, ClassNotFoundException, NoSuchMethodException { + // regression test: Utils.getClassStream() returns null (rather than throwing) for + // classes whose bytecode resource cannot be located (e.g. certain generated/proxy classes) + NullResourceClassLoader nullResourceClassLoader = new NullResourceClassLoader(); + + try (InputStream stream = Utils.getClassStream(NestedClass.class)) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int bytesRead; + while ((bytesRead = stream.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + nullResourceClassLoader.putClass(NestedClass.class.getName(), baos.toByteArray()); + } + + Class> unresolvableClass = nullResourceClassLoader.loadClass(NestedClass.class.getName()); + Method unresolvableMethod = unresolvableClass.getDeclaredMethod("aTestMethod"); + + ByteCodeLinesResolver linesResolver = new ByteCodeLinesResolver(); + Lines methodLines = linesResolver.getMethodLines(unresolvableMethod); + + assertFalse(methodLines.isValid()); + } + + @Test + void testReturnsEmptyMethodLinesWhenUnknownMethodIsAttemptedToBeResolved() + throws NoSuchMethodException { + Method abstractMethod = NestedClass.class.getDeclaredMethod("abstractMethod"); + ByteCodeLinesResolver.ClassMethodLines classMethodLines = + new ByteCodeLinesResolver.ClassMethodLines(); + + Lines methodLines = classMethodLines.get(abstractMethod); + + assertFalse(methodLines.isValid()); + } + + private abstract static class NestedClass { + static double aTestMethod() { + double random = Math.random(); + return random; + } + + abstract void abstractMethod(); + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/source/NullResourceClassLoader.java b/dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/source/NullResourceClassLoader.java new file mode 100644 index 00000000000..8b7bc588a05 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/source/NullResourceClassLoader.java @@ -0,0 +1,36 @@ +package datadog.trace.civisibility.source; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * A {@link ClassLoader} that defines classes from an in-memory map and returns {@code null} from + * resource lookups (rather than throwing), used to exercise {@code ByteCodeLinesResolver} when a + * class's bytecode resource cannot be located. + * + *
Kept in Java rather than the Java 8 test suite's Groovy counterpart on purpose: see {@link
+ * MisbehavingClassLoader} for why.
+ */
+final class NullResourceClassLoader extends ClassLoader {
+
+ private final Map