Skip to content

Commit 4d1bd27

Browse files
feat: unit tests
1 parent 827b1c4 commit 4d1bd27

6 files changed

Lines changed: 939 additions & 9 deletions

File tree

dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/FileBasedConfigurationApi.java

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@
22

33
import com.squareup.moshi.JsonAdapter;
44
import com.squareup.moshi.Moshi;
5-
import com.squareup.moshi.Types;
65
import datadog.trace.api.civisibility.config.Configurations;
76
import datadog.trace.api.civisibility.config.TestFQN;
87
import datadog.trace.api.civisibility.config.TestIdentifier;
98
import datadog.trace.api.civisibility.config.TestMetadata;
109
import java.io.IOException;
11-
import java.lang.reflect.ParameterizedType;
1210
import java.nio.file.Path;
1311
import java.util.BitSet;
1412
import java.util.Collection;
@@ -70,11 +68,7 @@ public FileBasedConfigurationApi(
7068
settingsAdapter = moshi.adapter(SettingsEnvelope.class);
7169
knownTestsAdapter = moshi.adapter(KnownTestsEnvelope.class);
7270
testManagementAdapter = moshi.adapter(TestManagementEnvelope.class);
73-
74-
ParameterizedType testIdentifiersType =
75-
Types.newParameterizedTypeWithOwner(
76-
FileBasedConfigurationApi.class, TestIdentifiersEnvelope.class);
77-
testIdentifiersAdapter = moshi.adapter(testIdentifiersType);
71+
testIdentifiersAdapter = moshi.adapter(TestIdentifiersEnvelope.class);
7872
}
7973

8074
@Override
@@ -278,8 +272,6 @@ private Map<TestSetting, Map<String, Collection<TestFQN>>> parseTestManagementTe
278272
return result;
279273
}
280274

