Skip to content
Merged
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 @@ -20,6 +20,24 @@ static void enableClassInjection() {
ClassInjector.enableClassInjection(TestAgent.getInstrumentation());
}

@Test
void repeatedEnablementPreservesClassInjection() throws Exception {
ClassLoader loader = newCL();
String name = "datadog.injected.RepeatedEnablementSample";
byte[] bytecode = sampleClass(name);
Class<?> injected = ClassInjector.injectClass(name, bytecode, loader);

ClassInjector.enableClassInjection(TestAgent.getInstrumentation());

assertThat(ClassInjector.injectClass(name, bytecode, loader)).isSameAs(injected);
assertThat(injected.getMethod("value").invoke(null)).isEqualTo(42);
ClassLoader anotherLoader = newCL();
Class<?> another = ClassInjector.injectClass(name, bytecode, anotherLoader);
assertThat(another).isNotSameAs(injected);
assertThat(another.getClassLoader()).isSameAs(anotherLoader);
assertThat(another.getMethod("value").invoke(null)).isEqualTo(42);
}

@Test
void injectsBootClass() throws Exception {
String name = "datadog.injected.BootSample";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,22 @@ void maskComposition(AccessMatcher left, AccessMatcher right, boolean foldAnd, b
}
}

@Test
void negativeMaskAndCustomMatcher() {
AccessMatcher custom = access -> (access & 3) == 3;
AccessMatcher conjunction = AccessMatcher.INSTANCE.and(custom);
for (int access = 0; access <= 0xffff; access++) {
assertEquals(
AccessMatcher.INSTANCE.test(access) && custom.test(access), conjunction.test(access));
}

AccessMatcher unexpected =
access -> {
throw new AssertionError("should short circuit");
};
assertFalse(AccessMatcher.INSTANCE.and(unexpected).test(0x0008));
}

