diff --git a/build.gradle.kts b/build.gradle.kts index 084d506..a12ae52 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,6 +12,7 @@ plugins { spotlessPredeclare { java { googleJavaFormat() + tableTestFormatter() } } diff --git a/buildSrc/src/main/kotlin/java-common.gradle.kts b/buildSrc/src/main/kotlin/java-common.gradle.kts index b8e9a29..3619cc2 100644 --- a/buildSrc/src/main/kotlin/java-common.gradle.kts +++ b/buildSrc/src/main/kotlin/java-common.gradle.kts @@ -20,6 +20,7 @@ dependencies { compileOnly(libs.spotbugs.annotations) testCompileOnly(libs.spotbugs.annotations) testImplementation(libs.junit.jupiter) + testImplementation(libs.tabletest) testImplementation(libs.assertj.core) testRuntimeOnly(libs.junit.launcher) } @@ -76,6 +77,7 @@ spotless { importOrder() removeUnusedImports() googleJavaFormat() + tableTestFormatter() } } diff --git a/class-inject/src/test/java/datadog/instrument/glue/DefineClassGlueGeneratorTest.java b/class-inject/src/test/java/datadog/instrument/glue/DefineClassGlueGeneratorTest.java index d234c3f..4d74859 100644 --- a/class-inject/src/test/java/datadog/instrument/glue/DefineClassGlueGeneratorTest.java +++ b/class-inject/src/test/java/datadog/instrument/glue/DefineClassGlueGeneratorTest.java @@ -5,15 +5,17 @@ import java.io.PrintWriter; import java.io.StringWriter; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.objectweb.asm.ClassReader; import org.objectweb.asm.util.CheckClassAdapter; +import org.tabletest.junit.TableTest; class DefineClassGlueGeneratorTest { - @ParameterizedTest - @ValueSource(strings = {"sun/misc", "jdk/internal/misc"}) + @TableTest({ + "unsafeNamespace ", + "'sun/misc' ", + "'jdk/internal/misc'" + }) void generatedBytecodePassesVerification(String unsafeNamespace) { byte[] bytecode = DefineClassGlueGenerator.generateBytecode(unsafeNamespace); StringWriter errors = new StringWriter(); @@ -25,8 +27,11 @@ void generatedBytecodePassesVerification(String unsafeNamespace) { } /** ClassInjector inlines the glue unpacking, so check the packed glue decodes as expected. */ - @ParameterizedTest - @ValueSource(strings = {"sun/misc", "jdk/internal/misc"}) + @TableTest({ + "unsafeNamespace ", + "'sun/misc' ", + "'jdk/internal/misc'" + }) void packedGlueUnpacksToGeneratedBytecode(String unsafeNamespace) { byte[] originalBytecode = DefineClassGlueGenerator.generateBytecode(unsafeNamespace); String packedBytecode = diff --git a/class-match/src/test/java/datadog/instrument/classmatch/AccessMatcherTest.java b/class-match/src/test/java/datadog/instrument/classmatch/AccessMatcherTest.java index e335279..185ab54 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/AccessMatcherTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/AccessMatcherTest.java @@ -1,61 +1,53 @@ package datadog.instrument.classmatch; -import static datadog.instrument.classmatch.AccessMatcher.ABSTRACT; -import static datadog.instrument.classmatch.AccessMatcher.CLASS; -import static datadog.instrument.classmatch.AccessMatcher.CONCRETE; -import static datadog.instrument.classmatch.AccessMatcher.FINAL; -import static datadog.instrument.classmatch.AccessMatcher.INSTANCE; -import static datadog.instrument.classmatch.AccessMatcher.INTERFACE; -import static datadog.instrument.classmatch.AccessMatcher.NON_FINAL; -import static datadog.instrument.classmatch.AccessMatcher.PACKAGE_PRIVATE; import static datadog.instrument.classmatch.AccessMatcher.PRIVATE; -import static datadog.instrument.classmatch.AccessMatcher.PROTECTED; import static datadog.instrument.classmatch.AccessMatcher.PUBLIC; import static datadog.instrument.classmatch.AccessMatcher.STATIC; -import static datadog.instrument.classmatch.AccessMatcher.SYNCHRONIZED; -import static datadog.instrument.classmatch.AccessMatcher.TRANSIENT; -import static datadog.instrument.classmatch.AccessMatcher.VARARGS; -import static datadog.instrument.classmatch.AccessMatcher.VOLATILE; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; +import org.tabletest.junit.TypeConverter; +import org.tabletest.junit.TypeConverterSources; +@TypeConverterSources(AccessMatcherTest.Converters.class) class AccessMatcherTest { - static Stream flags() { - return Stream.of( - Arguments.of("public", PUBLIC, 0x0001, false), - Arguments.of("private", PRIVATE, 0x0002, false), - Arguments.of("protected", PROTECTED, 0x0004, false), - Arguments.of("package-private", PACKAGE_PRIVATE, 0x0007, true), - Arguments.of("static", STATIC, 0x0008, false), - Arguments.of("instance", INSTANCE, 0x0008, true), - Arguments.of("final", FINAL, 0x0010, false), - Arguments.of("non-final", NON_FINAL, 0x0010, true), - Arguments.of("synchronized", SYNCHRONIZED, 0x0020, false), - Arguments.of("volatile", VOLATILE, 0x0040, false), - Arguments.of("varargs", VARARGS, 0x0080, false), - Arguments.of("transient", TRANSIENT, 0x0080, false), - Arguments.of("interface", INTERFACE, 0x0200, false), - Arguments.of("class", CLASS, 0x0200, true), - Arguments.of("abstract", ABSTRACT, 0x0400, false), - Arguments.of("concrete", CONCRETE, 0x0400, true)); + /** TableTest only calls public converters declared in public classes. */ + public static final class Converters { + @TypeConverter + public static AccessMatcher accessMatcher(String name) throws ReflectiveOperationException { + return (AccessMatcher) AccessMatcher.class.getField(name).get(null); + } } - @ParameterizedTest(name = "{0}") - @MethodSource("flags") - void flagsIgnoreUnrelatedBits(String name, AccessMatcher matcher, int flag, boolean inverted) { + @TableTest({ + "Scenario | Matcher | Flag | Inverted", + "public | PUBLIC | 0x0001 | false ", + "private | PRIVATE | 0x0002 | false ", + "protected | PROTECTED | 0x0004 | false ", + "package-private | PACKAGE_PRIVATE | 0x0007 | true ", + "static | STATIC | 0x0008 | false ", + "instance | INSTANCE | 0x0008 | true ", + "final | FINAL | 0x0010 | false ", + "non-final | NON_FINAL | 0x0010 | true ", + "synchronized | SYNCHRONIZED | 0x0020 | false ", + "volatile | VOLATILE | 0x0040 | false ", + "varargs | VARARGS | 0x0080 | false ", + "transient | TRANSIENT | 0x0080 | false ", + "interface | INTERFACE | 0x0200 | false ", + "class | CLASS | 0x0200 | true ", + "abstract | ABSTRACT | 0x0400 | false ", + "concrete | CONCRETE | 0x0400 | true " + }) + void flagsIgnoreUnrelatedBits(AccessMatcher matcher, int flag, boolean inverted) { // Exercise every combination of the JVM's 16 access bits, including synthetic/bridge bits. for (int access = 0; access <= 0xffff; access++) { boolean expected = ((access & flag) != 0) != inverted; if (matcher.test(access) != expected) { - fail(name + ": expected " + expected + " for access " + access); + fail("expected " + expected + " for access " + access); } } } diff --git a/class-match/src/test/java/datadog/instrument/classmatch/AnnotationRegistrationTest.java b/class-match/src/test/java/datadog/instrument/classmatch/AnnotationRegistrationTest.java index e4c537b..7fc3e30 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/AnnotationRegistrationTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/AnnotationRegistrationTest.java @@ -20,14 +20,16 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; class AnnotationRegistrationTest { @TempDir Path temporary; - @ParameterizedTest - @ValueSource(strings = {"single", "collection"}) + @TableTest({ + "mode ", + "'single' ", + "'collection'" + }) void firstRegistrationWorksInAFreshJvm(String mode) throws Exception { Path output = temporary.resolve("probe-" + mode + ".log"); Process process = diff --git a/class-match/src/test/java/datadog/instrument/classmatch/ClassFileUnicodeTest.java b/class-match/src/test/java/datadog/instrument/classmatch/ClassFileUnicodeTest.java index c77ce23..b3fc8fa 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/ClassFileUnicodeTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/ClassFileUnicodeTest.java @@ -8,13 +8,18 @@ import static org.objectweb.asm.Opcodes.ACC_ABSTRACT; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.objectweb.asm.ClassWriter; +import org.tabletest.junit.TableTest; class ClassFileUnicodeTest { - @ParameterizedTest - @ValueSource(strings = {"Café", "Grüße", "例クラス", "\uD801\uDC00", "Asciié例\uD801\uDC00End"}) + @TableTest({ + "Scenario | Name ", + "two-byte at end | 'Café' ", + "two-byte in middle | 'Grüße' ", + "three-byte only | '例クラス' ", + "surrogate pair only | '\uD801\uDC00' ", + "every encoded width | 'Asciié例\uD801\uDC00End'" + }) void modifiedUtf8NamesRoundTripAtEveryPositionAndAtAnOffset(String name) { String className = "tests/" + name; String descriptor = "L" + className + ";"; diff --git a/class-match/src/test/java/datadog/instrument/classmatch/FieldMatcherTest.java b/class-match/src/test/java/datadog/instrument/classmatch/FieldMatcherTest.java index 59fc442..317cd55 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/FieldMatcherTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/FieldMatcherTest.java @@ -16,12 +16,10 @@ import java.lang.reflect.Modifier; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; class FieldMatcherTest { - @ParameterizedTest - @MethodSource("datadog.instrument.classmatch.MatcherTestSupport#types") + @TableTest(resource = "types.table") void exactTypeOverloads(String name, Class type, String descriptor) { FieldOutline candidate = fieldOutline(descriptor); assertMatches(field("value").type(name), candidate, true); diff --git a/class-match/src/test/java/datadog/instrument/classmatch/GeneratedSignatureTest.java b/class-match/src/test/java/datadog/instrument/classmatch/GeneratedSignatureTest.java index 85ed1ef..0def4fa 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/GeneratedSignatureTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/GeneratedSignatureTest.java @@ -14,10 +14,9 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Type; +import org.tabletest.junit.TableTest; class GeneratedSignatureTest { private static final Class[] TYPES = { @@ -41,8 +40,12 @@ class GeneratedSignatureTest { @SuppressFBWarnings( value = "DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED", justification = "Generated fixtures use an isolated test class loader") - @ParameterizedTest - @ValueSource(ints = {0, 1, 2}) + @TableTest({ + "offset", + "0 ", + "1 ", + "2 " + }) void generatedSignaturesAgreeWithReflection(int offset) throws Exception { ClassWriter writer = classWriter("tests/GeneratedSignatures"); Class[] returns = Arrays.copyOf(TYPES, TYPES.length + 1); diff --git a/class-match/src/test/java/datadog/instrument/classmatch/MalformedClassFileTest.java b/class-match/src/test/java/datadog/instrument/classmatch/MalformedClassFileTest.java index 3327441..56a9852 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/MalformedClassFileTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/MalformedClassFileTest.java @@ -7,28 +7,21 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.Arrays; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; import org.objectweb.asm.Attribute; import org.objectweb.asm.ByteVector; import org.objectweb.asm.ClassWriter; +import org.tabletest.junit.TableTest; class MalformedClassFileTest { - static Stream incompleteDescriptors() { - return Stream.of( - Arguments.of("(I", new int[0]), - Arguments.of("(Ljava/lang/String", new int[0]), - Arguments.of("([", new int[0]), - Arguments.of("(ILjava/lang/String", new int[] {2}), - Arguments.of("(II[", new int[] {2, 3})); - } - - @ParameterizedTest - @MethodSource("incompleteDescriptors") + @TableTest({ + "Descriptor | Boundaries", + "'(I' | [] ", + "'(Ljava/lang/String' | [] ", + "'([' | [] ", + "'(ILjava/lang/String' | [2] ", + "'(II[' | [2, 3] " + }) void incompleteDescriptorsRetainOnlyPreviouslyParsedBoundaries( String descriptor, int[] boundaries) { MethodOutline method = methodOutline(descriptor); @@ -38,8 +31,13 @@ void incompleteDescriptorsRetainOnlyPreviouslyParsedBoundaries( } } - @ParameterizedTest - @ValueSource(ints = {0, 2, 13, 99}) + @TableTest({ + "Scenario | Tag", + "never assigned | 0 ", + "unassigned gap after Utf8 | 2 ", + "unassigned gap after NameAndType | 13 ", + "beyond highest defined tag | 99 " + }) void invalidConstantPoolTagsAreRejected(int tag) { byte[] bytes = classWriter("tests/BadConstantPool").toByteArray(); bytes[10] = (byte) tag; // first constant pool entry follows magic/version/count diff --git a/class-match/src/test/java/datadog/instrument/classmatch/MatcherTestSupport.java b/class-match/src/test/java/datadog/instrument/classmatch/MatcherTestSupport.java index 04eeedb..9cecd4c 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/MatcherTestSupport.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/MatcherTestSupport.java @@ -7,29 +7,11 @@ import java.io.InputStream; import java.io.UncheckedIOException; import java.util.function.Predicate; -import java.util.stream.Stream; -import org.junit.jupiter.params.provider.Arguments; /** Small fixtures with explicit JVM descriptors so expectations don't use production conversion. */ final class MatcherTestSupport { private MatcherTestSupport() {} - static Stream types() { - return Stream.of( - Arguments.of("boolean", boolean.class, "Z"), - Arguments.of("byte", byte.class, "B"), - Arguments.of("char", char.class, "C"), - Arguments.of("short", short.class, "S"), - Arguments.of("int", int.class, "I"), - Arguments.of("long", long.class, "J"), - Arguments.of("float", float.class, "F"), - Arguments.of("double", double.class, "D"), - Arguments.of("java.lang.String", String.class, "Ljava/lang/String;"), - Arguments.of("java/lang/String", String.class, "Ljava/lang/String;"), - Arguments.of("int[][]", int[][].class, "[[I"), - Arguments.of("java.lang.String[][]", String[][].class, "[[Ljava/lang/String;")); - } - static FieldOutline fieldOutline(String descriptor) { return fieldOutline(0, "value", descriptor); } diff --git a/class-match/src/test/java/datadog/instrument/classmatch/MethodMatcherTest.java b/class-match/src/test/java/datadog/instrument/classmatch/MethodMatcherTest.java index ad5835a..8f83563 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/MethodMatcherTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/MethodMatcherTest.java @@ -19,13 +19,10 @@ import java.lang.reflect.Modifier; import java.util.Collections; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; class MethodMatcherTest { - @ParameterizedTest - @MethodSource("datadog.instrument.classmatch.MatcherTestSupport#types") + @TableTest(resource = "types.table") void exactSignatureOverloads(String name, Class type, String descriptor) { MethodOutline candidate = methodOutline("(" + descriptor + descriptor + ")" + descriptor); assertMatches(method("execute").parameters(name, name).returning(name), candidate, true); @@ -43,8 +40,7 @@ void exactSignatureOverloads(String name, Class type, String descriptor) { assertMatches(method().noParameters(), candidate, false); } - @ParameterizedTest - @MethodSource("datadog.instrument.classmatch.MatcherTestSupport#types") + @TableTest(resource = "types.table") void exactReturnTypesDistinguishArraysFromTheirComponents( String name, Class type, String descriptor) { MethodOutline scalar = methodOutline("()" + descriptor); @@ -121,9 +117,15 @@ void objectParameterPredicatesAndReturnPredicates() { assertMatches(method().returning(type("java.lang.Object")), candidate, false); } - @ParameterizedTest - @ValueSource( - strings = {"()V", "()I", "()[I", "()[[Ljava/lang/String;", "(I)V", "([Ljava/lang/String;)V"}) + @TableTest({ + "descriptor ", + "'()V' ", + "'()I' ", + "'()[I' ", + "'()[[Ljava/lang/String;'", + "'(I)V' ", + "'([Ljava/lang/String;)V'" + }) void unsupportedOrAbsentTypesDoNotInvokeTypePredicates(String descriptor) { TypeMatcher unexpected = t -> { diff --git a/class-match/src/test/java/datadog/instrument/classmatch/MethodOutlineTest.java b/class-match/src/test/java/datadog/instrument/classmatch/MethodOutlineTest.java index 9e8718b..63fe6fa 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/MethodOutlineTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/MethodOutlineTest.java @@ -3,12 +3,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; class MethodOutlineTest { - @ParameterizedTest - @ValueSource(strings = {"(LA;)V", "(ILA;)V", "(LA;LA;)V"}) + @TableTest({ + "descriptor ", + "'(LA;)V' ", + "'(ILA;)V' ", + "'(LA;LA;)V'" + }) void lastObjectParameterExcludesDescriptorDelimiters(String descriptor) { MethodOutline method = new MethodOutline(0, "example", descriptor, new String[0]); int index = method.parameterCount() - 1; @@ -19,8 +22,11 @@ void lastObjectParameterExcludesDescriptorDelimiters(String descriptor) { assertTrue(MethodMatcher.method().parameter(index, TypeMatcher.type("A")).test(method)); } - @ParameterizedTest - @ValueSource(strings = {"(LA;I)V", "(LA;LA;)V"}) + @TableTest({ + "descriptor ", + "'(LA;I)V' ", + "'(LA;LA;)V'" + }) void earlierObjectParameterExcludesDescriptorDelimiters(String descriptor) { MethodOutline method = new MethodOutline(0, "example", descriptor, new String[0]); diff --git a/class-match/src/test/java/datadog/instrument/classmatch/OutlineTypeViewsTest.java b/class-match/src/test/java/datadog/instrument/classmatch/OutlineTypeViewsTest.java index 5a729c1..e0299c2 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/OutlineTypeViewsTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/OutlineTypeViewsTest.java @@ -10,13 +10,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; /** Descriptor views beyond the boundary regressions owned by PR #144. */ class OutlineTypeViewsTest { - @ParameterizedTest - @ValueSource(strings = {"A", "java/lang/String", "sample/Outer$Inner", "例/型"}) + @TableTest({ + "name ", + "'A' ", + "'java/lang/String' ", + "'sample/Outer$Inner'", + "'例/型' " + }) void fieldParameterAndReturnViewsHaveStringContentAndHashes(String name) { FieldOutline field = fieldOutline("L" + name + ";"); // Keep an additional primitive parameter: final object parameter coverage is in PR #144. @@ -32,8 +36,19 @@ void fieldParameterAndReturnViewsHaveStringContentAndHashes(String name) { } } - @ParameterizedTest - @ValueSource(strings = {"Z", "B", "C", "D", "F", "I", "J", "S", "[I", "[[Ljava/lang/String;"}) + @TableTest({ + "descriptor ", + "'Z' ", + "'B' ", + "'C' ", + "'D' ", + "'F' ", + "'I' ", + "'J' ", + "'S' ", + "'[I' ", + "'[[Ljava/lang/String;'" + }) void primitiveAndArrayDescriptorsHaveNoObjectViews(String descriptor) { FieldOutline field = fieldOutline(descriptor); MethodOutline method = methodOutline("(" + descriptor + ")" + descriptor); @@ -46,8 +61,12 @@ void primitiveAndArrayDescriptorsHaveNoObjectViews(String descriptor) { } } - @ParameterizedTest - @ValueSource(strings = {"()V", "(I)V", "(Ljava/lang/String;I)V"}) + @TableTest({ + "descriptor ", + "'()V' ", + "'(I)V' ", + "'(Ljava/lang/String;I)V'" + }) void voidReturnsAndMissingParametersHaveNoViews(String descriptor) { MethodOutline method = methodOutline(descriptor); assertNull(method.returnTypeString()); diff --git a/class-match/src/test/java/datadog/instrument/classmatch/TypeMatcherTest.java b/class-match/src/test/java/datadog/instrument/classmatch/TypeMatcherTest.java index f71e941..2346dac 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/TypeMatcherTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/TypeMatcherTest.java @@ -10,25 +10,24 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; class TypeMatcherTest { - static Stream names() { - return Stream.of("java/lang/String", "sample/Outer$Inner", "例/型", "A", "") - .flatMap(name -> Stream.of(Arguments.of(name, name), Arguments.of(name, typeString(name)))); - } - private static TypeString typeString(String name) { return new TypeString("L" + name + ";", 1, name.length() + 1, name.hashCode()); } - @ParameterizedTest - @MethodSource("names") - void exactNamesNormalizeFactoriesAndAcceptCharSequences(String name, CharSequence candidate) { + @TableTest({ + "Name | Type string ", + "'java/lang/String' | {false, true}", + "'sample/Outer$Inner' | {false, true}", + "'例/型' | {false, true}", + "'A' | {false, true}", + "'' | {false, true}" + }) + void exactNamesNormalizeFactoriesAndAcceptCharSequences(String name, boolean asTypeString) { + CharSequence candidate = asTypeString ? typeString(name) : name; assertMatches(type(name), candidate, true); assertMatches(type(name.replace('/', '.')), candidate, true); assertMatches(type("missing/Type"), candidate, false); diff --git a/class-match/src/test/java/datadog/instrument/classmatch/TypeStringTest.java b/class-match/src/test/java/datadog/instrument/classmatch/TypeStringTest.java index bccfa1e..fc8ea38 100644 --- a/class-match/src/test/java/datadog/instrument/classmatch/TypeStringTest.java +++ b/class-match/src/test/java/datadog/instrument/classmatch/TypeStringTest.java @@ -2,13 +2,35 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.stream.Stream; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.tabletest.junit.TableTest; +import org.tabletest.junit.TypeConverter; +import org.tabletest.junit.TypeConverterSources; +@TypeConverterSources(TypeStringTest.Converters.class) class TypeStringTest { - @ParameterizedTest - @MethodSource("typeStrings") + /** TableTest only calls public converters declared in public classes. */ + public static final class Converters { + /** + * Field descriptors give the field type; method descriptors their first parameter, or return. + */ + @TypeConverter + public static TypeString typeString(String descriptor) { + if (descriptor.charAt(0) != '(') { + return new FieldOutline(0, "example", descriptor).typeString(); + } + MethodOutline method = new MethodOutline(0, "example", descriptor, new String[0]); + return method.parameterCount() > 0 + ? method.parameterTypeString(0) + : method.returnTypeString(); + } + } + + @TableTest({ + "Scenario | Type string", + "field | 'LA;' ", + "parameter | '(LA;I)V' ", + "return | '()LA;' " + }) void prefixAndSuffixMatchingStayWithinTypeString(TypeString type) { String name = type.toString(); for (String candidate : new String[] {"", "A", "L", "LA", "A;", "A;I", "()LA", "Longer"}) { @@ -22,11 +44,4 @@ void prefixAndSuffixMatchingStayWithinTypeString(TypeString type) { "suffix: " + candidate); } } - - static Stream typeStrings() { - return Stream.of( - new FieldOutline(0, "example", "LA;").typeString(), - new MethodOutline(0, "example", "(LA;I)V", new String[0]).parameterTypeString(0), - new MethodOutline(0, "example", "()LA;", new String[0]).returnTypeString()); - } } diff --git a/class-match/src/test/resources/datadog/instrument/classmatch/types.table b/class-match/src/test/resources/datadog/instrument/classmatch/types.table new file mode 100644 index 0000000..94b15cd --- /dev/null +++ b/class-match/src/test/resources/datadog/instrument/classmatch/types.table @@ -0,0 +1,13 @@ +Name | Class | Descriptor +'boolean' | 'boolean' | 'Z' +'byte' | 'byte' | 'B' +'char' | 'char' | 'C' +'short' | 'short' | 'S' +'int' | 'int' | 'I' +'long' | 'long' | 'J' +'float' | 'float' | 'F' +'double' | 'double' | 'D' +'java.lang.String' | 'java.lang.String' | 'Ljava/lang/String;' +'java/lang/String' | 'java.lang.String' | 'Ljava/lang/String;' +'int[][]' | 'int[][]' | '[[I' +'java.lang.String[][]' | 'java.lang.String[][]' | '[[Ljava/lang/String;' diff --git a/field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreDispatchFunctionalTest.java b/field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreDispatchFunctionalTest.java index f5cbd20..9268190 100644 --- a/field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreDispatchFunctionalTest.java +++ b/field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreDispatchFunctionalTest.java @@ -24,8 +24,7 @@ import java.util.function.Function; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; /** * Loads and actually invokes the generated {@code ObjectStoreDispatch} bytecode, to check its @@ -217,8 +216,11 @@ void injectedAccessorFallsBackViaWeakGetPutForStoresItDoesNotHave() throws Excep assertThat(GlobalObjectStore.get(key, injectedId)).isNull(); } - @ParameterizedTest - @ValueSource(strings = {"getOrPut", "getOrCompute"}) + @TableTest({ + "operation ", + "'getOrPut' ", + "'getOrCompute'" + }) void concurrentInsertionOnSameKeyConvergesToASingleWinner(String operation) throws Exception { int storeId = objectStoreId("test.Dispatch.Concurrent.Key", "test.Dispatch.Concurrent.Value"); Map backing = Collections.synchronizedMap(new HashMap<>()); @@ -289,8 +291,12 @@ void concurrentInsertionOnSameKeyConvergesToASingleWinner(String operation) thro .allMatch(winner::equals); } - @ParameterizedTest - @ValueSource(strings = {"compute", "get", "put"}) + @TableTest({ + "failurePoint", + "'compute' ", + "'get' ", + "'put' " + }) void failureInsideMonitorReleasesLockAndAllowsRetry(String failurePoint) throws Exception { int storeId = objectStoreId("test.Dispatch.Failure.Key", "test.Dispatch.Failure.Value"); Map backing = Collections.synchronizedMap(new HashMap<>()); diff --git a/field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreTest.java b/field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreTest.java index e96ee1b..bdfd17a 100644 --- a/field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreTest.java +++ b/field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreTest.java @@ -26,8 +26,7 @@ import java.util.function.BooleanSupplier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; class ObjectStoreTest { @@ -115,8 +114,11 @@ void getOrPutNullDoesNotInsertOrReplaceValue() { assertEquals("first", store.get(key)); } - @ParameterizedTest - @ValueSource(strings = {"getOrPut", "getOrCompute"}) + @TableTest({ + "operation ", + "'getOrPut' ", + "'getOrCompute'" + }) void concurrentInsertionOnSameKeyReturnsSingleWinner(String operation) throws Exception { Object key = new Object(); int threadCount = 8; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc46337..984296f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,6 +2,7 @@ asm = "9.11.1" junit-jupiter = "5.14.4" junit-platform = "1.14.4" +tabletest = "1.2.2" assertj = "3.27.7" spotless = "8.10.4" spotbugs = "6.5.13" @@ -17,6 +18,7 @@ asm-util = { module = "org.ow2.asm:asm-util", version.ref = "asm" } asm-test = { module = "org.ow2.asm:asm-test", version.ref = "asm" } junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" } junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-platform" } +tabletest = { module = "org.tabletest:tabletest-junit", version.ref = "tabletest" } assertj-core = { module = "org.assertj:assertj-core", version.ref = "assertj" } spotless = { module = "com.diffplug.spotless:spotless-plugin-gradle", version.ref = "spotless" } spotbugs = { module = "com.github.spotbugs.snom:spotbugs-gradle-plugin", version.ref = "spotbugs" } diff --git a/utils/src/test/java/datadog/instrument/utils/ClassLoaderValueTest.java b/utils/src/test/java/datadog/instrument/utils/ClassLoaderValueTest.java index f8ac3fa..bc2ba52 100644 --- a/utils/src/test/java/datadog/instrument/utils/ClassLoaderValueTest.java +++ b/utils/src/test/java/datadog/instrument/utils/ClassLoaderValueTest.java @@ -16,8 +16,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; @SuppressFBWarnings("DM_GC") @SuppressWarnings({"UnusedAssignment", "ConstantValue"}) @@ -167,8 +166,11 @@ void removeStaleEntries() { visited.clear(); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) + @TableTest({ + "systemLoader", + "false ", + "true " + }) void competingComputationsReturnTheInstalledValue(boolean systemLoader) throws Exception { ClassLoader loader = systemLoader ? ClassLoader.getSystemClassLoader() : null; int threadCount = 8; diff --git a/utils/src/test/java/datadog/instrument/utils/ClassNameFilterTest.java b/utils/src/test/java/datadog/instrument/utils/ClassNameFilterTest.java index 44e203a..9a27f61 100644 --- a/utils/src/test/java/datadog/instrument/utils/ClassNameFilterTest.java +++ b/utils/src/test/java/datadog/instrument/utils/ClassNameFilterTest.java @@ -17,14 +17,16 @@ import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; class ClassNameFilterTest { - @ParameterizedTest - @ValueSource(strings = {"example.例", "example.例Class", "example.My例"}) + @TableTest({ + "className ", + "'example.例' ", + "'example.例Class'", + "'example.My例' " + }) void unicodeClassNames(String className) { ClassNameFilter filter = new ClassNameFilter(256); @@ -33,8 +35,13 @@ void unicodeClassNames(String className) { assertTrue(filter.contains(className)); } - @ParameterizedTest - @ValueSource(ints = {256, 257, 512, 513}) + @TableTest({ + "Scenario | Length", + "longest exactly encoded length | 256 ", + "encoded length wraps to 0 | 257 ", + "encoded length wraps to 255 | 512 ", + "encoded length wraps to 0 twice | 513 " + }) void longSimpleNames(int length) { char[] simpleName = new char[length]; Arrays.fill(simpleName, 'A'); @@ -78,11 +85,11 @@ void basicOperation() { assertFalse(filter.contains("example.test.NotMyClass")); } - @ParameterizedTest - @CsvSource({ - "example.GAnxX, example.ZvaQyFGz", // different simple-name lengths - "example.test.FB, example.test.Ea", // different first characters - "example.AaAa, example.AaBB" // same first character, different last characters + @TableTest({ + "Scenario | Stored | Collision ", + "different simple-name lengths | example.GAnxX | example.ZvaQyFGz", + "different first characters | example.test.FB | example.test.Ea ", + "same first character, different last characters | example.AaAa | example.AaBB " }) void uniqueClassCodes(String stored, String collision) { ClassNameFilter filter = new ClassNameFilter(8); diff --git a/utils/src/test/java/datadog/instrument/utils/ClassNameTrieTest.java b/utils/src/test/java/datadog/instrument/utils/ClassNameTrieTest.java index 45865a6..afc02ed 100644 --- a/utils/src/test/java/datadog/instrument/utils/ClassNameTrieTest.java +++ b/utils/src/test/java/datadog/instrument/utils/ClassNameTrieTest.java @@ -39,10 +39,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; +import org.tabletest.junit.TableTest; @SuppressFBWarnings("NP_NONNULL_PARAM_VIOLATION") class ClassNameTrieTest { @@ -56,85 +53,79 @@ static void buildTestClassNamesTrie() throws IOException { testClassNamesTrie = builder.buildTrie(); } - @ParameterizedTest - @MethodSource + @TableTest({ + "Class name | Value", + "'One' | 1 ", + "'com.Two' | 2 ", + "'com.foo.Three' | 3 ", + "'company.foo.Four' | 4 ", + "'com.foobar.Five' | 5 ", + "'company.foobar.Six' | 6 ", + "'company.foobar.Sixty' | 60 ", + "'com.f' | 7 ", + "'com.foo.a' | 8 ", + "'com.foobar.b' | 9 ", + "'company.f' | 10 ", + "'company.foo.a' | 11 ", + "'company.foobar.S' | 12 ", + "'com.Two$f' | 13 ", + "'foobar.Two$b' | 14 ", + "'' | -1 ", + "'O' | -1 ", + "'_' | -1 ", + "'On' | -1 ", + "'O_' | -1 ", + "'On_' | -1 ", + "'OneNoMatch' | -1 ", + "'com.Twos' | 7 ", + "'com.foo.Threes' | 8 ", + "'com.foobar.Fives' | 9 ", + "'foobar.Thre' | -1 ", + "'foobar.Three' | 15 ", + "'foobar.ThreeMore' | 15 " + }) void classNameMapping(String name, int value) { assertEquals(value, testClassNamesTrie.apply(name)); String internalName = name.replace('.', '/'); assertEquals(value, testClassNamesTrie.apply(internalName)); } - static Stream classNameMapping() { - return Stream.of( - Arguments.of("One", 1), - Arguments.of("com.Two", 2), - Arguments.of("com.foo.Three", 3), - Arguments.of("company.foo.Four", 4), - Arguments.of("com.foobar.Five", 5), - Arguments.of("company.foobar.Six", 6), - Arguments.of("company.foobar.Sixty", 60), - Arguments.of("com.f", 7), - Arguments.of("com.foo.a", 8), - Arguments.of("com.foobar.b", 9), - Arguments.of("company.f", 10), - Arguments.of("company.foo.a", 11), - Arguments.of("company.foobar.S", 12), - Arguments.of("com.Two$f", 13), - Arguments.of("foobar.Two$b", 14), - Arguments.of("", -1), - Arguments.of("O", -1), - Arguments.of("_", -1), - Arguments.of("On", -1), - Arguments.of("O_", -1), - Arguments.of("On_", -1), - Arguments.of("OneNoMatch", -1), - Arguments.of("com.Twos", 7), - Arguments.of("com.foo.Threes", 8), - Arguments.of("com.foobar.Fives", 9), - Arguments.of("foobar.Thre", -1), - Arguments.of("foobar.Three", 15), - Arguments.of("foobar.ThreeMore", 15)); - } - - @ParameterizedTest - @MethodSource + @TableTest({ + "Class name | Value", + "'garbage.One' | 1 ", + "'garbage.com.Two' | 2 ", + "'garbage.com.foo.Three' | 3 ", + "'garbage.company.foo.Four' | 4 ", + "'garbage.com.foobar.Five' | 5 ", + "'garbage.company.foobar.Six' | 6 ", + "'garbage.company.foobar.Sixty' | 60 ", + "'garbage.com.f' | 7 ", + "'garbage.com.foo.a' | 8 ", + "'garbage.com.foobar.b' | 9 ", + "'garbage.company.f' | 10 ", + "'garbage.company.foo.a' | 11 ", + "'garbage.company.foobar.S' | 12 ", + "'garbage.com.Two$f' | 13 ", + "'garbage.foobar.Two$b' | 14 ", + "'garbage.' | -1 ", + "'garbage.O' | -1 ", + "'garbage._' | -1 ", + "'garbage.On' | -1 ", + "'garbage.O_' | -1 ", + "'garbage.On_' | -1 ", + "'garbage.OneNoMatch' | -1 ", + "'garbage.com.Twos' | 7 ", + "'garbage.com.foo.Threes' | 8 ", + "'garbage.com.foobar.Fives' | 9 ", + "'garbage.foobar.Thre' | -1 ", + "'garbage.foobar.Three' | 15 ", + "'garbage.foobar.ThreeMore' | 15 " + }) void classNameMappingFromIndex(String name, int value) { int from = "garbage.".length(); assertEquals(value, testClassNamesTrie.apply(name, from)); } - static Stream classNameMappingFromIndex() { - return Stream.of( - Arguments.of("garbage.One", 1), - Arguments.of("garbage.com.Two", 2), - Arguments.of("garbage.com.foo.Three", 3), - Arguments.of("garbage.company.foo.Four", 4), - Arguments.of("garbage.com.foobar.Five", 5), - Arguments.of("garbage.company.foobar.Six", 6), - Arguments.of("garbage.company.foobar.Sixty", 60), - Arguments.of("garbage.com.f", 7), - Arguments.of("garbage.com.foo.a", 8), - Arguments.of("garbage.com.foobar.b", 9), - Arguments.of("garbage.company.f", 10), - Arguments.of("garbage.company.foo.a", 11), - Arguments.of("garbage.company.foobar.S", 12), - Arguments.of("garbage.com.Two$f", 13), - Arguments.of("garbage.foobar.Two$b", 14), - Arguments.of("garbage.", -1), - Arguments.of("garbage.O", -1), - Arguments.of("garbage._", -1), - Arguments.of("garbage.On", -1), - Arguments.of("garbage.O_", -1), - Arguments.of("garbage.On_", -1), - Arguments.of("garbage.OneNoMatch", -1), - Arguments.of("garbage.com.Twos", 7), - Arguments.of("garbage.com.foo.Threes", 8), - Arguments.of("garbage.com.foobar.Fives", 9), - Arguments.of("garbage.foobar.Thre", -1), - Arguments.of("garbage.foobar.Three", 15), - Arguments.of("garbage.foobar.ThreeMore", 15)); - } - @Test void generateJavaSource(@TempDir File classesDir) throws Exception { ClassNameTrie.JavaGenerator.main( @@ -303,8 +294,12 @@ void tooManyLongJumps() { IllegalArgumentException.class, () -> builder.put("" + ((char) 67) + ((char) 2), 1)); } - @ParameterizedTest - @ValueSource(chars = {'\u7FFF', '\u8000', '\uFFFF'}) + @TableTest({ + "Scenario | Next character", + "all bits except leaf marker | '\u7FFF' ", + "leaf marker bit only | '\u8000' ", + "all bits set | '\uFFFF' " + }) void extendingLeafPreservesFollowingUnicodeSegment(char nextCharacter) { for (String suffix : new String[] {"x", "xyz"}) { ClassNameTrie.Builder builder = new ClassNameTrie.Builder(); @@ -329,8 +324,11 @@ void extendingLeafPreservesFollowingUnicodeSegment(char nextCharacter) { } } - @ParameterizedTest - @ValueSource(booleans = {false, true}) + @TableTest({ + "Scenario | New builder", + "keep updating builder | false ", + "new builder from trie | true " + }) void builtTriePreservesValuesAfterBuilderUpdates(boolean newBuilder) { ClassNameTrie.Builder builder = new ClassNameTrie.Builder(); builder.put("a", 1); @@ -352,8 +350,11 @@ void builtTriePreservesValuesAfterBuilderUpdates(boolean newBuilder) { assertEquals(4, builder.apply("ab")); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) + @TableTest({ + "Scenario | New builder", + "keep updating builder | false ", + "new builder from trie | true " + }) void builtTriePreservesLongJumpsAfterBuilderUpdates(boolean newBuilder) { ClassNameTrie.Builder builder = new ClassNameTrie.Builder(); String padding = padding(7700); @@ -386,8 +387,11 @@ void builtTriePreservesLongJumpsAfterBuilderUpdates(boolean newBuilder) { assertEquals(4, builder.apply(extension + "x")); } - @ParameterizedTest - @ValueSource(booleans = {false, true}) + @TableTest({ + "Scenario | New builder", + "keep updating builder | false ", + "new builder from trie | true " + }) void growsLongJumpTableAfterCompaction(boolean newBuilder) { ClassNameTrie.Builder builder = new ClassNameTrie.Builder(); String padding = padding(7700); @@ -451,9 +455,18 @@ private static ClassNameTrie.Builder builderAtLongJumpLimit(String padding) { return builder; } - @ParameterizedTest - @MethodSource("insertionsGrowingAncestorJump") - void failedAncestorJumpConversionPreservesTrie(String key) throws IOException { + @TableTest({ + "Scenario | Prefix | Padding length | Suffix", + "extend a leaf segment | aa | 7675 | x ", + "insert a bud within a segment | aa | 100 | '' ", + "prepend a node to a segment | aa | 0 | '' ", + "split a segment on the left | aao | 0 | '' ", + "split a segment on the right | aaq | 0 | '' ", + "add a branch | az | 0 | '' " + }) + void failedAncestorJumpConversionPreservesTrie(String prefix, int paddingLength, String suffix) + throws IOException { + String key = prefix + padding(paddingLength) + suffix; ClassNameTrie.Builder builder = builderWithShortJumpNearLimit(0); byte[] before = serialize(builder); @@ -466,16 +479,6 @@ void failedAncestorJumpConversionPreservesTrie(String key) throws IOException { assertEquals(1, builder.apply("z")); } - static Stream insertionsGrowingAncestorJump() { - return Stream.of( - "aa" + padding(7675) + "x", // extend a leaf segment - "aa" + padding(100), // insert a bud within a segment - "aa", // prepend a node to a segment - "aao", // split a segment on the left - "aaq", // split a segment on the right - "az"); // add a branch - } - @Test void fullLongJumpTableAllowsOverwritesAndExistingJumpUpdates() { ClassNameTrie.Builder builder = builderWithShortJumpNearLimit(1);