281-
// --- DTOs ---
282-
283275
// Settings envelope: { "data": { "attributes": CiVisibilitySettings } }
284276
static final class SettingsEnvelope {
285277
DataDto<CiVisibilitySettings> data;
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
package datadog.trace.civisibility.config;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertNull;
6+
import static org.junit.jupiter.api.Assertions.assertSame;
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import datadog.trace.api.civisibility.config.TestFQN;
10+
import datadog.trace.api.civisibility.config.TestIdentifier;
11+
import datadog.trace.api.civisibility.config.TestMetadata;
12+
import freemarker.template.Configuration;
13+
import freemarker.template.Template;
14+
import java.io.IOException;
15+
import java.io.StringWriter;
16+
import java.nio.charset.StandardCharsets;
17+
import java.nio.file.Files;
18+
import java.nio.file.Path;
19+
import java.util.Arrays;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.HashMap;
23+
import java.util.HashSet;
24+
import java.util.Map;
25+
import org.junit.jupiter.api.Test;
26+
import org.junit.jupiter.api.io.TempDir;
27+
28+
/**
29+
* Tests {@link FileBasedConfigurationApi} against the same response fixtures used by the HTTP
30+
* variant ({@code ConfigurationApiImplTest}). Sharing the {@code *-response.ftl} templates keeps
31+
* both code paths aligned — any change to the backend payload shape only needs to be reflected in
32+
* one place.
33+
*/
34+
class FileBasedConfigurationApiTest {
35+
36+
private static final String FIXTURE_DIR = "/datadog/trace/civisibility/config/";
37+
38+
private static final TracerEnvironment ENV =
39+
TracerEnvironment.builder()
40+
.service("foo")
41+
.env("foo_env")
42+
.repositoryUrl("https://github.com/DataDog/foo")
43+
.branch("prod")
44+
.sha("d64185e45d1722ab3a53c45be47accae")
45+
.commitMessage("full commit message")
46+
.build();
47+
48+
@Test
49+
void returnsDefaultsWhenAllPathsAreNull() throws IOException {
50+
FileBasedConfigurationApi api = new FileBasedConfigurationApi(null, null, null, null, null);
51+
52+
assertSame(CiVisibilitySettings.DEFAULT, api.getSettings(ENV));
53+
assertSame(SkippableTests.EMPTY, api.getSkippableTests(ENV));
54+
assertEquals(0, api.getFlakyTestsByModule(ENV).size());
55+
assertEquals(0, api.getKnownTestsByModule(ENV).size());
56+
assertEquals(0, api.getTestManagementTestsByModule(ENV, null, null).size());
57+
}
58+
59+
@Test
60+
void parsesSettings(@TempDir Path tmp) throws IOException {
61+
CiVisibilitySettings expected =
62+
new CiVisibilitySettings(
63+
true,
64+
true,
65+
true,
66+
true,
67+
true,
68+
true,
69+
true,
70+
true,
71+
true,
72+
EarlyFlakeDetectionSettings.DEFAULT,
73+
TestManagementSettings.DEFAULT,
74+
"main",
75+
false);
76+
Map<String, Object> data = new HashMap<>();
77+
data.put("settings", expected);
78+
Path file = renderToFile(tmp, "settings-response.ftl", "settings.json", data);
79+
80+
FileBasedConfigurationApi api = new FileBasedConfigurationApi(file, null, null, null, null);
81+
82+
assertEquals(expected, api.getSettings(ENV));
83+
}
84+
85+
@Test
86+
void parsesSettingsWithEarlyFlakeDetectionAndTestManagement(@TempDir Path tmp)
87+
throws IOException {
88+
CiVisibilitySettings expected =
89+
new CiVisibilitySettings(
90+
false,
91+
true,
92+
false,
93+
true,
94+
false,
95+
true,
96+
false,
97+
false,
98+
true,
99+
new EarlyFlakeDetectionSettings(
100+
true, Arrays.asList(new ExecutionsByDuration(1000, 3)), 10),
101+
new TestManagementSettings(true, 10),
102+
"master",
103+
false);
104+
Map<String, Object> data = new HashMap<>();
105+
data.put("settings", expected);
106+
Path file = renderToFile(tmp, "settings-response.ftl", "settings.json", data);
107+
108+
FileBasedConfigurationApi api = new FileBasedConfigurationApi(file, null, null, null, null);
109+
110+
assertEquals(expected, api.getSettings(ENV));
111+
}
112+
113+
@Test
114+
void parsesSkippableTests(@TempDir Path tmp) throws IOException {
115+
Path file =
116+
renderToFile(tmp, "skippable-response.ftl", "skippable.json", Collections.emptyMap());
117+
118+
FileBasedConfigurationApi api = new FileBasedConfigurationApi(null, file, null, null, null);
119+
SkippableTests result = api.getSkippableTests(ENV);
120+
121+
Map<String, Map<TestIdentifier, TestMetadata>> expected = new HashMap<>();
122+
Map<TestIdentifier, TestMetadata> bundleA = new HashMap<>();
123+
bundleA.put(new TestIdentifier("suite-a", "name-a", "parameters-a"), new TestMetadata(true));
124+
expected.put("testBundle-a", bundleA);
125+
Map<TestIdentifier, TestMetadata> bundleB = new HashMap<>();
126+
bundleB.put(new TestIdentifier("suite-b", "name-b", null), new TestMetadata(false));
127+
expected.put("testBundle-b", bundleB);
128+
129+
assertEquals(expected, result.getIdentifiersByModule());
130+
assertEquals("11223344", result.getCorrelationId());
131+
// coverage bitmaps encoded in the template
132+
assertEquals(3, result.getCoveredLinesByRelativeSourcePath().size());
133+
assertTrue(
134+
result.getCoveredLinesByRelativeSourcePath().containsKey("src/main/java/Calculator.java"));
135+
assertTrue(
136+
result.getCoveredLinesByRelativeSourcePath().containsKey("src/main/java/utils/Math.java"));
137+
assertTrue(
138+
result
139+
.getCoveredLinesByRelativeSourcePath()
140+
.containsKey("src/test/java/CalculatorTest.java"));
141+
}
142+
143+
@Test
144+
void parsesFlakyTests(@TempDir Path tmp) throws IOException {
145+
Path file = renderToFile(tmp, "flaky-response.ftl", "flaky.json", Collections.emptyMap());
146+
147+
FileBasedConfigurationApi api = new FileBasedConfigurationApi(null, null, file, null, null);
148+
Map<String, Collection<TestFQN>> result = api.getFlakyTestsByModule(ENV);
149+
150+
Map<String, Collection<TestFQN>> expected = new HashMap<>();
151+
expected.put("testBundle-a", new HashSet<>(Arrays.asList(new TestFQN("suite-a", "name-a"))));
152+
expected.put("testBundle-b", new HashSet<>(Arrays.asList(new TestFQN("suite-b", "name-b"))));
153+
assertEquals(expected, result);
154+
}
155+
156+
@Test
157+
void parsesKnownTests(@TempDir Path tmp) throws IOException {
158+
Path file = renderToFile(tmp, "known-tests-response.ftl", "known.json", Collections.emptyMap());
159+
160+
FileBasedConfigurationApi api = new FileBasedConfigurationApi(null, null, null, file, null);
161+
Map<String, Collection<TestFQN>> result = api.getKnownTestsByModule(ENV);
162+
163+
assertNotNull(result);
164+
assertEquals(2, result.size());
165+
Collection<TestFQN> bundleA = result.get("test-bundle-a");
166+
assertTrue(bundleA.contains(new TestFQN("test-suite-a", "test-name-1")));
167+
assertTrue(bundleA.contains(new TestFQN("test-suite-a", "test-name-2")));
168+
assertTrue(bundleA.contains(new TestFQN("test-suite-b", "another-test-name-1")));
169+
assertTrue(bundleA.contains(new TestFQN("test-suite-b", "test-name-2")));
170+
Collection<TestFQN> bundleN = result.get("test-bundle-N");
171+
assertTrue(bundleN.contains(new TestFQN("test-suite-M", "test-name-1")));
172+
assertTrue(bundleN.contains(new TestFQN("test-suite-M", "test-name-2")));
173+
}
174+
175+
@Test
176+
void knownTestsReturnsNullWhenResponseHasNoTests(@TempDir Path tmp) throws IOException {
177+
// Matches the backend API contract: empty-but-present known-tests payload → null
178+
Path file = writeText(tmp, "empty-known.json", "{\"data\":{\"attributes\":{\"tests\":{}}}}");
179+
180+
FileBasedConfigurationApi api = new FileBasedConfigurationApi(null, null, null, file, null);
181+
182+
assertNull(api.getKnownTestsByModule(ENV));
183+
}
184+
185+
@Test
186+
void parsesTestManagement(@TempDir Path tmp) throws IOException {
187+
Path file =
188+
renderToFile(
189+
tmp, "test-management-tests-response.ftl", "mgmt.json", Collections.emptyMap());
190+
191+
FileBasedConfigurationApi api = new FileBasedConfigurationApi(null, null, null, null, file);
192+
Map<TestSetting, Map<String, Collection<TestFQN>>> result =
193+
api.getTestManagementTestsByModule(ENV, ENV.getSha(), ENV.getCommitMessage());
194+
195+
Map<String, Collection<TestFQN>> quarantined = new HashMap<>();
196+
quarantined.put(
197+
"module-a",
198+
new HashSet<>(
199+
Arrays.asList(new TestFQN("suite-a", "test-a"), new TestFQN("suite-b", "test-c"))));
200+
quarantined.put("module-b", new HashSet<>(Arrays.asList(new TestFQN("suite-c", "test-e"))));
201+
assertEquals(quarantined, result.get(TestSetting.QUARANTINED));
202+
203+
Map<String, Collection<TestFQN>> disabled = new HashMap<>();
204+
disabled.put("module-a", new HashSet<>(Arrays.asList(new TestFQN("suite-a", "test-b"))));
205+
disabled.put(
206+
"module-b",
207+
new HashSet<>(
208+
Arrays.asList(new TestFQN("suite-c", "test-d"), new TestFQN("suite-c", "test-f"))));
209+
assertEquals(disabled, result.get(TestSetting.DISABLED));
210+
211+
Map<String, Collection<TestFQN>> attemptToFix = new HashMap<>();
212+
attemptToFix.put("module-a", new HashSet<>(Arrays.asList(new TestFQN("suite-b", "test-c"))));
213+
attemptToFix.put(
214+
"module-b",
215+
new HashSet<>(
216+
Arrays.asList(new TestFQN("suite-c", "test-d"), new TestFQN("suite-c", "test-e"))));
217+
assertEquals(attemptToFix, result.get(TestSetting.ATTEMPT_TO_FIX));
218+
}
219+
220+
@Test
221+
void propagatesIOExceptionForMissingFile(@TempDir Path tmp) {
222+
Path missing = tmp.resolve("does-not-exist.json");
223+
FileBasedConfigurationApi api = new FileBasedConfigurationApi(missing, null, null, null, null);
224+
225+
assertThrowsIO(() -> api.getSettings(ENV));
226+
}
227+
228+
// --- helpers ---
229+
230+
private static Path renderToFile(
231+
Path dir, String templateName, String outputName, Map<String, Object> data)
232+
throws IOException {
233+
String content = render(templateName, data);
234+
return writeText(dir, outputName, content);
235+
}
236+
237+
private static Path writeText(Path dir, String name, String content) throws IOException {
238+
Path p = dir.resolve(name);
239+
Files.write(p, content.getBytes(StandardCharsets.UTF_8));
240+
return p;
241+
}
242+
243+
private static final Configuration FREEMARKER;
244+
245+
static {
246+
FREEMARKER = new Configuration(Configuration.VERSION_2_3_30);
247+
FREEMARKER.setClassLoaderForTemplateLoading(
248+
FileBasedConfigurationApiTest.class.getClassLoader(), "");
249+
FREEMARKER.setDefaultEncoding("UTF-8");
250+
FREEMARKER.setLogTemplateExceptions(false);
251+
FREEMARKER.setWrapUncheckedExceptions(true);
252+
FREEMARKER.setFallbackOnNullLoopVariable(false);
253+
FREEMARKER.setNumberFormat("0.######");
254+
}
255+
256+
private static String render(String templateName, Map<String, Object> data) throws IOException {
257+
Template template = FREEMARKER.getTemplate(FIXTURE_DIR + templateName);
258+
StringWriter out = new StringWriter();
259+
try {
260+
template.process(data, out);
261+
} catch (freemarker.template.TemplateException e) {
262+
throw new IOException("Failed to render template " + templateName, e);
263+
}
264+
return out.toString();
265+
}
266+
267+
private static void assertThrowsIO(ThrowingRunnable runnable) {
268+
try {
269+
runnable.run();
270+
} catch (IOException expected) {
271+
return;
272+
} catch (Throwable t) {
273+
throw new AssertionError("Expected IOException but got " + t, t);
274+
}
275+
throw new AssertionError("Expected IOException but none thrown");
276+
}
277+
278+
@FunctionalInterface
279+
private interface ThrowingRunnable {
280+
void run() throws IOException;
281+
}
282+
}

0 commit comments

Comments
 (0)