diff --git a/.claude/skills/migrate-groovy-to-java/SKILL.md b/.claude/skills/migrate-groovy-to-java/SKILL.md index dd6556e53f9..8d74a118ecb 100644 --- a/.claude/skills/migrate-groovy-to-java/SKILL.md +++ b/.claude/skills/migrate-groovy-to-java/SKILL.md @@ -5,16 +5,18 @@ description: migrate test groovy files to java Migrate test Groovy files to Java using JUnit 5 -1. List all groovy files of the current gradle module -2. convert groovy files to Java using Junit 5 -3. make sure the tests are still passing after migration -4. remove groovy files +1. List all Groovy files of the current Gradle module +2. Convert Groovy files to Java using JUnit 5 +3. Make sure the tests are still passing after migration and that the test count has not changed +4. Remove Groovy files -When converting groovy code to java code make sure that: -- the Java code generated is compatible with JDK 8 -- when translating Spock test, favor using `@CsvSource` with `|` delimiters -- when using a `@MethodSource`, use the test method name, and suffix it with `_arguments` -- when converting tuples, create light dedicated structure instead to keep the typing system +When converting Groovy code to Java code, make sure that: +- The Java code generated is compatible with JDK 8 +- When translating Spock tests, favor using `@CsvSource` with `|` delimiters +- When using `@MethodSource`, name the arguments method by appending `Arguments` using camelCase to the test method name (e.g. `testMethodArguments`) +- Ensure parameterized test names are human-readable (i.e. no hashcodes); instead add a description string as the first `Arguments.of(...)` value or index the test case +- When converting tuples, create a light dedicated structure instead to keep the typing system - Instead of checking a state and throwing an exception, use JUnit asserts -- Do not wrap checked exception and throwing a Runtime exception, prefer adding a throws clause at method declaration +- Do not wrap checked exceptions and throw a Runtime exception; prefer adding a throws clause at method declaration - Do not mark local variables `final` +- Ensure variables are human-readable; avoid single-letter names and pre-define variables that are referenced multiple times diff --git a/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy b/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy deleted file mode 100644 index 43dd4dcb0aa..00000000000 --- a/components/json/src/test/groovy/datadog/json/JsonMapperTest.groovy +++ /dev/null @@ -1,170 +0,0 @@ -package datadog.json - -import spock.lang.Specification - -import static java.lang.Math.PI -import static java.util.Collections.emptyMap - -class JsonMapperTest extends Specification { - - def "test mapping to JSON object: #input"() { - setup: - def parsedExpected = input == null ? emptyMap() : input.clone() - parsedExpected.collect { - it -> { - if (it.value instanceof UnsupportedType) { - it.value = it.value.toString() - } else if (it.value instanceof Float) { - it.value = new Double(it.value) - } - - it - } - } - - when: - String json = JsonMapper.toJson((Map) input) - - then: - json == expected - - when: - def parsed = JsonMapper.fromJsonToMap(json) - - then: - if (input == null) { - parsed == [:] - } else { - parsed.size() == input.size() - input.each { - assert parsed.containsKey(it.key) - if (it.value instanceof UnsupportedType) { - assert parsed.get(it.key) == it.value.toString() - } else if (it.value instanceof Float) { - assert parsed.get(it.key) instanceof Double - assert (parsed.get(it.key) - it.value) < 0.001 - } else { - assert parsed.get(it.key) == it.value - } - } - } - - where: - input | expected - null | '{}' - new HashMap<>() | '{}' - ['key1': 'value1'] | '{"key1":"value1"}' - ['key1': 'value1', 'key2': 'value2'] | '{"key1":"value1","key2":"value2"}' - ['key1': 'va"lu"e1', 'ke"y2': 'value2'] | '{"key1":"va\\"lu\\"e1","ke\\"y2":"value2"}' - ['key1': null, 'key2': 'bar', 'key3': 3, 'key4': 3456789123L, 'key5': 3.142f, 'key6': PI, 'key7': true, 'key8': new UnsupportedType()] | '{"key1":null,"key2":"bar","key3":3,"key4":3456789123,"key5":3.142,"key6":3.141592653589793,"key7":true,"key8":"toString"}' - } - - private class UnsupportedType { - @Override - String toString() { - 'toString' - } - } - - def "test mapping to Map from empty JSON object"() { - when: - def parsed = JsonMapper.fromJsonToMap(json) - - then: - parsed == [:] - - where: - json << [null, 'null', '', '{}'] - } - - def "test mapping to Map from non-object JSON"() { - when: - JsonMapper.fromJsonToMap(json) - - then: - thrown(IOException) - - where: - json << ['1', '[1, 2]'] - } - - def "test mapping iterable to JSON array: #input"() { - when: - String json = JsonMapper.toJson(input as Collection) - - then: - json == expected - - when: - def parsed = JsonMapper.fromJsonToList(json) - - then: - parsed == (input?:[]) - - where: - input | expected - null | "[]" - new ArrayList<>() | "[]" - ['value1'] | "[\"value1\"]" - ['value1', 'value2'] | "[\"value1\",\"value2\"]" - ['va"lu"e1', 'value2'] | "[\"va\\\"lu\\\"e1\",\"value2\"]" - } - - def "test mapping array to JSON array: #input"() { - when: - String json = JsonMapper.toJson((String[]) input) - - then: - json == expected - - when: - def parsed = JsonMapper.fromJsonToList(json).toArray(new String[0]) - - then: - parsed == (String[]) (input?:[]) - - where: - input | expected - null | "[]" - [] | "[]" - ['value1'] | "[\"value1\"]" - ['value1', 'value2'] | "[\"value1\",\"value2\"]" - ['va"lu"e1', 'value2'] | "[\"va\\\"lu\\\"e1\",\"value2\"]" - } - - def "test mapping to List from empty JSON object"() { - when: - def parsed = JsonMapper.fromJsonToList(json) - - then: - parsed == [] - - where: - json << [null, 'null', '', '[]'] - } - - def "test mapping to JSON string: input"() { - when: - String escaped = JsonMapper.toJson((String) string) - - then: - escaped == expected - - where: - string | expected - null | "" - "" | "" - ((char) 4096).toString() | '"\\u1000"' - ((char) 256).toString() | '"\\u0100"' - ((char) 128).toString() | '"\\u0080"' - "\b" | '"\\b"' - "\t" | '"\\t"' - "\n" | '"\\n"' - "\f" | '"\\f"' - "\r" | '"\\r"' - '"' | '"\\\""' - '/' | '"\\/"' - '\\' | '"\\\\"' - "a" | '"a"' - } -} diff --git a/components/json/src/test/java/datadog/json/JsonMapperTest.java b/components/json/src/test/java/datadog/json/JsonMapperTest.java new file mode 100644 index 00000000000..6f9660d07b5 --- /dev/null +++ b/components/json/src/test/java/datadog/json/JsonMapperTest.java @@ -0,0 +1,194 @@ +package datadog.json; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +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 JsonMapperTest { + + @ParameterizedTest(name = "test mapping to JSON object: {0}") + @MethodSource("testMappingToJsonObjectArguments") + void testMappingToJsonObject(String testCase, Map input, String expected) + throws IOException { + String json = JsonMapper.toJson(input); + assertEquals(expected, json); + + Map parsed = JsonMapper.fromJsonToMap(json); + if (input == null) { + assertEquals(emptyMap(), parsed); + } else { + assertEquals(input.size(), parsed.size()); + for (Map.Entry entry : input.entrySet()) { + String expectedKey = entry.getKey(); + Object expectedValue = entry.getValue(); + assertTrue(parsed.containsKey(expectedKey)); + Object parsedValue = parsed.get(expectedKey); + if (expectedValue instanceof UnsupportedType) { + assertEquals(expectedValue.toString(), parsedValue); + } else if (expectedValue instanceof Float) { + assertTrue(parsedValue instanceof Double); + assertEquals((Float) expectedValue, (Double) parsedValue, 0.001); + } else { + assertEquals(expectedValue, parsedValue); + } + } + } + } + + static Stream testMappingToJsonObjectArguments() { + Map singleEntry = new LinkedHashMap<>(); + singleEntry.put("key1", "value1"); + + Map twoEntries = new LinkedHashMap<>(); + twoEntries.put("key1", "value1"); + twoEntries.put("key2", "value2"); + + Map quotedEntries = new LinkedHashMap<>(); + quotedEntries.put("key1", "va\"lu\"e1"); + quotedEntries.put("ke\"y2", "value2"); + + Map complexMap = new LinkedHashMap<>(); + complexMap.put("key1", null); + complexMap.put("key2", "bar"); + complexMap.put("key3", 3); + complexMap.put("key4", 3456789123L); + complexMap.put("key5", 3.142f); + complexMap.put("key6", Math.PI); + complexMap.put("key7", true); + complexMap.put("key8", new UnsupportedType()); + + return Stream.of( + Arguments.of("null input", null, "{}"), + Arguments.of("empty map", new HashMap<>(), "{}"), + Arguments.of("single entry", singleEntry, "{\"key1\":\"value1\"}"), + Arguments.of("two entries", twoEntries, "{\"key1\":\"value1\",\"key2\":\"value2\"}"), + Arguments.of( + "quoted entries", + quotedEntries, + "{\"key1\":\"va\\\"lu\\\"e1\",\"ke\\\"y2\":\"value2\"}"), + Arguments.of( + "complex map", + complexMap, + "{\"key1\":null,\"key2\":\"bar\",\"key3\":3,\"key4\":3456789123,\"key5\":3.142,\"key6\":3.141592653589793,\"key7\":true,\"key8\":\"toString\"}")); + } + + @ParameterizedTest(name = "test mapping to Map from empty JSON object: {0}") + @MethodSource("testMappingToMapFromEmptyJsonObjectArguments") + void testMappingToMapFromEmptyJsonObject(String json) throws IOException { + Map parsed = JsonMapper.fromJsonToMap(json); + assertEquals(emptyMap(), parsed); + } + + static Stream testMappingToMapFromEmptyJsonObjectArguments() { + return Stream.of( + Arguments.of((Object) null), Arguments.of("null"), Arguments.of(""), Arguments.of("{}")); + } + + @ParameterizedTest(name = "test mapping to Map from non-object JSON: {0}") + @ValueSource(strings = {"1", "[1, 2]"}) + void testMappingToMapFromNonObjectJson(String json) { + assertThrows(IOException.class, () -> JsonMapper.fromJsonToMap(json)); + } + + @ParameterizedTest(name = "test mapping iterable to JSON array: {0}") + @MethodSource("testMappingIterableToJsonArrayArguments") + void testMappingIterableToJsonArray(List input, String expected) throws IOException { + String json = JsonMapper.toJson(input); + assertEquals(expected, json); + + List parsed = JsonMapper.fromJsonToList(json); + assertEquals(input != null ? input : emptyList(), parsed); + } + + static Stream testMappingIterableToJsonArrayArguments() { + return Stream.of( + Arguments.of(null, "[]"), + Arguments.of(new ArrayList<>(), "[]"), + Arguments.of(Arrays.asList("value1"), "[\"value1\"]"), + Arguments.of(Arrays.asList("value1", "value2"), "[\"value1\",\"value2\"]"), + Arguments.of(Arrays.asList("va\"lu\"e1", "value2"), "[\"va\\\"lu\\\"e1\",\"value2\"]")); + } + + @ParameterizedTest(name = "test mapping array to JSON array: {0}") + @MethodSource("testMappingArrayToJsonArrayArguments") + void testMappingArrayToJsonArray(String testCase, String[] input, String expected) + throws IOException { + String json = JsonMapper.toJson(input); + assertEquals(expected, json); + + String[] parsed = JsonMapper.fromJsonToList(json).toArray(new String[] {}); + assertArrayEquals(input != null ? input : new String[] {}, parsed); + } + + static Stream testMappingArrayToJsonArrayArguments() { + return Stream.of( + Arguments.of("null input", (Object) null, "[]"), + Arguments.of("empty array", new String[] {}, "[]"), + Arguments.of("single element", new String[] {"value1"}, "[\"value1\"]"), + Arguments.of("two elements", new String[] {"value1", "value2"}, "[\"value1\",\"value2\"]"), + Arguments.of( + "escaped quotes", + new String[] {"va\"lu\"e1", "value2"}, + "[\"va\\\"lu\\\"e1\",\"value2\"]")); + } + + @ParameterizedTest(name = "test mapping to List from empty JSON object: {0}") + @MethodSource("testMappingToListFromEmptyJsonObjectArguments") + void testMappingToListFromEmptyJsonObject(String json) throws IOException { + List parsed = JsonMapper.fromJsonToList(json); + assertEquals(emptyList(), parsed); + } + + static Stream testMappingToListFromEmptyJsonObjectArguments() { + return Stream.of( + Arguments.of((Object) null), Arguments.of("null"), Arguments.of(""), Arguments.of("[]")); + } + + @ParameterizedTest(name = "test mapping to JSON string: {0}") + @MethodSource("testMappingToJsonStringArguments") + void testMappingToJsonString(String input, String expected) { + String json = JsonMapper.toJson(input); + assertEquals(expected, json); + } + + static Stream testMappingToJsonStringArguments() { + return Stream.of( + Arguments.of((Object) null, ""), + Arguments.of("", ""), + Arguments.of(String.valueOf((char) 4096), "\"\\u1000\""), + Arguments.of(String.valueOf((char) 256), "\"\\u0100\""), + Arguments.of(String.valueOf((char) 128), "\"\\u0080\""), + Arguments.of("\b", "\"\\b\""), + Arguments.of("\t", "\"\\t\""), + Arguments.of("\n", "\"\\n\""), + Arguments.of("\f", "\"\\f\""), + Arguments.of("\r", "\"\\r\""), + Arguments.of("\"", "\"\\\"\""), + Arguments.of("/", "\"\\/\""), + Arguments.of("\\", "\"\\\\\""), + Arguments.of("a", "\"a\"")); + } + + private static class UnsupportedType { + @Override + public String toString() { + return "toString"; + } + } +}