Skip to content

Commit 4890097

Browse files
Merge branch 'master' into alexeyk/fix-port-utils
2 parents 45edb56 + 1e2a36d commit 4890097

6 files changed

Lines changed: 195 additions & 77 deletions

File tree

.claude/skills/migrate-groovy-to-java/SKILL.md

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ Migrate test Groovy files to Java using JUnit 5
1616

1717
When converting Groovy code to Java code, make sure that:
1818
- The Java code generated is compatible with JDK 8
19-
- When translating Spock tests, favor using `@CsvSource` with `|` delimiters
20-
- When using `@MethodSource`, name the arguments method by appending `Arguments` using camelCase to the test method name (e.g. `testMethodArguments`) and return a `Stream` of arguments using `Stream.of(...)` and `arguments(...)` with static import.
19+
- When translating Spock tests, prefer `@TableTest` for data rows that are naturally tabular. See detailed guidance in the "TableTest usage" section.
20+
- `@TableTest` and `@MethodSource` may be combined on the same `@ParameterizedTest` when most cases are tabular but a few cases require programmatic setup.
21+
- In combined mode, keep table-friendly cases in `@TableTest`, and put only non-tabular/complex cases in `@MethodSource`.
22+
- If `@TableTest` is not viable for the test at all, use `@MethodSource` only.
23+
- For `@MethodSource`, name the arguments method `<testMethodName>Arguments` (camelCase, e.g. `testMethodArguments`) and return `Stream<Arguments>` using `Stream.of(...)` and `arguments(...)` with static import.
2124
- Ensure parameterized test names are human-readable (i.e. no hashcodes); instead add a description string as the first `Arguments.arguments(...)` value or index the test case
2225
- When converting tuples, create a light dedicated structure instead to keep the typing system
2326
- Instead of checking a state and throwing an exception, use JUnit asserts
@@ -27,3 +30,35 @@ When converting Groovy code to Java code, make sure that:
2730
- Do not mark local variables `final`
2831
- Ensure variables are human-readable; avoid single-letter names and pre-define variables that are referenced multiple times
2932
- When translating Spock `Mock(...)` usage, use `libs.bundles.mockito` instead of writing manual recording/stub implementations
33+
34+
TableTest usage
35+
Dependency, if missing add:
36+
- Groovy: testImplementation libs.tabletest
37+
- Kotlin: testImplementation(libs.tabletest)
38+
39+
Import: `import org.tabletest.junit.TableTest;`
40+
41+
JDK 8 rules:
42+
- No text blocks.
43+
- @TableTest must use String[] annotation array syntax:
44+
```
45+
@TableTest({
46+
"a | b",
47+
"1 | 2"
48+
})
49+
```
50+
51+
Spock `where:`@TableTest:
52+
- First row = header (column names = method parameters).
53+
- Add `scenario` column as first column (display name, not a method parameter).
54+
- Use `|` delimiter; align columns so pipes line up vertically.
55+
- Prefer single quotes for strings with special chars (e.g., `'a|b'`, `'[]'`).
56+
- Blank cell = null (object types); `''` = empty string.
57+
- Collections: `[a, b]` = List/array, `{a, b}` = Set, `[k: v]` = Map.
58+
59+
Mixed eligibility:
60+
- Prefer combining `@TableTest` + `@MethodSource` on one `@ParameterizedTest` when only some cases are complex.
61+
- Use `@MethodSource` only when tabular representation is not practical for the test.
62+
63+
Do NOT use @TableTest when:
64+
- Majority of rows require complex objects or custom converters.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
name: migrate-junit-source-to-tabletest
3+
description: Convert JUnit 5 @MethodSource/@CsvSource/@ValueSource parameterized tests to @TableTest (JDK8)
4+
---
5+
Goal: Migrate JUnit 5 parameterized tests using @MethodSource/@CsvSource/@ValueSource to @TableTest with minimal churn and passing tests.
6+
7+
Process (do in this order):
8+
1) Locate targets via Grep (no agent subprocess). Search for: "@ParameterizedTest", "@MethodSource", "@CsvSource", "@ValueSource".
9+
2) Read all matching files up front (parallel is OK).
10+
3) Convert eligible tests to @TableTest.
11+
4) Write each modified file once in full using Write (no incremental per-test edits).
12+
5) Run module tests once and verify "BUILD SUCCESSFUL". If failed, inspect JUnit XML report.
13+
14+
Dependency:
15+
- If missing, add:
16+
- Groovy: testImplementation libs.tabletest
17+
- Kotlin: testImplementation(libs.tabletest)
18+
19+
Import: `import org.tabletest.junit.TableTest;`
20+
21+
JDK 8 rules:
22+
- No text blocks.
23+
- @TableTest must use String[] annotation array syntax:
24+
```
25+
@TableTest({
26+
"a | b",
27+
"1 | 2"
28+
})
29+
```
30+
31+
Table formatting rules (mandatory):
32+
- Always include a header row (parameter names).
33+
- Always add a "scenario" column; using common sense for naming; scenario is NOT a method parameter.
34+
- Use '|' as delimiter.
35+
- Align columns with spaces so pipes line up vertically.
36+
- Prefer single quotes for strings requiring quotes (e.g., 'a|b', '[]', '{}', ' ').
37+
38+
Conversions:
39+
A) @CsvSource
40+
- Remove @ParameterizedTest and @CsvSource.
41+
- If delimiter is '|': rows map directly to @TableTest.
42+
- If delimiter is ',' (default): replace ',' with '|' in rows.
43+
44+
B) @ValueSource
45+
- Convert to @TableTest with header from parameter name.
46+
- Each value becomes one row.
47+
- Add "scenario" column using common sense for name.
48+
49+
C) @MethodSource (convert only if values are representable as strings)
50+
- Convert when argument values are primitives, strings, enums, booleans, nulls, and simple collection literals supported by TableTest:
51+
- Array: [a, b, ...]
52+
- List: [a, b, ...]
53+
- Set: {a, b, ...}
54+
- Map: [k: v, ...]
55+
- `@TableTest` and `@MethodSource` may be combined on the same `@ParameterizedTest` when most cases are tabular but a few cases require programmatic setup.
56+
- In combined mode, keep table-friendly cases in `@TableTest`, and put only non-tabular/complex cases in `@MethodSource`.
57+
- If `@TableTest` is not viable for the test at all, use `@MethodSource` only.
58+
- For `@MethodSource`, name the arguments method `<testMethodName>Arguments` (camelCase, e.g. `testMethodArguments`) and return `Stream<Arguments>` using `Stream.of(...)` and `arguments(...)` with static import.
59+
- Blank cell = null (non-primitive).
60+
- '' = empty string.
61+
- For String params that start with '[' or '{', quote to avoid collection parsing (prefer '[]'/'{}').
62+
63+
Scenario handling:
64+
- If MethodSource includes a leading description string OR @ParameterizedTest(name=...) uses {0}, convert that to a scenario column and remove that parameter from method signature.
65+
66+
Cleanup:
67+
- Delete now-unused @MethodSource provider methods and unused imports.
68+
69+
Mixed eligibility:
70+
- Prefer combining `@TableTest` + `@MethodSource` on one `@ParameterizedTest` when only some cases are complex.
71+
72+
Do NOT convert when:
73+
- Most rows require complex builders/mocks.
74+
75+
Test command (exact):
76+
./gradlew :path:to:module:test --rerun-tasks 2>&1 | tail -20
77+
- If BUILD FAILED: cat path/to/module/build/test-results/test/TEST-*.xml

