Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ static class Parent {
CoverageReportUploader coverageReportUploader =
executionSettings.isCodeCoverageReportUploadEnabled()
? new CoverageReportUploader(
services.ciIntake, repoServices.ciTags, services.metricCollector)
services.ciIntake,
repoServices.ciTags,
services.config.getCodeCoverageFlags(),
services.metricCollector)
: null;

coverageProcessorFactory =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;
import okhttp3.MediaType;
Expand All @@ -28,26 +31,32 @@ public class CoverageReportUploader {

private final BackendApi backendApi;
private final Map<String, String> ciTags;
private final List<String> flags;
private final CiVisibilityMetricCollector metricCollector;
private final JsonAdapter<Map<String, String>> eventAdapter;
private final JsonAdapter<Map<String, Object>> eventAdapter;

public CoverageReportUploader(
BackendApi backendApi,
Map<String, String> ciTags,
List<String> flags,
CiVisibilityMetricCollector metricCollector) {
this.backendApi = backendApi;
this.ciTags = ciTags;
this.flags = Collections.unmodifiableList(new ArrayList<>(flags));
this.metricCollector = metricCollector;

Moshi moshi = new Moshi.Builder().build();
Type type = Types.newParameterizedType(Map.class, String.class, String.class);
Type type = Types.newParameterizedType(Map.class, String.class, Object.class);
eventAdapter = moshi.adapter(type);
}

public void upload(String format, InputStream reportStream) throws IOException {
Map<String, String> event = new HashMap<>(ciTags);
Map<String, Object> event = new HashMap<>(ciTags);
event.put("format", format);
event.put("type", "coverage_report");
if (!flags.isEmpty()) {
event.put("report.flags", flags);
}
String eventJson = eventAdapter.toJson(event);
RequestBody eventBody = jsonRequestBodyOf(eventJson.getBytes(StandardCharsets.UTF_8));

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package datadog.trace.civisibility.coverage.report;

import static datadog.trace.agent.test.server.http.JavaTestHttpServer.httpServer;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import datadog.communication.BackendApi;
import datadog.communication.IntakeApi;
import datadog.communication.http.HttpRetryPolicy;
import datadog.communication.http.OkHttpUtils;
import datadog.trace.agent.test.server.http.JavaTestHttpServer;
import datadog.trace.api.civisibility.telemetry.NoOpMetricCollector;
import datadog.trace.api.intake.Intake;
import datadog.trace.test.util.MultipartRequestParser;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import org.apache.commons.fileupload.FileItem;
import org.junit.jupiter.api.Test;

class CoverageReportUploaderTest {

private static final int REQUEST_TIMEOUT_MILLIS = 15_000;

private static final ObjectMapper JSON_MAPPER = new ObjectMapper();
private static final TypeReference<Map<String, Object>> EVENT_TYPE =
new TypeReference<Map<String, Object>>() {};

private static final String COVERAGE_REPORT_BODY = "report-body";
private static final String JACOCO_FORMAT = "jacoco";
private static final String CI_TAG_KEY = "ci-tag-key";
private static final String CI_TAG_VALUE = "ci-tag-value";

@Test
void uploadsCoverageReportWithoutFlags() throws IOException {
CapturedRequest request = uploadCoverageReport(Collections.emptyList());

assertEquals(3, request.event.size());
assertEquals(JACOCO_FORMAT, request.event.get("format"));
assertEquals("coverage_report", request.event.get("type"));
assertEquals(CI_TAG_VALUE, request.event.get(CI_TAG_KEY));
assertFalse(request.event.containsKey("report.flags"));
assertEquals(COVERAGE_REPORT_BODY, new String(request.coverage, StandardCharsets.UTF_8));
}

@Test
void uploadsCoverageReportWithOrderedDuplicateFlags() throws IOException {
List<String> flags = Arrays.asList("type:unit-tests", "jvm-21", "jvm-21");

CapturedRequest request = uploadCoverageReport(flags);

assertEquals(4, request.event.size());
assertEquals(JACOCO_FORMAT, request.event.get("format"));
assertEquals("coverage_report", request.event.get("type"));
assertEquals(CI_TAG_VALUE, request.event.get(CI_TAG_KEY));
assertEquals(flags, request.event.get("report.flags"));
assertEquals(COVERAGE_REPORT_BODY, new String(request.coverage, StandardCharsets.UTF_8));
}

private static CapturedRequest uploadCoverageReport(List<String> flags) throws IOException {
CapturedRequest capturedRequest = new CapturedRequest();
try (JavaTestHttpServer server =
httpServer(
s ->
s.handlers(
h ->
h.prefix(
"/api/v2/cicovreprt",
api -> {
Map<String, List<FileItem>> multipart =
MultipartRequestParser.parseRequest(
api.getRequest().getBody(),
api.getRequest().getHeader("Content-Type"));
capturedRequest.event =
JSON_MAPPER.readValue(
multipart.get("event").get(0).get(), EVENT_TYPE);
capturedRequest.coverage =
gunzip(multipart.get("coverage").get(0).get());
api.getResponse().status(200).send();
})))) {
BackendApi backendApi = givenIntakeApi(server.getAddress());
CoverageReportUploader uploader =
new CoverageReportUploader(
backendApi,
Collections.singletonMap(CI_TAG_KEY, CI_TAG_VALUE),
flags,
NoOpMetricCollector.INSTANCE);

uploader.upload(
JACOCO_FORMAT,
new ByteArrayInputStream(COVERAGE_REPORT_BODY.getBytes(StandardCharsets.UTF_8)));
}
return capturedRequest;
}

private static byte[] gunzip(byte[] compressed) throws IOException {
try (ByteArrayInputStream input = new ByteArrayInputStream(compressed);
GZIPInputStream gzip = new GZIPInputStream(input);
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
for (int readCount; (readCount = gzip.read(buffer)) != -1; ) {
output.write(buffer, 0, readCount);
}
return output.toByteArray();
}
}

private static BackendApi givenIntakeApi(URI address) {
HttpUrl intakeUrl =
HttpUrl.get(String.format("%s/api/%s/", address, Intake.CI_INTAKE.getVersion()));
HttpRetryPolicy.Factory retryPolicyFactory = new HttpRetryPolicy.Factory(5, 100, 2.0);
OkHttpClient client = OkHttpUtils.buildHttpClient(intakeUrl, REQUEST_TIMEOUT_MILLIS);
return new IntakeApi(intakeUrl, "api-key", "a-trace-id", retryPolicyFactory, client, false);
}

private static final class CapturedRequest {
private Map<String, Object> event;
private byte[] coverage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public final class CiVisibilityConfig {
public static final String GIT_COMMIT_HEAD_SHA = "git.commit.head.sha";

/* COVERAGE SETTINGS */
public static final String CODE_COVERAGE_FLAGS = "code.coverage.flags";
public static final String CIVISIBILITY_CODE_COVERAGE_ENABLED =
"civisibility.code.coverage.enabled";
public static final String CIVISIBILITY_CODE_COVERAGE_LINES_ENABLED =
Expand Down
33 changes: 33 additions & 0 deletions internal-api/src/main/java/datadog/trace/api/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,7 @@
import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_TEST_SKIPPING_ENABLED;
import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_TOTAL_FLAKY_RETRY_COUNT;
import static datadog.trace.api.config.CiVisibilityConfig.CIVISIBILITY_TRACE_SANITATION_ENABLED;
import static datadog.trace.api.config.CiVisibilityConfig.CODE_COVERAGE_FLAGS;
import static datadog.trace.api.config.CiVisibilityConfig.GIT_COMMIT_HEAD_SHA;
import static datadog.trace.api.config.CiVisibilityConfig.GIT_PULL_REQUEST_BASE_BRANCH;
import static datadog.trace.api.config.CiVisibilityConfig.GIT_PULL_REQUEST_BASE_BRANCH_SHA;
Expand Down Expand Up @@ -822,6 +823,7 @@
public class Config {

private static final Logger log = LoggerFactory.getLogger(Config.class);
private static final int MAX_CODE_COVERAGE_FLAGS = 32;

private static final Pattern COLON = Pattern.compile(":");

Expand Down Expand Up @@ -1148,6 +1150,7 @@ public static String getHostName() {
private final boolean ciVisibilityCodeCoverageEnabled;
private final Boolean ciVisibilityCoverageLinesEnabled;
private final String ciVisibilityCodeCoverageReportDumpDir;
private final List<String> codeCoverageFlags;
private final String ciVisibilityCompilerPluginVersion;
private final String ciVisibilityJacocoPluginVersion;
private final boolean ciVisibilityJacocoPluginVersionProvided;
Expand Down Expand Up @@ -2700,6 +2703,7 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment())
configProvider.getBoolean(CIVISIBILITY_CODE_COVERAGE_LINES_ENABLED);
ciVisibilityCodeCoverageReportDumpDir =
configProvider.getString(CIVISIBILITY_CODE_COVERAGE_REPORT_DUMP_DIR);
codeCoverageFlags = parseCodeCoverageFlags(configProvider.getList(CODE_COVERAGE_FLAGS));
ciVisibilityCompilerPluginVersion =
configProvider.getString(
CIVISIBILITY_COMPILER_PLUGIN_VERSION, DEFAULT_CIVISIBILITY_COMPILER_PLUGIN_VERSION);
Expand Down Expand Up @@ -4399,6 +4403,10 @@ public String getCiVisibilityCodeCoverageReportDumpDir() {
return ciVisibilityCodeCoverageReportDumpDir;
}

public List<String> getCodeCoverageFlags() {
return codeCoverageFlags;
}

public String getCiVisibilityCompilerPluginVersion() {
return ciVisibilityCompilerPluginVersion;
}
Expand Down Expand Up @@ -6081,6 +6089,29 @@ private static Set<String> parseStringIntoSetOfNonEmptyStrings(
return Collections.unmodifiableSet(result);
}

private static List<String> parseCodeCoverageFlags(List<String> configuredFlags) {
if (configuredFlags.isEmpty()) {
return Collections.emptyList();
}

List<String> flags = new ArrayList<>(configuredFlags.size());
for (String configuredFlag : configuredFlags) {
if (!configuredFlag.isEmpty()) {
flags.add(configuredFlag);
}
}

if (flags.size() > MAX_CODE_COVERAGE_FLAGS) {
log.warn(
"Cannot apply {} code coverage report flags: the maximum supported number is {}. The report will be uploaded without flags.",
flags.size(),
MAX_CODE_COVERAGE_FLAGS);
return Collections.emptyList();
}

return Collections.unmodifiableList(flags);
}

private static <T> Set<T> convertStringSetToSet(
String setting, final Set<String> input, Function<String, T> mapper) {
if (input.isEmpty()) {
Expand Down Expand Up @@ -6269,6 +6300,8 @@ public String toString() {
+ experimentalFeaturesEnabled
+ ", integrationSynapseLegacyOperationName="
+ integrationSynapseLegacyOperationName
+ ", codeCoverageFlags="
+ codeCoverageFlags
+ ", writerType='"
+ writerType
+ '\''
Expand Down
Loading