Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +69 to +71

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid caching a resource lookup failure

When a custom class loader temporarily returns null but can provide the resource later, returning this non-null empty value from ClassMethodLines.parse causes methodLinesCache.computeIfAbsent to cache the failed lookup. Subsequent calls that hit the entry therefore never retry and continue returning missing method borders, unlike the existing exception path explicitly marked “do not cache failure”; handle the null case without storing a negative result while still suppressing the error-level log.

Useful? React with 👍 / 👎.

}
ClassReader classReader = new ClassReader(classStream);
MethodLocator methodLocator = new MethodLocator(classMethodLines);
classReader.accept(methodLocator, ClassReader.SKIP_FRAMES);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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());
Comment on lines +89 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the regression test distinguish the new behavior

This assertion also passes against the previous implementation: ClassReader throws for the null stream, but getMethodLines catches that exception and already returns Lines.EMPTY. Consequently, removing the new guard and restoring the production ERROR log would leave this regression test green; call ClassMethodLines.parse directly and assert it does not throw, or capture the logger and verify the error-level event is absent.

Useful? React with 👍 / 👎.

}

@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();
}
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Kept in Java rather than the Java 8 test suite's Groovy counterpart on purpose: see {@link
* MisbehavingClassLoader} for why.
*/
Comment on lines +7 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit:

Suggested change
/**
* 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.
*
* <p>Kept in Java rather than the Java 8 test suite's Groovy counterpart on purpose: see {@link
* MisbehavingClassLoader} for why.
*/
/**
* 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.
*/

final class NullResourceClassLoader extends ClassLoader {

private final Map<String, byte[]> 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);
}
}
Loading