diff --git a/dd-trace-api/src/test/groovy/datadog/trace/api/DDSpanIdTest.groovy b/dd-trace-api/src/test/groovy/datadog/trace/api/DDSpanIdTest.groovy deleted file mode 100644 index 12b07cc1b46..00000000000 --- a/dd-trace-api/src/test/groovy/datadog/trace/api/DDSpanIdTest.groovy +++ /dev/null @@ -1,134 +0,0 @@ -package datadog.trace.api - -import datadog.trace.test.util.DDSpecification - -class DDSpanIdTest extends DDSpecification { - - def "convert ids from/to String #stringId"() { - when: - final ddid = DDSpanId.from(stringId) - - then: - ddid == expectedId - DDSpanId.toString(ddid) == stringId - - where: - stringId | expectedId - "0" | 0 - "1" | 1 - "18446744073709551615" | DDSpanId.MAX - "${Long.MAX_VALUE}" | Long.MAX_VALUE - "${BigInteger.valueOf(Long.MAX_VALUE).plus(1)}" | Long.MIN_VALUE - } - - def "fail on illegal String"() { - when: - DDSpanId.from(stringId) - - then: - thrown NumberFormatException - - where: - stringId << [ - null, - "", - "-1", - "18446744073709551616", - "18446744073709551625", - "184467440737095516150", - "18446744073709551a1", - "184467440737095511a" - ] - } - - def "convert ids from/to hex String"() { - when: - final ddid = DDSpanId.fromHex(hexId) - final padded16 = hexId.length() <= 16 ? - ("0" * 16).substring(0, 16 - hexId.length()) + hexId : - hexId.substring(hexId.length() - 16, hexId.length()) - - then: - ddid == expectedId - if (hexId.length() > 1) { - hexId = hexId.replaceAll("^0+", "") // drop leading zeros - } - DDSpanId.toHexString(ddid) == hexId - DDSpanId.toHexStringPadded(ddid) == padded16 - - where: - hexId | expectedId - "0" | 0 - "1" | 1 - "f" * 16 | DDSpanId.MAX - "7" + "f" * 15 | Long.MAX_VALUE - "8" + "0" * 15 | Long.MIN_VALUE - "0" * 4 + "8" + "0" * 15 | Long.MIN_VALUE - "cafebabe" | 3405691582 - "123456789abcdef" | 81985529216486895 - } - - def "convert ids from part of hex String"() { - when: - Long ddid = null - try { - ddid = DDSpanId.fromHex(hexId, start, length, lcOnly) - } catch (NumberFormatException ignored) { - } - - then: - if (expectedId) { - assert ddid == expectedId - } else { - assert !ddid - } - - where: - hexId | start| length | lcOnly | expectedId - null | 1 | 1 | false | null - "" | 1 | 1 | false | null - "00" | -1 | 1 | false | null - "00" | 0 | 0 | false | null - "00" | 0 | 1 | false | DDSpanId.ZERO - "00" | 1 | 1 | false | DDSpanId.ZERO - "00" | 1 | 1 | false | DDSpanId.ZERO - "f" * 16 | 0 | 16 | true | DDSpanId.MAX - "f" * 12 + "Ffff"| 0 | 16 | true | null - "f" * 12 + "Ffff"| 0 | 16 | false | DDSpanId.MAX - } - - def "fail on illegal hex String"() { - when: - DDSpanId.fromHex(hexId) - - then: - thrown NumberFormatException - - where: - hexId << [ - null, - "", - "-1", - "1" + "0" * 16, - "f" * 14 + "zf", - "f" * 15 + "z" - ] - } - - def "generate id with #strategyName"() { - when: - def strategy = IdGenerationStrategy.fromName(strategyName) - def spanIds = (0..32768).collect { strategy.generateSpanId() } - Set checked = new HashSet<>() - - then: - spanIds.forEach { spanId -> - assert spanId != 0 - assert !checked.contains(spanId) - checked.add(spanId) - } - - where: - strategyName << ["RANDOM", "SEQUENTIAL", "SECURE_RANDOM"] - } -} diff --git a/dd-trace-api/src/test/groovy/datadog/trace/api/DDTraceIdTest.groovy b/dd-trace-api/src/test/groovy/datadog/trace/api/DDTraceIdTest.groovy deleted file mode 100644 index f8d1d70ae17..00000000000 --- a/dd-trace-api/src/test/groovy/datadog/trace/api/DDTraceIdTest.groovy +++ /dev/null @@ -1,202 +0,0 @@ -package datadog.trace.api - -import datadog.trace.test.util.DDSpecification - -class DDTraceIdTest extends DDSpecification { - def "convert 64-bit ids from/to long #longId and check strings"() { - when: - final ddid = DD64bTraceId.from(longId) - final defaultDdid = DDTraceId.from(longId) - - then: - ddid == expectedId - ddid == defaultDdid - ddid.toLong() == longId - ddid.toHighOrderLong() == 0L - ddid.toString() == expectedString - ddid.toHexString() == expectedHex - - where: - longId | expectedId | expectedString | expectedHex - 0 | DD64bTraceId.ZERO | "0" | "0" * 32 - 1 | DD64bTraceId.ONE | "1" | "0" * 31 + "1" - -1 | DD64bTraceId.MAX | "18446744073709551615" | "0" * 16 + "f" * 16 - Long.MAX_VALUE | DD64bTraceId.from(Long.MAX_VALUE) | "9223372036854775807" | "0" * 16 + "7" + "f" * 15 - Long.MIN_VALUE | DD64bTraceId.from(Long.MIN_VALUE) | "9223372036854775808" | "0" * 16 + "8" + "0" * 15 - } - - def "convert 64-bit ids from/to String representation: #stringId"() { - when: - final ddid = DD64bTraceId.from(stringId) - - then: - ddid == expectedId - ddid.toString() == stringId - - where: - stringId | expectedId - "0" | DD64bTraceId.ZERO - "1" | DD64bTraceId.ONE - "18446744073709551615" | DD64bTraceId.MAX - "${Long.MAX_VALUE}" | DD64bTraceId.from(Long.MAX_VALUE) - "${BigInteger.valueOf(Long.MAX_VALUE).plus(1)}" | DD64bTraceId.from(Long.MIN_VALUE) - } - - def "fail parsing illegal 64-bit id String representation: #stringId"() { - when: - DD64bTraceId.from(stringId) - - then: - thrown NumberFormatException - - where: - stringId << [ - null, - "", - "-1", - "18446744073709551616", - "18446744073709551625", - "184467440737095516150", - "18446744073709551a1", - "184467440737095511a" - ] - } - - def "convert 64-bit ids from/to hex String representation: #hexId"() { - when: - final ddid = DD64bTraceId.fromHex(hexId) - final padded16 = hexId.length() <= 16 ? - ("0" * 16).substring(0, 16 - hexId.length()) + hexId : - hexId.substring(hexId.length() - 16, hexId.length()) - final padded32 = ("0" * 32).substring(0, 32 - hexId.length()) + hexId - - then: - ddid == expectedId - ddid.toHexString() == padded32 - ddid.toHexStringPadded(16) == padded16 - ddid.toHexStringPadded(32) == padded32 - - where: - hexId | expectedId - "0" | DD64bTraceId.ZERO - "1" | DD64bTraceId.ONE - "f" * 16 | DD64bTraceId.MAX - "7" + "f" * 15 | DD64bTraceId.from(Long.MAX_VALUE) - "8" + "0" * 15 | DD64bTraceId.from(Long.MIN_VALUE) - "0" * 4 + "8" + "0" * 15 | DD64bTraceId.from(Long.MIN_VALUE) - "cafebabe" | DD64bTraceId.from(3405691582) - "123456789abcdef" | DD64bTraceId.from(81985529216486895) - } - - def "fail parsing illegal 64-bit hexadecimal String representation: #hexId"() { - when: - DD64bTraceId.fromHex(hexId) - - then: - thrown NumberFormatException - - where: - hexId << [ - null, - "", - "-1", - "1" + "0" * 16, - "f" * 14 + "zf", - "f" * 15 + "z" - ] - } - - def "convert 128-bit ids from/to hexadecimal String representation #hexId"() { - when: - def parsedId = DD128bTraceId.fromHex(hexId) - def id = DD128bTraceId.from(high, low) - def paddedHexId = hexId.padLeft(32, '0') - - then: - id == parsedId - parsedId.toHexString() == paddedHexId - parsedId.toHexStringPadded(16) == paddedHexId.substring(16, 32) - parsedId.toHexStringPadded(32) == paddedHexId - parsedId.toLong() == low - parsedId.toHighOrderLong() == high - parsedId.toString() == Long.toUnsignedString(low) - - where: - high | low | hexId - Long.MIN_VALUE | Long.MIN_VALUE | "8" + "0" * 15 + "8" + "0" * 15 - Long.MIN_VALUE | 1L | "8" + "0" * 15 + "0" * 15 + "1" - Long.MIN_VALUE | Long.MAX_VALUE | "8" + "0" * 15 + "7" + "f" * 15 - 1L | Long.MIN_VALUE | "0" * 15 + "1" + "8" + "0" * 15 - 1L | 1L | "0" * 15 + "1" + "0" * 15 + "1" - 1L | Long.MAX_VALUE | "0" * 15 + "1" + "7" + "f" * 15 - Long.MAX_VALUE | Long.MIN_VALUE | "7" + "f" * 15 + "8" + "0" * 15 - Long.MAX_VALUE | 1L | "7" + "f" * 15 + "0" * 15 + "1" - Long.MAX_VALUE | Long.MAX_VALUE | "7" + "f" * 15 + "7" + "f" * 15 - 0L | 0L | "0" * 1 - 0L | 0L | "0" * 16 - 0L | 0L | "0" * 17 - 0L | 0L | "0" * 32 - 0L | 15L | "f" * 1 - 0L | -1L | "f" * 16 - 15L | -1L | "f" * 17 - -1L | -1L | "f" * 32 - 1311768467463790320L | 1311768467463790320L | "123456789abcdef0123456789abcdef0" - } - - def "fail parsing illegal 128-bit id hexadecimal String representation: #hexId"() { - when: - DD128bTraceId.fromHex(hexId) - - then: - thrown NumberFormatException - - where: - hexId << [ - null, - "", - "-1", - "-A", - "1" * 33, - "123ABC", - "123abcg", - ] - } - - def "fail parsing illegal 128-bit id hexadecimal String representation from partial String: #hexId"() { - when: - DD128bTraceId.fromHex(hexId, start, length, lowerCaseOnly) - - then: - thrown NumberFormatException - - where: - hexId | start | length | lowerCaseOnly - // Null string - null | 0 | 0 | true - // Empty string - "" | 0 | 0 | true - // Out of bound - "123456789abcdef0" | 0 | 17 | true - "123456789abcdef0" | 7 | 10 | true - "123456789abcdef0" | 17 | 0 | true - // Invalid characters - "-1" | 0 | 1 | true - "-a" | 0 | 1 | true - "123abcg" | 0 | 7 | true - // Invalid case - "A" | 0 | 1 | true - "123ABC" | 0 | 6 | true - // Too long id - "1" * 33 | 0 | 33 | true - } - - def "check ZERO constant initialization"() { - when: - def zero = DDTraceId.ZERO - def fromZero = DDTraceId.from(0) - - then: - zero != null - zero.is(fromZero) - } -} diff --git a/dd-trace-api/src/test/groovy/datadog/trace/api/IdGenerationStrategyTest.groovy b/dd-trace-api/src/test/groovy/datadog/trace/api/IdGenerationStrategyTest.groovy deleted file mode 100644 index 7f8d31ed916..00000000000 --- a/dd-trace-api/src/test/groovy/datadog/trace/api/IdGenerationStrategyTest.groovy +++ /dev/null @@ -1,83 +0,0 @@ -package datadog.trace.api - -import datadog.trace.test.util.DDSpecification - -import java.security.SecureRandom - -class IdGenerationStrategyTest extends DDSpecification { - def "generate id with #strategyName and #tIdSize bits"() { - when: - def strategy = IdGenerationStrategy.fromName(strategyName, tId128b) - def traceIds = (0..32768).collect { strategy.generateTraceId() } - Set checked = new HashSet<>() - - then: - traceIds.forEach { traceId -> - // Test equals implementation - assert !traceId.equals(null) - assert !traceId.equals("foo") - assert traceId != DDTraceId.ZERO - assert traceId.equals(traceId) - // Test #hashCode implementation - assert traceId.hashCode() == (int) (traceId.toHighOrderLong() ^ (traceId.toHighOrderLong() >>> 32) ^ traceId.toLong() ^ (traceId.toLong() >>> 32)) - assert !checked.contains(traceId) - checked.add(traceId) - } - - where: - tId128b | strategyName - false | "RANDOM" - false | "SEQUENTIAL" - false | "SECURE_RANDOM" - true | "RANDOM" - true | "SEQUENTIAL" - true | "SECURE_RANDOM" - - tIdSize = tId128b ? 128 : 64 - } - - def "return null for non existing strategy #strategyName"() { - when: - def strategy = IdGenerationStrategy.fromName(strategyName) - - then: - strategy == null - - where: - // Check unknown strategies for code coverage - strategyName << ["SOME", "UNKNOWN", "STRATEGIES"] - } - - def "exception created on SecureRandom strategy"() { - setup: - def provider = Mock(IdGenerationStrategy.ThrowingSupplier) - - when: - new IdGenerationStrategy.SRandom(false, provider) - - then: - 1 * provider.get() >> { throw new IllegalArgumentException("SecureRandom init exception") } - 0 * _ - final ExceptionInInitializerError exception = thrown() - exception.cause.message == "SecureRandom init exception" - } - - def "SecureRandom ids will always be non-zero"() { - setup: - def provider = Mock(IdGenerationStrategy.ThrowingSupplier) - def random = Mock(SecureRandom) - - when: - def strategy = new IdGenerationStrategy.SRandom(false, provider) - strategy.generateTraceId().toLong() == 47 - strategy.generateSpanId() == 11 - - then: - 1 * provider.get() >> { random } - 1 * random.nextLong() >> { 0 } - 1 * random.nextLong() >> { 47 } - 1 * random.nextLong() >> { 0 } - 1 * random.nextLong() >> { 11 } - 0 * _ - } -} diff --git a/dd-trace-api/src/test/java/datadog/trace/api/DDSpanIdTest.java b/dd-trace-api/src/test/java/datadog/trace/api/DDSpanIdTest.java new file mode 100644 index 00000000000..e6cc4719b88 --- /dev/null +++ b/dd-trace-api/src/test/java/datadog/trace/api/DDSpanIdTest.java @@ -0,0 +1,181 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.math.BigInteger; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Stream; +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; + +class DDSpanIdTest { + + @ParameterizedTest(name = "convert ids from/to String {0}") + @MethodSource("convertIdsFromToStringArguments") + void convertIdsFromToString(String displayName, String stringId, long expectedId) { + long ddid = DDSpanId.from(stringId); + + assertEquals(expectedId, ddid); + assertEquals(stringId, DDSpanId.toString(ddid)); + } + + static Stream convertIdsFromToStringArguments() { + return Stream.of( + Arguments.of("zero", "0", 0L), + Arguments.of("one", "1", 1L), + Arguments.of("max", "18446744073709551615", DDSpanId.MAX), + Arguments.of("long max", String.valueOf(Long.MAX_VALUE), Long.MAX_VALUE), + Arguments.of( + "long max plus one", + BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE).toString(), + Long.MIN_VALUE)); + } + + @ParameterizedTest(name = "fail on illegal String {0}") + @MethodSource("failOnIllegalStringArguments") + void failOnIllegalString(String stringId) { + assertThrows(NumberFormatException.class, () -> DDSpanId.from(stringId)); + } + + static Stream failOnIllegalStringArguments() { + return Stream.of( + Arguments.of((Object) null), + Arguments.of(""), + Arguments.of("-1"), + Arguments.of("18446744073709551616"), + Arguments.of("18446744073709551625"), + Arguments.of("184467440737095516150"), + Arguments.of("18446744073709551a1"), + Arguments.of("184467440737095511a")); + } + + @ParameterizedTest(name = "convert ids from/to hex String {0}") + @MethodSource("convertIdsFromToHexStringArguments") + void convertIdsFromToHexString(String hexId, long expectedId) { + long ddid = DDSpanId.fromHex(hexId); + String padded16 = + hexId.length() <= 16 ? leftPadWithZeros(hexId, 16) : hexId.substring(hexId.length() - 16); + String normalizedHexId = hexId.length() > 1 ? hexId.replaceFirst("^0+", "") : hexId; + if (normalizedHexId.isEmpty()) { + normalizedHexId = "0"; + } + + assertEquals(expectedId, ddid); + assertEquals(normalizedHexId, DDSpanId.toHexString(ddid)); + assertEquals(padded16, DDSpanId.toHexStringPadded(ddid)); + } + + static Stream convertIdsFromToHexStringArguments() { + return Stream.of( + Arguments.of("0", 0L), + Arguments.of("1", 1L), + Arguments.of(repeat("f", 16), DDSpanId.MAX), + Arguments.of("7" + repeat("f", 15), Long.MAX_VALUE), + Arguments.of("8" + repeat("0", 15), Long.MIN_VALUE), + Arguments.of(repeat("0", 4) + "8" + repeat("0", 15), Long.MIN_VALUE), + Arguments.of("cafebabe", 3405691582L), + Arguments.of("123456789abcdef", 81985529216486895L)); + } + + @ParameterizedTest(name = "convert ids from part of hex String {0}") + @MethodSource("convertIdsFromPartOfHexStringArguments") + void convertIdsFromPartOfHexString( + String displayName, + String hexId, + int start, + int length, + boolean lowerCaseOnly, + Long expectedId) { + Long parsedId = null; + try { + parsedId = DDSpanId.fromHex(hexId, start, length, lowerCaseOnly); + } catch (NumberFormatException ignored) { + } + + if (expectedId == null) { + assertNull(parsedId); + } else { + assertNotNull(parsedId); + assertEquals(expectedId.longValue(), parsedId.longValue()); + } + } + + static Stream convertIdsFromPartOfHexStringArguments() { + return Stream.of( + Arguments.of("null input", null, 1, 1, false, null), + Arguments.of("empty input", "", 1, 1, false, null), + Arguments.of("negative start", "00", -1, 1, false, null), + Arguments.of("zero length", "00", 0, 0, false, null), + Arguments.of("single zero at index 0", "00", 0, 1, false, DDSpanId.ZERO), + Arguments.of("single zero at index 1", "00", 1, 1, false, DDSpanId.ZERO), + Arguments.of("single zero at index 1 duplicate", "00", 1, 1, false, DDSpanId.ZERO), + Arguments.of("max lower-case", repeat("f", 16), 0, 16, true, DDSpanId.MAX), + Arguments.of( + "upper-case rejected when lower-case only", + repeat("f", 12) + "Ffff", + 0, + 16, + true, + null), + Arguments.of( + "upper-case accepted when lower-case disabled", + repeat("f", 12) + "Ffff", + 0, + 16, + false, + DDSpanId.MAX)); + } + + @ParameterizedTest(name = "fail on illegal hex String {0}") + @MethodSource("failOnIllegalHexStringArguments") + void failOnIllegalHexString(String hexId) { + assertThrows(NumberFormatException.class, () -> DDSpanId.fromHex(hexId)); + } + + static Stream failOnIllegalHexStringArguments() { + return Stream.of( + Arguments.of((Object) null), + Arguments.of(""), + Arguments.of("-1"), + Arguments.of("1" + repeat("0", 16)), + Arguments.of(repeat("f", 14) + "zf"), + Arguments.of(repeat("f", 15) + "z")); + } + + @ParameterizedTest(name = "generate id with {0}") + @ValueSource(strings = {"RANDOM", "SEQUENTIAL", "SECURE_RANDOM"}) + void generateIdWithStrategy(String strategyName) { + IdGenerationStrategy strategy = IdGenerationStrategy.fromName(strategyName); + Set checked = new HashSet(); + + for (int index = 0; index <= 32768; index++) { + long spanId = strategy.generateSpanId(); + assertNotEquals(0L, spanId); + assertFalse(checked.contains(spanId)); + checked.add(spanId); + } + } + + private static String leftPadWithZeros(String value, int size) { + if (value.length() >= size) { + return value; + } + return repeat("0", size - value.length()) + value; + } + + private static String repeat(String value, int count) { + StringBuilder builder = new StringBuilder(value.length() * count); + for (int index = 0; index < count; index++) { + builder.append(value); + } + return builder.toString(); + } +} diff --git a/dd-trace-api/src/test/java/datadog/trace/api/DDTraceIdTest.java b/dd-trace-api/src/test/java/datadog/trace/api/DDTraceIdTest.java new file mode 100644 index 00000000000..176b0aeed3f --- /dev/null +++ b/dd-trace-api/src/test/java/datadog/trace/api/DDTraceIdTest.java @@ -0,0 +1,286 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.math.BigInteger; +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; + +class DDTraceIdTest { + + @ParameterizedTest(name = "convert 64-bit ids from/to long {1} and check strings") + @MethodSource("convert64BitIdsFromToLongAndCheckStringsArguments") + void convert64BitIdsFromToLongAndCheckStrings( + String displayName, + long longId, + DD64bTraceId expectedId, + String expectedString, + String expectedHex) { + DD64bTraceId ddid = DD64bTraceId.from(longId); + DDTraceId defaultDdid = DDTraceId.from(longId); + + assertEquals(expectedId, ddid); + assertEquals(defaultDdid, ddid); + assertEquals(longId, ddid.toLong()); + assertEquals(0L, ddid.toHighOrderLong()); + assertEquals(expectedString, ddid.toString()); + assertEquals(expectedHex, ddid.toHexString()); + } + + static Stream convert64BitIdsFromToLongAndCheckStringsArguments() { + return Stream.of( + Arguments.of("zero", 0L, DD64bTraceId.ZERO, "0", repeat("0", 32)), + Arguments.of("one", 1L, DD64bTraceId.ONE, "1", repeat("0", 31) + "1"), + Arguments.of( + "minus one", + -1L, + DD64bTraceId.MAX, + "18446744073709551615", + repeat("0", 16) + repeat("f", 16)), + Arguments.of( + "long max", + Long.MAX_VALUE, + DD64bTraceId.from(Long.MAX_VALUE), + "9223372036854775807", + repeat("0", 16) + "7" + repeat("f", 15)), + Arguments.of( + "long min", + Long.MIN_VALUE, + DD64bTraceId.from(Long.MIN_VALUE), + "9223372036854775808", + repeat("0", 16) + "8" + repeat("0", 15))); + } + + @ParameterizedTest(name = "convert 64-bit ids from/to String representation: {1}") + @MethodSource("convert64BitIdsFromToStringRepresentationArguments") + void convert64BitIdsFromToStringRepresentation( + String displayName, String stringId, DD64bTraceId expectedId) { + DD64bTraceId ddid = DD64bTraceId.from(stringId); + + assertEquals(expectedId, ddid); + assertEquals(stringId, ddid.toString()); + } + + static Stream convert64BitIdsFromToStringRepresentationArguments() { + return Stream.of( + Arguments.of("zero", "0", DD64bTraceId.ZERO), + Arguments.of("one", "1", DD64bTraceId.ONE), + Arguments.of("max", "18446744073709551615", DD64bTraceId.MAX), + Arguments.of("long max", String.valueOf(Long.MAX_VALUE), DD64bTraceId.from(Long.MAX_VALUE)), + Arguments.of( + "long max plus one", + BigInteger.valueOf(Long.MAX_VALUE).add(BigInteger.ONE).toString(), + DD64bTraceId.from(Long.MIN_VALUE))); + } + + @ParameterizedTest(name = "fail parsing illegal 64-bit id String representation: {0}") + @MethodSource("failParsingIllegal64BitIdStringRepresentationArguments") + void failParsingIllegal64BitIdStringRepresentation(String stringId) { + assertThrows(NumberFormatException.class, () -> DD64bTraceId.from(stringId)); + } + + static Stream failParsingIllegal64BitIdStringRepresentationArguments() { + return Stream.of( + Arguments.of((Object) null), + Arguments.of(""), + Arguments.of("-1"), + Arguments.of("18446744073709551616"), + Arguments.of("18446744073709551625"), + Arguments.of("184467440737095516150"), + Arguments.of("18446744073709551a1"), + Arguments.of("184467440737095511a")); + } + + @ParameterizedTest(name = "convert 64-bit ids from/to hex String representation: {0}") + @MethodSource("convert64BitIdsFromToHexStringRepresentationArguments") + void convert64BitIdsFromToHexStringRepresentation(String hexId, DD64bTraceId expectedId) { + DD64bTraceId ddid = DD64bTraceId.fromHex(hexId); + String padded16 = + hexId.length() <= 16 ? leftPadWithZeros(hexId, 16) : hexId.substring(hexId.length() - 16); + String padded32 = leftPadWithZeros(hexId, 32); + + assertEquals(expectedId, ddid); + assertEquals(padded32, ddid.toHexString()); + assertEquals(padded16, ddid.toHexStringPadded(16)); + assertEquals(padded32, ddid.toHexStringPadded(32)); + } + + static Stream convert64BitIdsFromToHexStringRepresentationArguments() { + return Stream.of( + Arguments.of("0", DD64bTraceId.ZERO), + Arguments.of("1", DD64bTraceId.ONE), + Arguments.of(repeat("f", 16), DD64bTraceId.MAX), + Arguments.of("7" + repeat("f", 15), DD64bTraceId.from(Long.MAX_VALUE)), + Arguments.of("8" + repeat("0", 15), DD64bTraceId.from(Long.MIN_VALUE)), + Arguments.of(repeat("0", 4) + "8" + repeat("0", 15), DD64bTraceId.from(Long.MIN_VALUE)), + Arguments.of("cafebabe", DD64bTraceId.from(3405691582L)), + Arguments.of("123456789abcdef", DD64bTraceId.from(81985529216486895L))); + } + + @ParameterizedTest(name = "fail parsing illegal 64-bit hexadecimal String representation: {0}") + @MethodSource("failParsingIllegal64BitHexadecimalStringRepresentationArguments") + void failParsingIllegal64BitHexadecimalStringRepresentation(String hexId) { + assertThrows(NumberFormatException.class, () -> DD64bTraceId.fromHex(hexId)); + } + + static Stream failParsingIllegal64BitHexadecimalStringRepresentationArguments() { + return Stream.of( + Arguments.of((Object) null), + Arguments.of(""), + Arguments.of("-1"), + Arguments.of("1" + repeat("0", 16)), + Arguments.of(repeat("f", 14) + "zf"), + Arguments.of(repeat("f", 15) + "z")); + } + + @ParameterizedTest(name = "convert 128-bit ids from/to hexadecimal String representation {3}") + @MethodSource("convert128BitIdsFromToHexadecimalStringRepresentationArguments") + void convert128BitIdsFromToHexadecimalStringRepresentation( + String displayName, long highOrderBits, long lowOrderBits, String hexId) { + DDTraceId parsedId = DD128bTraceId.fromHex(hexId); + DDTraceId id = DD128bTraceId.from(highOrderBits, lowOrderBits); + String paddedHexId = leftPadWithZeros(hexId, 32); + + assertEquals(id, parsedId); + assertEquals(paddedHexId, parsedId.toHexString()); + assertEquals(paddedHexId.substring(16, 32), parsedId.toHexStringPadded(16)); + assertEquals(paddedHexId, parsedId.toHexStringPadded(32)); + assertEquals(lowOrderBits, parsedId.toLong()); + assertEquals(highOrderBits, parsedId.toHighOrderLong()); + assertEquals(Long.toUnsignedString(lowOrderBits), parsedId.toString()); + } + + static Stream convert128BitIdsFromToHexadecimalStringRepresentationArguments() { + return Stream.of( + Arguments.of( + "both long min", + Long.MIN_VALUE, + Long.MIN_VALUE, + "8" + repeat("0", 15) + "8" + repeat("0", 15)), + Arguments.of( + "high long min low one", + Long.MIN_VALUE, + 1L, + "8" + repeat("0", 15) + repeat("0", 15) + "1"), + Arguments.of( + "high long min low long max", + Long.MIN_VALUE, + Long.MAX_VALUE, + "8" + repeat("0", 15) + "7" + repeat("f", 15)), + Arguments.of( + "high one low long min", + 1L, + Long.MIN_VALUE, + repeat("0", 15) + "1" + "8" + repeat("0", 15)), + Arguments.of("high one low one", 1L, 1L, repeat("0", 15) + "1" + repeat("0", 15) + "1"), + Arguments.of( + "high one low long max", + 1L, + Long.MAX_VALUE, + repeat("0", 15) + "1" + "7" + repeat("f", 15)), + Arguments.of( + "high long max low long min", + Long.MAX_VALUE, + Long.MIN_VALUE, + "7" + repeat("f", 15) + "8" + repeat("0", 15)), + Arguments.of( + "high long max low one", + Long.MAX_VALUE, + 1L, + "7" + repeat("f", 15) + repeat("0", 15) + "1"), + Arguments.of( + "high long max low long max", + Long.MAX_VALUE, + Long.MAX_VALUE, + "7" + repeat("f", 15) + "7" + repeat("f", 15)), + Arguments.of("all zeros length one", 0L, 0L, repeat("0", 1)), + Arguments.of("all zeros length sixteen", 0L, 0L, repeat("0", 16)), + Arguments.of("all zeros length seventeen", 0L, 0L, repeat("0", 17)), + Arguments.of("all zeros length thirty-two", 0L, 0L, repeat("0", 32)), + Arguments.of("low fifteen", 0L, 15L, repeat("f", 1)), + Arguments.of("low minus one", 0L, -1L, repeat("f", 16)), + Arguments.of("high fifteen low minus one", 15L, -1L, repeat("f", 17)), + Arguments.of("all f", -1L, -1L, repeat("f", 32)), + Arguments.of( + "hex literal", + 1311768467463790320L, + 1311768467463790320L, + "123456789abcdef0123456789abcdef0")); + } + + @ParameterizedTest( + name = "fail parsing illegal 128-bit id hexadecimal String representation: {0}") + @MethodSource("failParsingIllegal128BitIdHexadecimalStringRepresentationArguments") + void failParsingIllegal128BitIdHexadecimalStringRepresentation(String hexId) { + assertThrows(NumberFormatException.class, () -> DD128bTraceId.fromHex(hexId)); + } + + static Stream failParsingIllegal128BitIdHexadecimalStringRepresentationArguments() { + return Stream.of( + Arguments.of((Object) null), + Arguments.of(""), + Arguments.of("-1"), + Arguments.of("-A"), + Arguments.of(repeat("1", 33)), + Arguments.of("123ABC"), + Arguments.of("123abcg")); + } + + @ParameterizedTest( + name = + "fail parsing illegal 128-bit id hexadecimal String representation from partial String: {1}") + @MethodSource( + "failParsingIllegal128BitIdHexadecimalStringRepresentationFromPartialStringArguments") + void failParsingIllegal128BitIdHexadecimalStringRepresentationFromPartialString( + String displayName, String hexId, int start, int length, boolean lowerCaseOnly) { + assertThrows( + NumberFormatException.class, + () -> DD128bTraceId.fromHex(hexId, start, length, lowerCaseOnly)); + } + + static Stream + failParsingIllegal128BitIdHexadecimalStringRepresentationFromPartialStringArguments() { + return Stream.of( + Arguments.of("null string", null, 0, 0, true), + Arguments.of("empty string", "", 0, 0, true), + Arguments.of("out of bound length", "123456789abcdef0", 0, 17, true), + Arguments.of("out of bound end", "123456789abcdef0", 7, 10, true), + Arguments.of("out of bound start", "123456789abcdef0", 17, 0, true), + Arguments.of("invalid minus one", "-1", 0, 1, true), + Arguments.of("invalid minus a", "-a", 0, 1, true), + Arguments.of("invalid character", "123abcg", 0, 7, true), + Arguments.of("invalid upper case A", "A", 0, 1, true), + Arguments.of("invalid upper case ABC", "123ABC", 0, 6, true), + Arguments.of("too long", repeat("1", 33), 0, 33, true)); + } + + @Test + void checkZeroConstantInitialization() { + DDTraceId zero = DDTraceId.ZERO; + DDTraceId fromZero = DDTraceId.from(0); + + assertNotNull(zero); + assertSame(fromZero, zero); + } + + private static String leftPadWithZeros(String value, int size) { + if (value.length() >= size) { + return value; + } + return repeat("0", size - value.length()) + value; + } + + private static String repeat(String value, int count) { + StringBuilder builder = new StringBuilder(value.length() * count); + for (int index = 0; index < count; index++) { + builder.append(value); + } + return builder.toString(); + } +} diff --git a/dd-trace-api/src/test/java/datadog/trace/api/IdGenerationStrategyTest.java b/dd-trace-api/src/test/java/datadog/trace/api/IdGenerationStrategyTest.java new file mode 100644 index 00000000000..2504b55ed24 --- /dev/null +++ b/dd-trace-api/src/test/java/datadog/trace/api/IdGenerationStrategyTest.java @@ -0,0 +1,126 @@ +package datadog.trace.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.security.SecureRandom; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class IdGenerationStrategyTest { + + @ParameterizedTest(name = "generate id with {1} and {0} bits") + @CsvSource( + value = { + "false|RANDOM", + "false|SEQUENTIAL", + "false|SECURE_RANDOM", + "true|RANDOM", + "true|SEQUENTIAL", + "true|SECURE_RANDOM" + }, + delimiter = '|') + void generateIdWithStrategyAndBitSize( + boolean traceId128BitGenerationEnabled, String strategyName) { + IdGenerationStrategy strategy = + IdGenerationStrategy.fromName(strategyName, traceId128BitGenerationEnabled); + Set checked = new HashSet(); + + for (int index = 0; index <= 32768; index++) { + DDTraceId traceId = strategy.generateTraceId(); + assertNotNull(traceId); + assertFalse(traceId.equals(null)); + assertFalse(traceId.equals("foo")); + assertNotEquals(DDTraceId.ZERO, traceId); + assertTrue(traceId.equals(traceId)); + + int expectedHash = + (int) + (traceId.toHighOrderLong() + ^ (traceId.toHighOrderLong() >>> 32) + ^ traceId.toLong() + ^ (traceId.toLong() >>> 32)); + assertEquals(expectedHash, traceId.hashCode()); + + assertFalse(checked.contains(traceId)); + checked.add(traceId); + } + } + + @ParameterizedTest(name = "return null for non existing strategy {0}") + @ValueSource(strings = {"SOME", "UNKNOWN", "STRATEGIES"}) + void returnNullForNonExistingStrategy(String strategyName) { + assertNull(IdGenerationStrategy.fromName(strategyName)); + } + + @org.junit.jupiter.api.Test + void exceptionCreatedOnSecureRandomStrategy() { + ExceptionInInitializerError error = + assertThrows( + ExceptionInInitializerError.class, + () -> + new IdGenerationStrategy.SRandom( + false, + () -> { + throw new IllegalArgumentException("SecureRandom init exception"); + })); + + assertNotNull(error.getCause()); + assertEquals("SecureRandom init exception", error.getCause().getMessage()); + } + + @org.junit.jupiter.api.Test + void secureRandomIdsWillAlwaysBeNonZero() { + ScriptedSecureRandom random = new ScriptedSecureRandom(new long[] {0L, 47L, 0L, 11L}); + CallCounter providerCallCounter = new CallCounter(); + + IdGenerationStrategy strategy = + new IdGenerationStrategy.SRandom( + false, + () -> { + providerCallCounter.count++; + return random; + }); + + long traceId = strategy.generateTraceId().toLong(); + long spanId = strategy.generateSpanId(); + + assertEquals(1, providerCallCounter.count); + assertEquals(47L, traceId); + assertEquals(11L, spanId); + assertEquals(4, random.calls); + } + + private static class CallCounter { + private int count; + } + + private static class ScriptedSecureRandom extends SecureRandom { + private final long[] values; + private int index; + private int calls; + + private ScriptedSecureRandom(long[] values) { + this.values = values; + } + + @Override + public long nextLong() { + calls++; + if (index >= values.length) { + return 0L; + } + long value = values[index]; + index++; + return value; + } + } +}