@Test
void maskNegationAndChaining() {
AccessMatcher positive = PUBLIC.or(AccessMatcher.PROTECTED).or(PRIVATE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,43 @@ void differentKeysHaveIndependentEntries() {
assertEquals("v2", store.get(key2));
}

@Test
void equalKeysAreStoredByIdentity() {
char[] characters = {'k', 'e', 'y'};
String first = new String(characters);
String second = new String(characters);
assertThat(first).isEqualTo(second).isNotSameAs(second);

store.put(first, "first");
assertNull(store.get(second));
assertEquals("second", store.getOrPut(second, "second"));
assertEquals("first", store.get(first));
assertEquals("second", store.get(second));

assertEquals("first", store.remove(first));
assertNull(store.get(first));
assertEquals("second", store.get(second));
assertEquals("computed", store.getOrCompute(first, key -> "computed"));
assertEquals("computed", store.get(first));
assertEquals("second", store.get(second));
}

@Test
void sameKeyWithDifferentValueTypesIsIsolated() {
Object key = new Object();
ObjectStore<Object, String> strings = ObjectStore.of(Object.class, String.class);
ObjectStore<Object, Integer> integers = ObjectStore.of(Object.class, Integer.class);

strings.put(key, "string");
assertNull(integers.get(key));
assertEquals(42, integers.getOrCompute(key, ignored -> 42));
assertEquals("string", strings.get(key));
assertEquals(42, integers.remove(key));
assertNull(integers.get(key));
assertEquals("string", strings.get(key));
assertEquals("string", strings.remove(key));
}

@Test
void sameKeyInDifferentStoresIsIsolated() {
ObjectStore<Object, String> storeA = ObjectStore.of("test.IsolA.Key", "test.IsolA.Value");
Expand Down
143 changes: 143 additions & 0 deletions utils/src/test/java/datadog/instrument/utils/ClassNameTrieTest.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package datadog.instrument.utils;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonList;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.joining;
Expand All @@ -26,7 +27,9 @@
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
Expand Down Expand Up @@ -179,6 +182,146 @@ void generateEmptyJavaSource(@TempDir File classesDir) throws Exception {
}
}

@Test
void generateLargeJavaSource(@TempDir File directory) throws Exception {
File trieDir = new File(directory, "input");
File javaDir = new File(directory, "output");
Files.createDirectories(new File(trieDir, "example").toPath());
Files.createDirectories(javaDir.toPath());
Map<String, Integer> mappings = largeTrieMappings();
List<String> lines = new ArrayList<>();
mappings.forEach((key, value) -> lines.add(value + " " + key));
lines.add("default.Value"); // omitted values default to one
Files.write(new File(trieDir, "example/large_names.trie").toPath(), lines, UTF_8);

ClassNameTrie.JavaGenerator.main(
new String[] {trieDir.toString(), javaDir.toString(), "example/large_names.trie"});

assertEquals(
0,
getSystemJavaCompiler()
.run(
null,
null,
null,
"-d",
javaDir.toString(),
new File(javaDir, "example/LargeNamesTrie.java").toString()));

try (URLClassLoader cl = URLClassLoader.newInstance(new URL[] {javaDir.toURI().toURL()})) {
Class<?> trieClass = cl.loadClass("example.LargeNamesTrie");
Method apply = trieClass.getMethod("apply", String.class);
Method applyFromIndex = trieClass.getMethod("apply", String.class, int.class);
for (Map.Entry<String, Integer> entry : mappings.entrySet()) {
assertEquals(entry.getValue(), apply.invoke(null, entry.getKey()));
assertEquals(entry.getValue(), applyFromIndex.invoke(null, "prefix/" + entry.getKey(), 7));
}
assertEquals(1, apply.invoke(null, "default.Value"));
assertEquals(-1, apply.invoke(null, "missing.Type"));
assertEquals(-1, applyFromIndex.invoke(null, "prefix/missing.Type", 7));
}
}

@Test
void roundTripWithLongJumps() throws IOException {
Map<String, Integer> mappings = largeTrieMappings();
ClassNameTrie.Builder builder = new ClassNameTrie.Builder();
mappings.forEach(builder::put);

ClassNameTrie restored =
ClassNameTrie.readFrom(new DataInputStream(new ByteArrayInputStream(serialize(builder))));

for (Map.Entry<String, Integer> entry : mappings.entrySet()) {
assertEquals(entry.getValue(), restored.apply(entry.getKey()));
assertEquals(entry.getValue(), restored.apply("prefix/" + entry.getKey(), 7));
}
assertEquals(-1, restored.apply("missing.Type"));
assertEquals(-1, restored.apply("aaa"));
}

private static Map<String, Integer> largeTrieMappings() {
Map<String, Integer> mappings = new LinkedHashMap<>();
// The first subtree exceeds the short-jump range and the Java string chunk size.
String padding = padding(7700);
for (char branch = 'a'; branch <= 'h'; branch++) {
mappings.put("aa" + branch + padding, branch - 'a' + 1);
}
for (char branch = 'b'; branch <= 'q'; branch++) {
mappings.put("a" + branch, branch - 'a' + 10);
}
return mappings;
}

@TableTest({
"Scenario | Branch | Padding length",
"next jump stays short | m | 6 ",
"next jump reaches long-jump boundary | m | 7 ",
"next jump crosses long-jump boundary | m | 8 ",
"later jump stays short | 0 | 6 ",
"later jump reaches long-jump boundary | 0 | 7 ",
"later jump crosses long-jump boundary | 0 | 8 "
})
void insertingBranchConvertsSiblingJumps(String branch, int paddingLength) throws IOException {
ClassNameTrie.Builder builder = new ClassNameTrie.Builder();
Map<String, Integer> mappings = new LinkedHashMap<>();
for (char child = 'a'; child <= 'h'; child++) {
mappings.put("a" + child + padding(7675), child - 'a' + 1);
}
mappings.put("z", 9);
mappings.forEach(builder::put);
ClassNameTrie original = builder.buildTrie();

// The 'z' jump is 61432: inserting 7, 8, or 9 chars straddles the 61440 boundary.
// 'm' inserts immediately before 'z'; '0' also shifts the later sibling jump to 'z'.
String inserted = branch + padding(paddingLength);
builder.put(inserted, 10);
ClassNameTrie updated = builder.buildTrie();
ClassNameTrie restored =
ClassNameTrie.readFrom(new DataInputStream(new ByteArrayInputStream(serialize(builder))));

for (Map.Entry<String, Integer> entry : mappings.entrySet()) {
assertEquals(entry.getValue(), original.apply(entry.getKey()));
assertEquals(entry.getValue(), builder.apply(entry.getKey()));
assertEquals(entry.getValue(), updated.apply(entry.getKey()));
assertEquals(entry.getValue(), restored.apply(entry.getKey()));
}
assertEquals(-1, original.apply(inserted));
assertEquals(10, builder.apply(inserted));
assertEquals(10, updated.apply(inserted));
assertEquals(10, restored.apply(inserted));
assertEquals(-1, restored.apply(inserted + "missing"));
}

@TableTest({
"Scenario | First | Second | Third",
"shorten a leaf segment | abc | ab | a ",
"prefix at a branch boundary | abc | abd | ab ",
"diverge left at the last char | abc | abb | a ",
"extend a left branch | abc | abbx | ab ",
"extend then shorten a segment | abc | abcd | ab ",
"prefix before a short leaf | ab | a | ac ",
"diverge right at last char | abc | abx | aby ",
"prefix before longer branches | abxyz | abqrs | ab "
})
void insertingPrefixesAndDivergingKeysPreservesMappings(String first, String second, String third)
throws IOException {
ClassNameTrie.Builder builder = new ClassNameTrie.Builder();
String[] keys = {first, second, third};
for (int i = 0; i < keys.length; i++) {
builder.put(keys[i], i + 1);
ClassNameTrie trie = builder.buildTrie();
ClassNameTrie restored =
ClassNameTrie.readFrom(new DataInputStream(new ByteArrayInputStream(serialize(builder))));
for (int j = 0; j <= i; j++) {
assertEquals(j + 1, builder.apply(keys[j]));
assertEquals(j + 1, trie.apply(keys[j]));
assertEquals(j + 1, restored.apply(keys[j]));
}
assertEquals(-1, trie.apply("missing"));
assertEquals(-1, trie.apply(keys[i] + "missing"));
}
}

@Test
@SuppressWarnings("UnnecessaryUnicodeEscape")
void manual() throws IOException {
Expand Down
Loading