components/json/build.gradle.kts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ apply(from = "$rootDir/gradle/java.gradle")
77
jmh {
88
jmhVersion = libs.versions.jmh.get()
99
}
10+
11+
dependencies {
12+
testImplementation(libs.tabletest)
13+
}

components/json/src/test/java/datadog/json/JsonMapperTest.java

Lines changed: 68 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,29 @@
1010
import static org.junit.jupiter.params.provider.Arguments.arguments;
1111

1212
import java.io.IOException;
13-
import java.util.ArrayList;
14-
import java.util.Arrays;
15-
import java.util.HashMap;
1613
import java.util.LinkedHashMap;
1714
import java.util.List;
1815
import java.util.Map;
1916
import java.util.stream.Stream;
2017
import org.junit.jupiter.params.ParameterizedTest;
2118
import org.junit.jupiter.params.provider.Arguments;
2219
import org.junit.jupiter.params.provider.MethodSource;
23-
import org.junit.jupiter.params.provider.ValueSource;
20+
import org.tabletest.junit.Scenario;
21+
import org.tabletest.junit.TableTest;
2422

2523
class JsonMapperTest {
26-
24+
@TableTest({
25+
"Scenario | Input | Expected ",
26+
"null input | | '{}' ",
27+
"empty map | [:] | '{}' ",
28+
"single entry | [key1: value1] | '{\"key1\":\"value1\"}' ",
29+
"two entries | [key1: value1, key2: value2] | '{\"key1\":\"value1\",\"key2\":\"value2\"}' ",
30+
"quoted entries | [key1: va\"lu\"e1, ke\"y2: value2] | '{\"key1\":\"va\\\"lu\\\"e1\",\"ke\\\"y2\":\"value2\"}'"
31+
})
2732
@ParameterizedTest(name = "test mapping to JSON object: {0}")
2833
@MethodSource("testMappingToJsonObjectArguments")
2934
void testMappingToJsonObject(
30-
@SuppressWarnings("unused") String testCase, Map<String, Object> input, String expected)
35+
@Scenario String ignoredScenario, Map<String, Object> input, String expected)
3136
throws IOException {
3237
String json = JsonMapper.toJson(input);
3338
assertEquals(expected, json);
@@ -55,17 +60,6 @@ void testMappingToJsonObject(
5560
}
5661

5762
static Stream<Arguments> testMappingToJsonObjectArguments() {
58-
Map<String, Object> singleEntry = new LinkedHashMap<>();
59-
singleEntry.put("key1", "value1");
60-
61-
Map<String, Object> twoEntries = new LinkedHashMap<>();
62-
twoEntries.put("key1", "value1");
63-
twoEntries.put("key2", "value2");
64-
65-
Map<String, Object> quotedEntries = new LinkedHashMap<>();
66-
quotedEntries.put("key1", "va\"lu\"e1");
67-
quotedEntries.put("ke\"y2", "value2");
68-
6963
Map<String, Object> complexMap = new LinkedHashMap<>();
7064
complexMap.put("key1", null);
7165
complexMap.put("key2", "bar");
@@ -77,39 +71,47 @@ static Stream<Arguments> testMappingToJsonObjectArguments() {
7771
complexMap.put("key8", new UnsupportedType());
7872

7973
return Stream.of(
80-
arguments("null input", null, "{}"),
81-
arguments("empty map", new HashMap<>(), "{}"),
82-
arguments("single entry", singleEntry, "{\"key1\":\"value1\"}"),
83-
arguments("two entries", twoEntries, "{\"key1\":\"value1\",\"key2\":\"value2\"}"),
84-
arguments(
85-
"quoted entries",
86-
quotedEntries,
87-
"{\"key1\":\"va\\\"lu\\\"e1\",\"ke\\\"y2\":\"value2\"}"),
8874
arguments(
8975
"complex map",
9076
complexMap,
9177
"{\"key1\":null,\"key2\":\"bar\",\"key3\":3,\"key4\":3456789123,\"key5\":3.142,\"key6\":3.141592653589793,\"key7\":true,\"key8\":\"toString\"}"));
9278
}
9379

80+
@TableTest({
81+
"Scenario | Json ",
82+
"null | ",
83+
"null string | 'null'",
84+
"empty string | '' ",
85+
"empty object | '{}' "
86+
})
9487
@ParameterizedTest(name = "test mapping to Map from empty JSON object: {0}")
95-
@MethodSource("testMappingToMapFromEmptyJsonObjectArguments")
9688
void testMappingToMapFromEmptyJsonObject(String json) throws IOException {
9789
Map<String, Object> parsed = JsonMapper.fromJsonToMap(json);
9890
assertEquals(emptyMap(), parsed);
9991
}
10092

101-
static Stream<Arguments> testMappingToMapFromEmptyJsonObjectArguments() {
102-
return Stream.of(arguments((Object) null), arguments("null"), arguments(""), arguments("{}"));
103-
}
104-
93+
// temporary disable spotless, will open issue to fix this.
94+
// spotless:off
95+
@TableTest({
96+
"Scenario | Json ",
97+
"integer | 1 ",
98+
"array | [1, 2]"
99+
})
100+
// spotless:on
105101
@ParameterizedTest(name = "test mapping to Map from non-object JSON: {0}")
106-
@ValueSource(strings = {"1", "[1, 2]"})
107102
void testMappingToMapFromNonObjectJson(String json) {
108103
assertThrows(IOException.class, () -> JsonMapper.fromJsonToMap(json));
109104
}
110105

106+
@TableTest({
107+
"Scenario | Input | Expected ",
108+
"null input | | '[]' ",
109+
"empty list | [] | '[]' ",
110+
"single value | [value1] | '[\"value1\"]' ",
111+
"two values | [value1, value2] | '[\"value1\",\"value2\"]' ",
112+
"quoted values | [va\"lu\"e1, value2] | '[\"va\\\"lu\\\"e1\",\"value2\"]' "
113+
})
111114
@ParameterizedTest(name = "test mapping iterable to JSON array: {0}")
112-
@MethodSource("testMappingIterableToJsonArrayArguments")
113115
void testMappingIterableToJsonArray(List<String> input, String expected) throws IOException {
114116
String json = JsonMapper.toJson(input);
115117
assertEquals(expected, json);
@@ -118,18 +120,16 @@ void testMappingIterableToJsonArray(List<String> input, String expected) throws
118120
assertEquals(input != null ? input : emptyList(), parsed);
119121
}
120122

121-
static Stream<Arguments> testMappingIterableToJsonArrayArguments() {
122-
return Stream.of(
123-
arguments(null, "[]"),
124-
arguments(new ArrayList<>(), "[]"),
125-
arguments(Arrays.asList("value1"), "[\"value1\"]"),
126-
arguments(Arrays.asList("value1", "value2"), "[\"value1\",\"value2\"]"),
127-
arguments(Arrays.asList("va\"lu\"e1", "value2"), "[\"va\\\"lu\\\"e1\",\"value2\"]"));
128-
}
129-
123+
@TableTest({
124+
"Scenario | Input | Expected ",
125+
"null input | | '[]' ",
126+
"empty array | [] | '[]' ",
127+
"single element | [value1] | '[\"value1\"]' ",
128+
"two elements | [value1, value2] | '[\"value1\",\"value2\"]' ",
129+
"escaped quotes | [va\"lu\"e1, value2] | '[\"va\\\"lu\\\"e1\",\"value2\"]'"
130+
})
130131
@ParameterizedTest(name = "test mapping array to JSON array: {0}")
131-
@MethodSource("testMappingArrayToJsonArrayArguments")
132-
void testMappingArrayToJsonArray(String testCase, String[] input, String expected)
132+
void testMappingArrayToJsonArray(String ignoredScenario, String[] input, String expected)
133133
throws IOException {
134134
String json = JsonMapper.toJson(input);
135135
assertEquals(expected, json);
@@ -138,52 +138,45 @@ void testMappingArrayToJsonArray(String testCase, String[] input, String expecte
138138
assertArrayEquals(input != null ? input : new String[] {}, parsed);
139139
}
140140

141-
static Stream<Arguments> testMappingArrayToJsonArrayArguments() {
142-
return Stream.of(
143-
arguments("null input", (Object) null, "[]"),
144-
arguments("empty array", new String[] {}, "[]"),
145-
arguments("single element", new String[] {"value1"}, "[\"value1\"]"),
146-
arguments("two elements", new String[] {"value1", "value2"}, "[\"value1\",\"value2\"]"),
147-
arguments(
148-
"escaped quotes",
149-
new String[] {"va\"lu\"e1", "value2"},
150-
"[\"va\\\"lu\\\"e1\",\"value2\"]"));
151-
}
152-
141+
@TableTest({
142+
"Scenario | Json ",
143+
"null | ",
144+
"null string | 'null'",
145+
"empty string | '' ",
146+
"empty array | '[]' "
147+
})
153148
@ParameterizedTest(name = "test mapping to List from empty JSON object: {0}")
154-
@MethodSource("testMappingToListFromEmptyJsonObjectArguments")
155149
void testMappingToListFromEmptyJsonObject(String json) throws IOException {
156150
List<String> parsed = JsonMapper.fromJsonToList(json);
157151
assertEquals(emptyList(), parsed);
158152
}
159153

160-
static Stream<Arguments> testMappingToListFromEmptyJsonObjectArguments() {
161-
return Stream.of(arguments((Object) null), arguments("null"), arguments(""), arguments("[]"));
162-
}
163-
154+
@TableTest({
155+
"Scenario | input | expected ",
156+
" null value | | '' ",
157+
" empty string | '' | '' ",
158+
" \\b | '\b' | '\"\\b\"' ",
159+
" \\t | '\t' | '\"\\t\"' ",
160+
" \\f | '\f' | '\"\\f\"' ",
161+
" a | 'a' | '\"a\"' ",
162+
" / | '/' | '\"\\/\"' ",
163+
})
164164
@ParameterizedTest(name = "test mapping to JSON string: {0}")
165165
@MethodSource("testMappingToJsonStringArguments")
166-
void testMappingToJsonString(String input, String expected) {
166+
void testMappingToJsonString(@Scenario String ignoredScenario, String input, String expected) {
167167
String json = JsonMapper.toJson(input);
168168
assertEquals(expected, json);
169169
}
170170

171171
static Stream<Arguments> testMappingToJsonStringArguments() {
172172
return Stream.of(
173-
arguments((Object) null, ""),
174-
arguments("", ""),
175-
arguments(String.valueOf((char) 4096), "\"\\u1000\""),
176-
arguments(String.valueOf((char) 256), "\"\\u0100\""),
177-
arguments(String.valueOf((char) 128), "\"\\u0080\""),
178-
arguments("\b", "\"\\b\""),
179-
arguments("\t", "\"\\t\""),
180-
arguments("\n", "\"\\n\""),
181-
arguments("\f", "\"\\f\""),
182-
arguments("\r", "\"\\r\""),
183-
arguments("\"", "\"\\\"\""),
184-
arguments("/", "\"\\/\""),
185-
arguments("\\", "\"\\\\\""),
186-
arguments("a", "\"a\""));
173+
arguments("char #4096", String.valueOf((char) 4096), "\"\\u1000\""),
174+
arguments("char #256", String.valueOf((char) 256), "\"\\u0100\""),
175+
arguments("char #128", String.valueOf((char) 128), "\"\\u0080\""),
176+
arguments("\\n", "\n", "\"\\n\""),
177+
arguments("\\r", "\r", "\"\\r\""),
178+
arguments("\"", "\"", "\"\\\"\""),
179+
arguments("\\", "\\", "\"\\\\\""));
187180
}
188181

189182
private static class UnsupportedType {

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ junit5 = "5.14.1"
7070
junit-platform = "1.14.1"
7171
mockito = "4.4.0"
7272
spock = "2.4-groovy-3.0"
73+
tabletest = "1.2.0"
7374
testcontainers = "1.21.4"
7475

7576
[libraries]
@@ -161,6 +162,7 @@ objenesis = { module = "org.objenesis:objenesis", version = "3.3" } # Used by Sp
161162
spock-core = { module = "org.spockframework:spock-core", version.ref = "spock" }
162163
spock-junit4 = { module = "org.spockframework:spock-junit4", version.ref = "spock" }
163164
spock-spring = { module = "org.spockframework:spock-spring", version.ref = "spock" }
165+
tabletest = { module = "org.tabletest:tabletest-junit", version.ref = "tabletest" }
164166
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
165167
testcontainers-localstack = { module = "org.testcontainers:localstack", version.ref = "testcontainers" }
166168

gradle/repositories.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ repositories {
77
maven {
88
url project.rootProject.property("mavenRepositoryProxy")
99
allowInsecureProtocol = true
10+
content {
11+
// TODO: For unknown reasons `org.tabletest` artifacts resolved as invalid jars via `mavenRepositoryProxy`.
12+
// Build is failing with message: `error reading .gradle/caches/.../tabletest-junit-1.2.0.jar; zip END header not found`
13+
// Revisit this place once `org.tabletest` artifacts will be updated, there is a chance that issue will be fixed.
14+
// Temporary exclude it here so Gradle resolves it directly from mavenCentral().
15+
excludeGroup "org.tabletest"
16+
}
1017
}
1118
}
1219
mavenCentral()

0 commit comments

Comments
 (0)