From c34874c3282de69a3470f668ef39c66b6d40e504 Mon Sep 17 00:00:00 2001 From: Douglas Q Hawkins Date: Wed, 2 Sep 2026 06:44:15 -0400 Subject: [PATCH] Return empty method lines instead of throwing when class bytecode resource is missing Utils.getClassStream() is documented @Nullable and can return null for classes whose .class resource can't be located on the classpath (e.g. Mockito-generated proxy classes). ByteCodeLinesResolver.ClassMethodLines.parse passed that possibly-null stream straight into ClassReader, which threw an IOException that got wrapped and logged at ERROR, then swallowed. This has been recurring in error tracking for CI Visibility test runs across many tracer versions. Short-circuit on a null stream and log at debug instead, matching the existing handling in Utils.getFileName for the same condition. Co-Authored-By: Claude Sonnet 5 --- .../source/ByteCodeLinesResolver.java | 4 + .../source/ByteCodeLinesResolverTest.groovy | 85 ------------- .../source/ByteCodeLinesResolverTest.java | 115 ++++++++++++++++++ .../source/NullResourceClassLoader.java | 36 ++++++ 4 files changed, 155 insertions(+), 85 deletions(-) delete mode 100644 dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/source/ByteCodeLinesResolverTest.groovy create mode 100644 dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/source/ByteCodeLinesResolverTest.java create mode 100644 dd-java-agent/agent-ci-visibility/src/test/java/datadog/trace/civisibility/source/NullResourceClassLoader.java 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 classes = new HashMap<>(); + + @Override + public InputStream getResourceAsStream(String name) { + return null; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + byte[] bytes = classes.get(name); + if (bytes != null) { + return defineClass(name, bytes, 0, bytes.length); + } + return super.loadClass(name); + } + + void putClass(String name, byte[] bytes) { + classes.put(name, bytes); + } +}