diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityRepoServices.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityRepoServices.java index 228661690c1..6a269fa1964 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityRepoServices.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityRepoServices.java @@ -8,6 +8,7 @@ import datadog.trace.civisibility.ci.CIInfo; import datadog.trace.civisibility.ci.CIProviderInfo; import datadog.trace.civisibility.ci.CITagsProvider; +import datadog.trace.civisibility.ci.PullRequestInfo; import datadog.trace.civisibility.codeowners.Codeowners; import datadog.trace.civisibility.codeowners.CodeownersProvider; import datadog.trace.civisibility.codeowners.NoCodeowners; @@ -22,6 +23,7 @@ import datadog.trace.civisibility.git.tree.GitDataApi; import datadog.trace.civisibility.git.tree.GitDataUploader; import datadog.trace.civisibility.git.tree.GitDataUploaderImpl; +import datadog.trace.civisibility.git.tree.GitRepoUnshallow; import datadog.trace.civisibility.ipc.ExecutionSettingsRequest; import datadog.trace.civisibility.ipc.ExecutionSettingsResponse; import datadog.trace.civisibility.ipc.SignalClient; @@ -31,6 +33,8 @@ import datadog.trace.civisibility.source.SourcePathResolver; import datadog.trace.civisibility.source.index.RepoIndexProvider; import datadog.trace.civisibility.source.index.RepoIndexSourcePathResolver; +import datadog.trace.util.Strings; +import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; @@ -59,16 +63,26 @@ public class CiVisibilityRepoServices { ciProvider = ciProviderInfo.getProvider(); CIInfo ciInfo = ciProviderInfo.buildCIInfo(); - repoRoot = ciInfo.getNormalizedCiWorkspace(); - moduleName = getModuleName(services.config, path, ciInfo); - ciTags = new CITagsProvider().getCiTags(ciInfo); + PullRequestInfo pullRequestInfo = ciProviderInfo.buildPullRequestInfo(); + + if (pullRequestInfo.isNotEmpty()) { + LOGGER.info("PR detected: {}", pullRequestInfo); + } + + repoRoot = appendSlashIfNeeded(getRepoRoot(ciInfo, services.gitClientFactory)); + moduleName = getModuleName(services.config, repoRoot, path); + ciTags = new CITagsProvider().getCiTags(ciInfo, pullRequestInfo); + + GitClient gitClient = services.gitClientFactory.create(repoRoot); + GitRepoUnshallow gitRepoUnshallow = new GitRepoUnshallow(services.config, gitClient); gitDataUploader = buildGitDataUploader( services.config, services.metricCollector, services.gitInfoProvider, - services.gitClientFactory, + gitClient, + gitRepoUnshallow, services.backendApi, repoRoot); repoIndexProvider = services.repoIndexProviderFactory.create(repoRoot); @@ -84,18 +98,49 @@ public class CiVisibilityRepoServices { services.config, services.metricCollector, services.backendApi, + gitClient, + gitRepoUnshallow, gitDataUploader, + pullRequestInfo, repoRoot); } } - static String getModuleName(Config config, Path path, CIInfo ciInfo) { + private static String getRepoRoot(CIInfo ciInfo, GitClient.Factory gitClientFactory) { + String ciWorkspace = ciInfo.getNormalizedCiWorkspace(); + if (Strings.isNotBlank(ciWorkspace)) { + return ciWorkspace; + + } else { + try { + return gitClientFactory.create(".").getRepoRoot(); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOGGER.error("Interrupted while getting repo root", e); + return null; + + } catch (Exception e) { + LOGGER.error("Error while getting repo root", e); + return null; + } + } + } + + private static String appendSlashIfNeeded(String repoRoot) { + if (repoRoot != null && !repoRoot.endsWith(File.separator)) { + return repoRoot + File.separator; + } else { + return repoRoot; + } + } + + static String getModuleName(Config config, String repoRoot, Path path) { // if parent process is instrumented, it will provide build system's module name String parentModuleName = config.getCiVisibilityModuleName(); if (parentModuleName != null) { return parentModuleName; } - String repoRoot = ciInfo.getNormalizedCiWorkspace(); if (repoRoot != null && path.startsWith(repoRoot)) { String relativePath = Paths.get(repoRoot).relativize(path).toString(); if (!relativePath.isEmpty()) { @@ -126,7 +171,10 @@ private static ExecutionSettingsFactory buildExecutionSettingsFactory( Config config, CiVisibilityMetricCollector metricCollector, BackendApi backendApi, + GitClient gitClient, + GitRepoUnshallow gitRepoUnshallow, GitDataUploader gitDataUploader, + PullRequestInfo pullRequestInfo, String repoRoot) { ConfigurationApi configurationApi; if (backendApi == null) { @@ -138,7 +186,14 @@ private static ExecutionSettingsFactory buildExecutionSettingsFactory( } ExecutionSettingsFactoryImpl factory = - new ExecutionSettingsFactoryImpl(config, configurationApi, gitDataUploader, repoRoot); + new ExecutionSettingsFactoryImpl( + config, + configurationApi, + gitClient, + gitRepoUnshallow, + gitDataUploader, + pullRequestInfo, + repoRoot); if (processHierarchy.isHeadless()) { return factory; } else { @@ -150,7 +205,8 @@ private static GitDataUploader buildGitDataUploader( Config config, CiVisibilityMetricCollector metricCollector, GitInfoProvider gitInfoProvider, - GitClient.Factory gitClientFactory, + GitClient gitClient, + GitRepoUnshallow gitRepoUnshallow, BackendApi backendApi, String repoRoot) { if (!config.isCiVisibilityGitUploadEnabled()) { @@ -171,9 +227,15 @@ private static GitDataUploader buildGitDataUploader( String remoteName = config.getCiVisibilityGitRemoteName(); GitDataApi gitDataApi = new GitDataApi(backendApi, metricCollector); - GitClient gitClient = gitClientFactory.create(repoRoot); return new GitDataUploaderImpl( - config, metricCollector, gitDataApi, gitClient, gitInfoProvider, repoRoot, remoteName); + config, + metricCollector, + gitDataApi, + gitClient, + gitRepoUnshallow, + gitInfoProvider, + repoRoot, + remoteName); } private static SourcePathResolver buildSourcePathResolver( diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityServices.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityServices.java index 6e30c5af715..c2b3f22fa20 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityServices.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilityServices.java @@ -8,8 +8,11 @@ import datadog.communication.ddagent.SharedCommunicationObjects; import datadog.communication.http.HttpRetryPolicy; import datadog.communication.http.OkHttpUtils; +import datadog.communication.util.IOUtils; import datadog.trace.api.Config; +import datadog.trace.api.civisibility.telemetry.CiVisibilityCountMetric; import datadog.trace.api.civisibility.telemetry.CiVisibilityMetricCollector; +import datadog.trace.api.civisibility.telemetry.tag.Command; import datadog.trace.api.git.GitInfoProvider; import datadog.trace.civisibility.ci.CIProviderInfoFactory; import datadog.trace.civisibility.ci.env.CiEnvironment; @@ -22,12 +25,16 @@ import datadog.trace.civisibility.git.CIProviderGitInfoBuilder; import datadog.trace.civisibility.git.GitClientGitInfoBuilder; import datadog.trace.civisibility.git.tree.GitClient; +import datadog.trace.civisibility.git.tree.NoOpGitClient; +import datadog.trace.civisibility.git.tree.ShellGitClient; import datadog.trace.civisibility.ipc.SignalClient; import datadog.trace.civisibility.source.BestEffortLinesResolver; import datadog.trace.civisibility.source.ByteCodeLinesResolver; import datadog.trace.civisibility.source.CompilerAidedLinesResolver; import datadog.trace.civisibility.source.LinesResolver; import datadog.trace.civisibility.source.index.*; +import datadog.trace.civisibility.utils.ShellCommandExecutor; +import java.io.File; import java.lang.reflect.Type; import java.net.InetSocketAddress; import java.nio.file.FileSystem; @@ -35,11 +42,11 @@ import java.nio.file.Path; import java.util.Collections; import java.util.Map; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -78,7 +85,7 @@ public class CiVisibilityServices { this.backendApi = new BackendApiFactory(config, sco).createBackendApi(BackendApiFactory.Intake.API); this.jvmInfoFactory = new CachingJvmInfoFactory(config, new JvmInfoFactoryImpl()); - this.gitClientFactory = new GitClient.Factory(config, metricCollector); + this.gitClientFactory = buildGitClientFactory(config, metricCollector); CiEnvironment environment = buildCiEnvironment(config, sco); this.ciProviderInfoFactory = new CIProviderInfoFactory(config, environment); @@ -111,7 +118,30 @@ public class CiVisibilityServices { } } - @NotNull + private static GitClient.Factory buildGitClientFactory( + Config config, CiVisibilityMetricCollector metricCollector) { + if (!config.isCiVisibilityGitClientEnabled()) { + return r -> NoOpGitClient.INSTANCE; + } + try { + ShellCommandExecutor shellCommandExecutor = + new ShellCommandExecutor(new File("."), config.getCiVisibilityGitCommandTimeoutMillis()); + String gitVersion = shellCommandExecutor.executeCommand(IOUtils::readFully, "git", "version"); + logger.debug("Detected git executable version {}", gitVersion); + return new ShellGitClient.Factory(config, metricCollector); + + } catch (Exception e) { + metricCollector.add( + CiVisibilityCountMetric.GIT_COMMAND_ERRORS, + 1, + Command.OTHER, + ShellCommandExecutor.getExitCode(e)); + logger.info("No git executable detected, some features will not be available"); + return r -> NoOpGitClient.INSTANCE; + } + } + + @Nonnull private static CiEnvironment buildCiEnvironment(Config config, SharedCommunicationObjects sco) { String remoteEnvVarsProviderUrl = config.getCiVisibilityRemoteEnvVarsProviderUrl(); if (remoteEnvVarsProviderUrl != null) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilitySystem.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilitySystem.java index 688f19f9892..495b83210ec 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilitySystem.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/CiVisibilitySystem.java @@ -238,7 +238,11 @@ private static TestFrameworkSession.Factory childTestFrameworkSessionFactory( new TestDecoratorImpl(component, sessionName, testCommand, repoServices.ciTags); ExecutionStrategy executionStrategy = - new ExecutionStrategy(services.config, executionSettings); + new ExecutionStrategy( + services.config, + executionSettings, + repoServices.sourcePathResolver, + services.linesResolver); return new ProxyTestSession( services.processHierarchy.parentProcessModuleContext, @@ -268,7 +272,11 @@ private static TestFrameworkSession.Factory headlessTestFrameworkEssionFactory( new TestDecoratorImpl(component, sessionName, projectName, repoServices.ciTags); ExecutionStrategy executionStrategy = - new ExecutionStrategy(services.config, executionSettings); + new ExecutionStrategy( + services.config, + executionSettings, + repoServices.sourcePathResolver, + services.linesResolver); return new HeadlessTestSession( projectName, startTime, diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AppVeyorInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AppVeyorInfo.java index d5e97d581b3..3657766e312 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AppVeyorInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AppVeyorInfo.java @@ -9,6 +9,7 @@ import datadog.trace.api.git.GitInfo; import datadog.trace.api.git.PersonInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; +import javax.annotation.Nonnull; class AppVeyorInfo implements CIProviderInfo { @@ -79,6 +80,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String buildGitBranch(final String repoProvider) { if ("github".equals(repoProvider)) { String branch = environment.get(APPVEYOR_PULL_REQUEST_HEAD_REPO_BRANCH); diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AwsCodePipelineInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AwsCodePipelineInfo.java index 72d672e0528..8635d4a136d 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AwsCodePipelineInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AwsCodePipelineInfo.java @@ -3,6 +3,7 @@ import datadog.trace.api.civisibility.telemetry.tag.Provider; import datadog.trace.api.git.GitInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; +import javax.annotation.Nonnull; class AwsCodePipelineInfo implements CIProviderInfo { @@ -35,6 +36,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + @Override public Provider getProvider() { return Provider.AWS; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AzurePipelinesInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AzurePipelinesInfo.java index 708a6bea6be..6ca4907f220 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AzurePipelinesInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/AzurePipelinesInfo.java @@ -11,6 +11,7 @@ import datadog.trace.api.git.GitInfo; import datadog.trace.api.git.PersonInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; +import javax.annotation.Nonnull; class AzurePipelinesInfo implements CIProviderInfo { @@ -81,6 +82,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String buildGitBranch() { String gitBranchOrTag = getGitBranchOrTag(); if (!isTagReference(gitBranchOrTag)) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BitBucketInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BitBucketInfo.java index 63b9eace1e5..bbda148a978 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BitBucketInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BitBucketInfo.java @@ -10,6 +10,7 @@ import datadog.trace.api.git.GitInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; import datadog.trace.util.Strings; +import javax.annotation.Nonnull; class BitBucketInfo implements CIProviderInfo { @@ -70,6 +71,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String buildPipelineUrl(final String repo, final String number) { return String.format( "https://bitbucket.org/%s/addon/pipelines/home#!/results/%s", repo, number); diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BitriseInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BitriseInfo.java index b5af995835a..080b81fea25 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BitriseInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BitriseInfo.java @@ -10,6 +10,7 @@ import datadog.trace.api.git.GitInfo; import datadog.trace.api.git.PersonInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; +import javax.annotation.Nonnull; class BitriseInfo implements CIProviderInfo { @@ -66,6 +67,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String buildGitCommit() { final String fromCommit = environment.get(BITRISE_GIT_PR_COMMIT); if (fromCommit != null && !fromCommit.isEmpty()) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuddyInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuddyInfo.java index 8118c9c22c8..4ae853c55e4 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuddyInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuddyInfo.java @@ -9,6 +9,7 @@ import datadog.trace.api.git.GitInfo; import datadog.trace.api.git.PersonInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; +import javax.annotation.Nonnull; class BuddyInfo implements CIProviderInfo { @@ -58,6 +59,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String getPipelineId(String pipelineNumber) { String pipelineId = environment.get(BUDDY_PIPELINE_ID); if (pipelineId == null) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuildkiteInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuildkiteInfo.java index a558543e791..e7cc93d4475 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuildkiteInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/BuildkiteInfo.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import javax.annotation.Nonnull; class BuildkiteInfo implements CIProviderInfo { @@ -74,6 +75,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String buildCiNodeLabels() { List labels = new ArrayList<>(); for (Map.Entry e : environment.get().entrySet()) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CIProviderInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CIProviderInfo.java index 5c189dfe5d5..7e617412185 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CIProviderInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CIProviderInfo.java @@ -2,6 +2,7 @@ import datadog.trace.api.civisibility.telemetry.tag.Provider; import datadog.trace.api.git.GitInfo; +import javax.annotation.Nonnull; public interface CIProviderInfo { @@ -9,5 +10,8 @@ public interface CIProviderInfo { CIInfo buildCIInfo(); + @Nonnull + PullRequestInfo buildPullRequestInfo(); + Provider getProvider(); } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CITagsProvider.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CITagsProvider.java index 469cc5762de..a41bbb28af9 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CITagsProvider.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CITagsProvider.java @@ -21,7 +21,7 @@ public CITagsProvider() { this.gitInfoProvider = gitInfoProvider; } - public Map getCiTags(CIInfo ciInfo) { + public Map getCiTags(CIInfo ciInfo, PullRequestInfo pullRequestInfo) { String repoRoot = ciInfo.getNormalizedCiWorkspace(); GitInfo gitInfo = gitInfoProvider.getGitInfo(repoRoot); @@ -39,6 +39,9 @@ public Map getCiTags(CIInfo ciInfo) { .withCiNodeLabels(ciInfo.getCiNodeLabels()) .withCiEnvVars(ciInfo.getCiEnvVars()) .withAdditionalTags(ciInfo.getAdditionalTags()) + .withPullRequestBaseBranch(pullRequestInfo) + .withPullRequestBaseBranchSha(pullRequestInfo) + .withGitCommitHeadSha(pullRequestInfo) .withGitRepositoryUrl(gitInfo) .withGitCommit(gitInfo) .withGitBranch(gitInfo) @@ -118,6 +121,20 @@ public CITagsBuilder withAdditionalTags(final Map additionalTags return this; } + public CITagsBuilder withPullRequestBaseBranch(final PullRequestInfo pullRequestInfo) { + return putTagValue( + Tags.GIT_PULL_REQUEST_BASE_BRANCH, pullRequestInfo.getPullRequestBaseBranch()); + } + + public CITagsBuilder withPullRequestBaseBranchSha(final PullRequestInfo pullRequestInfo) { + return putTagValue( + Tags.GIT_PULL_REQUEST_BASE_BRANCH_SHA, pullRequestInfo.getPullRequestBaseBranchSha()); + } + + public CITagsBuilder withGitCommitHeadSha(final PullRequestInfo pullRequestInfo) { + return putTagValue(Tags.GIT_COMMIT_HEAD_SHA, pullRequestInfo.getGitCommitHeadSha()); + } + public CITagsBuilder withGitRepositoryUrl(final GitInfo gitInfo) { return putTagValue(Tags.GIT_REPOSITORY_URL, gitInfo.getRepositoryURL()); } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CircleCIInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CircleCIInfo.java index f18fab7c949..9abcc07f863 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CircleCIInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CircleCIInfo.java @@ -9,6 +9,7 @@ import datadog.trace.api.git.CommitInfo; import datadog.trace.api.git.GitInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; +import javax.annotation.Nonnull; class CircleCIInfo implements CIProviderInfo { @@ -56,6 +57,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String buildPipelineUrl(final String pipelineId) { if (pipelineId == null) { return null; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CodefreshInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CodefreshInfo.java index 5dcdbe1abcb..6f107913da0 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CodefreshInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/CodefreshInfo.java @@ -9,6 +9,7 @@ import datadog.trace.api.git.GitInfo; import datadog.trace.api.git.PersonInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; +import javax.annotation.Nonnull; public class CodefreshInfo implements CIProviderInfo { public static final String CODEFRESH = "CF_BUILD_ID"; @@ -40,6 +41,12 @@ public GitInfo buildCIGitInfo() { environment.get(CF_COMMIT_MESSAGE))); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String buildGitBranch() { String gitBranchOrTag = getGitBranchOrTag(); if (!isTagReference(gitBranchOrTag)) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/GitLabInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/GitLabInfo.java index ce99e955371..17ceedb61cd 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/GitLabInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/GitLabInfo.java @@ -12,6 +12,7 @@ import datadog.trace.api.git.PersonInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; import de.thetaphi.forbiddenapis.SuppressForbidden; +import javax.annotation.Nonnull; @SuppressForbidden class GitLabInfo implements CIProviderInfo { @@ -38,6 +39,12 @@ class GitLabInfo implements CIProviderInfo { public static final String GITLAB_GIT_COMMIT_TIMESTAMP = "CI_COMMIT_TIMESTAMP"; public static final String GITLAB_CI_RUNNER_ID = "CI_RUNNER_ID"; public static final String GITLAB_CI_RUNNER_TAGS = "CI_RUNNER_TAGS"; + public static final String GITLAB_PULL_REQUEST_BASE_BRANCH = + "CI_MERGE_REQUEST_TARGET_BRANCH_NAME"; + public static final String GITLAB_PULL_REQUEST_BASE_BRANCH_SHA = + "CI_MERGE_REQUEST_TARGET_BRANCH_SHA"; + public static final String GITLAB_PULL_REQUEST_COMMIT_HEAD_SHA = + "CI_MERGE_REQUEST_SOURCE_BRANCH_SHA"; private final CiEnvironment environment; @@ -76,6 +83,15 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return new PullRequestInfo( + environment.get(GITLAB_PULL_REQUEST_BASE_BRANCH), + environment.get(GITLAB_PULL_REQUEST_BASE_BRANCH_SHA), + environment.get(GITLAB_PULL_REQUEST_COMMIT_HEAD_SHA)); + } + private PersonInfo buildGitCommitAuthor() { final String gitAuthor = environment.get(GITLAB_GIT_COMMIT_AUTHOR); if (gitAuthor == null || gitAuthor.isEmpty()) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/GithubActionsInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/GithubActionsInfo.java index ee38ed4aefc..a2b39fac978 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/GithubActionsInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/GithubActionsInfo.java @@ -18,8 +18,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.HashMap; import java.util.Map; +import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,10 +44,6 @@ class GithubActionsInfo implements CIProviderInfo { public static final String GITHUB_BASE_REF = "GITHUB_BASE_REF"; public static final String GITHUB_EVENT_PATH = "GITHUB_EVENT_PATH"; - public static final String GIT_PULL_REQUEST_BASE_BRANCH = "git.pull_request.base_branch"; - public static final String GIT_PULL_REQUEST_BASE_BRANCH_SHA = "git.pull_request.base_branch_sha"; - public static final String GIT_COMMIT_HEAD_SHA = "git.commit.head_sha"; - private final CiEnvironment environment; GithubActionsInfo(CiEnvironment environment) { @@ -80,9 +76,6 @@ public CIInfo buildCIInfo() { environment.get(GHACTIONS_SHA)); CIInfo.Builder builder = CIInfo.builder(environment); - - setAdditionalTagsIfApplicable(builder); - return builder .ciProviderName(GHACTIONS_PROVIDER_NAME) .ciPipelineId(environment.get(GHACTIONS_PIPELINE_ID)) @@ -97,16 +90,15 @@ public CIInfo buildCIInfo() { .build(); } - private void setAdditionalTagsIfApplicable(CIInfo.Builder builder) { + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { String baseRef = environment.get(GITHUB_BASE_REF); if (!Strings.isNotBlank(baseRef)) { - return; + return PullRequestInfo.EMPTY; } try { - Map additionalTags = new HashMap<>(); - additionalTags.put(GIT_PULL_REQUEST_BASE_BRANCH, baseRef); - Path eventPath = Paths.get(environment.get(GITHUB_EVENT_PATH)); String event = new String(Files.readAllBytes(eventPath), StandardCharsets.UTF_8); @@ -115,25 +107,27 @@ private void setAdditionalTagsIfApplicable(CIInfo.Builder builder) { moshi.adapter(Types.newParameterizedType(Map.class, String.class, Object.class)); Map eventJson = mapJsonAdapter.fromJson(event); + String baseSha = null; + String headSha = null; + Map pullRequest = (Map) eventJson.get("pull_request"); if (pullRequest != null) { Map head = (Map) pullRequest.get("head"); if (head != null) { - String headSha = (String) head.get("sha"); - additionalTags.put(GIT_COMMIT_HEAD_SHA, headSha); + headSha = (String) head.get("sha"); } Map base = (Map) pullRequest.get("base"); if (base != null) { - String baseSha = (String) base.get("sha"); - additionalTags.put(GIT_PULL_REQUEST_BASE_BRANCH_SHA, baseSha); + baseSha = (String) base.get("sha"); } } - builder.additionalTags(additionalTags); + return new PullRequestInfo(baseRef, baseSha, headSha); } catch (Exception e) { LOGGER.warn("Error while parsing GitHub event", e); + return PullRequestInfo.EMPTY; } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/JenkinsInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/JenkinsInfo.java index 0d0ec299ac6..252a42a3b2a 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/JenkinsInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/JenkinsInfo.java @@ -15,6 +15,7 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; +import javax.annotation.Nonnull; @SuppressForbidden class JenkinsInfo implements CIProviderInfo { @@ -68,6 +69,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String buildCiNodeLabels() { String labels = environment.get(JENKINS_NODE_LABELS); if (labels == null || labels.isEmpty()) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/PullRequestInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/PullRequestInfo.java new file mode 100644 index 00000000000..7365f8f824a --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/PullRequestInfo.java @@ -0,0 +1,72 @@ +package datadog.trace.civisibility.ci; + +import datadog.trace.util.Strings; +import java.util.Objects; + +public class PullRequestInfo { + + public static final PullRequestInfo EMPTY = new PullRequestInfo(null, null, null); + + private final String pullRequestBaseBranch; + private final String pullRequestBaseBranchSha; + private final String gitCommitHeadSha; + + public PullRequestInfo( + String pullRequestBaseBranch, String pullRequestBaseBranchSha, String gitCommitHeadSha) { + this.pullRequestBaseBranch = pullRequestBaseBranch; + this.pullRequestBaseBranchSha = pullRequestBaseBranchSha; + this.gitCommitHeadSha = gitCommitHeadSha; + } + + public String getPullRequestBaseBranch() { + return pullRequestBaseBranch; + } + + public String getPullRequestBaseBranchSha() { + return pullRequestBaseBranchSha; + } + + public String getGitCommitHeadSha() { + return gitCommitHeadSha; + } + + public boolean isNotEmpty() { + return Strings.isNotBlank(pullRequestBaseBranch) + || Strings.isNotBlank(pullRequestBaseBranchSha) + || Strings.isNotBlank(gitCommitHeadSha); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + PullRequestInfo that = (PullRequestInfo) o; + return Objects.equals(pullRequestBaseBranch, that.pullRequestBaseBranch) + && Objects.equals(pullRequestBaseBranchSha, that.pullRequestBaseBranchSha) + && Objects.equals(gitCommitHeadSha, that.gitCommitHeadSha); + } + + @Override + public int hashCode() { + return Objects.hash(pullRequestBaseBranch, pullRequestBaseBranchSha, gitCommitHeadSha); + } + + @Override + public String toString() { + return "PR{" + + "baseBranch='" + + pullRequestBaseBranch + + '\'' + + ", baseSHA='" + + pullRequestBaseBranchSha + + '\'' + + ", commitSHA='" + + gitCommitHeadSha + + '\'' + + '}'; + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/TeamcityInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/TeamcityInfo.java index 996576f134a..e2173ead686 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/TeamcityInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/TeamcityInfo.java @@ -4,6 +4,7 @@ import datadog.trace.api.git.CommitInfo; import datadog.trace.api.git.GitInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; +import javax.annotation.Nonnull; public class TeamcityInfo implements CIProviderInfo { public static final String TEAMCITY = "TEAMCITY_VERSION"; @@ -31,6 +32,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + @Override public Provider getProvider() { return Provider.TEAMCITY; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/TravisInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/TravisInfo.java index 8bcad157624..e95cb34ca31 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/TravisInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/TravisInfo.java @@ -9,6 +9,7 @@ import datadog.trace.api.git.GitInfo; import datadog.trace.api.git.PersonInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; +import javax.annotation.Nonnull; class TravisInfo implements CIProviderInfo { @@ -60,6 +61,12 @@ public CIInfo buildCIInfo() { .build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + private String buildGitBranch() { final String fromBranch = environment.get(TRAVIS_GIT_PR_BRANCH); if (fromBranch != null && !fromBranch.isEmpty()) { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/UnknownCIInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/UnknownCIInfo.java index 72ddbb4017c..df6ade9a13e 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/UnknownCIInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ci/UnknownCIInfo.java @@ -6,6 +6,7 @@ import datadog.trace.api.git.GitInfo; import datadog.trace.civisibility.ci.env.CiEnvironment; import java.nio.file.Path; +import javax.annotation.Nonnull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +61,12 @@ public CIInfo buildCIInfo() { return CIInfo.builder(environment).ciWorkspace(workspace.toAbsolutePath().toString()).build(); } + @Nonnull + @Override + public PullRequestInfo buildPullRequestInfo() { + return PullRequestInfo.EMPTY; + } + protected String getTargetFolder() { return targetFolder; } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ChangedFiles.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ChangedFiles.java new file mode 100644 index 00000000000..3867c290d99 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ChangedFiles.java @@ -0,0 +1,28 @@ +package datadog.trace.civisibility.config; + +import com.squareup.moshi.Json; +import java.util.Collections; +import java.util.Set; + +public class ChangedFiles { + + public static final ChangedFiles EMPTY = new ChangedFiles(null, Collections.emptySet()); + + @Json(name = "base_sha") + private final String baseSha; + + private final Set files; + + ChangedFiles(String baseSha, Set files) { + this.baseSha = baseSha; + this.files = files; + } + + public String getBaseSha() { + return baseSha; + } + + public Set getFiles() { + return files; + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/CiVisibilitySettings.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/CiVisibilitySettings.java index 9ba52a45662..a1686897a16 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/CiVisibilitySettings.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/CiVisibilitySettings.java @@ -8,13 +8,14 @@ public class CiVisibilitySettings { public static final CiVisibilitySettings DEFAULT = new CiVisibilitySettings( - false, false, false, false, false, EarlyFlakeDetectionSettings.DEFAULT); + false, false, false, false, false, false, EarlyFlakeDetectionSettings.DEFAULT); private final boolean itrEnabled; private final boolean codeCoverage; private final boolean testsSkipping; private final boolean requireGit; private final boolean flakyTestRetriesEnabled; + private final boolean impactedTestsDetectionEnabled; private final EarlyFlakeDetectionSettings earlyFlakeDetectionSettings; private CiVisibilitySettings( @@ -23,12 +24,14 @@ private CiVisibilitySettings( boolean testsSkipping, boolean requireGit, boolean flakyTestRetriesEnabled, + boolean impactedTestsDetectionEnabled, EarlyFlakeDetectionSettings earlyFlakeDetectionSettings) { this.itrEnabled = itrEnabled; this.codeCoverage = codeCoverage; this.testsSkipping = testsSkipping; this.requireGit = requireGit; this.flakyTestRetriesEnabled = flakyTestRetriesEnabled; + this.impactedTestsDetectionEnabled = impactedTestsDetectionEnabled; this.earlyFlakeDetectionSettings = earlyFlakeDetectionSettings; } @@ -52,6 +55,10 @@ public boolean isFlakyTestRetriesEnabled() { return flakyTestRetriesEnabled; } + public boolean isImpactedTestsDetectionEnabled() { + return impactedTestsDetectionEnabled; + } + public EarlyFlakeDetectionSettings getEarlyFlakeDetectionSettings() { return earlyFlakeDetectionSettings; } @@ -76,6 +83,7 @@ public CiVisibilitySettings fromJson(Map json) { getBoolean(json, "tests_skipping", false), getBoolean(json, "require_git", false), getBoolean(json, "flaky_test_retries_enabled", false), + getBoolean(json, "impacted_tests_enabled", false), EarlyFlakeDetectionSettingsJsonAdapter.INSTANCE.fromJson( (Map) json.get("early_flake_detection"))); } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ConfigurationApi.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ConfigurationApi.java index 2d7791eae71..bafd88f8e59 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ConfigurationApi.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ConfigurationApi.java @@ -31,6 +31,11 @@ public Map> getKnownTestsByModule( TracerEnvironment tracerEnvironment) { return Collections.emptyMap(); } + + @Override + public ChangedFiles getChangedFiles(TracerEnvironment tracerEnvironment) { + return ChangedFiles.EMPTY; + } }; CiVisibilitySettings getSettings(TracerEnvironment tracerEnvironment) throws IOException; @@ -42,4 +47,6 @@ Map> getFlakyTestsByModule(TracerEnvironment Map> getKnownTestsByModule(TracerEnvironment tracerEnvironment) throws IOException; + + ChangedFiles getChangedFiles(TracerEnvironment tracerEnvironment) throws IOException; } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ConfigurationApiImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ConfigurationApiImpl.java index 9667357ca03..947804e4320 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ConfigurationApiImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ConfigurationApiImpl.java @@ -47,6 +47,7 @@ public class ConfigurationApiImpl implements ConfigurationApi { private static final String SETTINGS_URI = "libraries/tests/services/setting"; private static final String SKIPPABLE_TESTS_URI = "ci/tests/skippable"; + private static final String CHANGED_FILES_URI = "ci/tests/diffs"; private static final String FLAKY_TESTS_URI = "ci/libraries/tests/flaky"; private static final String KNOWN_TESTS_URI = "ci/libraries/tests"; @@ -58,6 +59,7 @@ public class ConfigurationApiImpl implements ConfigurationApi { private final JsonAdapter> settingsResponseAdapter; private final JsonAdapter> testIdentifiersResponseAdapter; private final JsonAdapter> testFullNamesResponseAdapter; + private final JsonAdapter> changedFilesResponseAdapter; public ConfigurationApiImpl(BackendApi backendApi, CiVisibilityMetricCollector metricCollector) { this(backendApi, metricCollector, () -> UUID.randomUUID().toString()); @@ -98,6 +100,11 @@ public ConfigurationApiImpl(BackendApi backendApi, CiVisibilityMetricCollector m Types.newParameterizedTypeWithOwner( ConfigurationApiImpl.class, EnvelopeDto.class, KnownTestsDto.class); testFullNamesResponseAdapter = moshi.adapter(testFullNamesResponseType); + + ParameterizedType changedFilesResponseAdapterType = + Types.newParameterizedTypeWithOwner( + ConfigurationApiImpl.class, EnvelopeDto.class, ChangedFiles.class); + changedFilesResponseAdapter = moshi.adapter(changedFilesResponseAdapterType); } @Override @@ -285,6 +292,37 @@ private Map> parseTestIdentifiers(KnownTestsD return testIdentifiers; } + @Override + public ChangedFiles getChangedFiles(TracerEnvironment tracerEnvironment) throws IOException { + OkHttpUtils.CustomListener telemetryListener = + new TelemetryListener.Builder(metricCollector) + .requestCount(CiVisibilityCountMetric.IMPACTED_TESTS_DETECTION_REQUEST) + .requestErrors(CiVisibilityCountMetric.IMPACTED_TESTS_DETECTION_REQUEST_ERRORS) + .requestDuration(CiVisibilityDistributionMetric.IMPACTED_TESTS_DETECTION_REQUEST_MS) + .responseBytes(CiVisibilityDistributionMetric.IMPACTED_TESTS_DETECTION_RESPONSE_BYTES) + .build(); + + String uuid = uuidGenerator.get(); + EnvelopeDto request = + new EnvelopeDto<>(new DataDto<>(uuid, "ci_app_tests_diffs_request", tracerEnvironment)); + String json = requestAdapter.toJson(request); + RequestBody requestBody = RequestBody.create(JSON, json); + ChangedFiles changedFiles = + backendApi.post( + CHANGED_FILES_URI, + requestBody, + is -> + changedFilesResponseAdapter.fromJson(Okio.buffer(Okio.source(is))).data.attributes, + telemetryListener, + false); + + int filesCount = changedFiles.getFiles().size(); + LOGGER.debug("Received {} changed files", filesCount); + metricCollector.add( + CiVisibilityDistributionMetric.IMPACTED_TESTS_DETECTION_RESPONSE_FILES, filesCount); + return changedFiles; + } + private static final class EnvelopeDto { private final DataDto data; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/EarlyFlakeDetectionSettingsSerializer.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/EarlyFlakeDetectionSettingsSerializer.java index 85208850134..ee1ce6bf9c0 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/EarlyFlakeDetectionSettingsSerializer.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/EarlyFlakeDetectionSettingsSerializer.java @@ -1,6 +1,6 @@ package datadog.trace.civisibility.config; -import datadog.trace.civisibility.ipc.Serializer; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; import java.util.List; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ExecutionSettings.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ExecutionSettings.java index dce2f5dffc7..58030658476 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ExecutionSettings.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ExecutionSettings.java @@ -2,7 +2,9 @@ import datadog.trace.api.civisibility.config.TestIdentifier; import datadog.trace.api.civisibility.config.TestMetadata; -import datadog.trace.civisibility.ipc.Serializer; +import datadog.trace.civisibility.diff.Diff; +import datadog.trace.civisibility.diff.LineDiff; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; import java.util.BitSet; import java.util.Collection; @@ -21,45 +23,53 @@ public class ExecutionSettings { false, false, false, + false, EarlyFlakeDetectionSettings.DEFAULT, null, Collections.emptyMap(), Collections.emptyMap(), Collections.emptyList(), - null); + null, + LineDiff.EMPTY); private final boolean itrEnabled; private final boolean codeCoverageEnabled; private final boolean testSkippingEnabled; private final boolean flakyTestRetriesEnabled; + private final boolean impactedTestsDetectionEnabled; @Nonnull private final EarlyFlakeDetectionSettings earlyFlakeDetectionSettings; @Nullable private final String itrCorrelationId; @Nonnull private final Map skippableTests; @Nullable private final Map skippableTestsCoverage; @Nullable private final Collection flakyTests; @Nullable private final Collection knownTests; + @Nonnull private final Diff pullRequestDiff; public ExecutionSettings( boolean itrEnabled, boolean codeCoverageEnabled, boolean testSkippingEnabled, boolean flakyTestRetriesEnabled, + boolean impactedTestsDetectionEnabled, @Nonnull EarlyFlakeDetectionSettings earlyFlakeDetectionSettings, @Nullable String itrCorrelationId, @Nonnull Map skippableTests, @Nullable Map skippableTestsCoverage, @Nullable Collection flakyTests, - @Nullable Collection knownTests) { + @Nullable Collection knownTests, + @Nonnull Diff pullRequestDiff) { this.itrEnabled = itrEnabled; this.codeCoverageEnabled = codeCoverageEnabled; this.testSkippingEnabled = testSkippingEnabled; this.flakyTestRetriesEnabled = flakyTestRetriesEnabled; + this.impactedTestsDetectionEnabled = impactedTestsDetectionEnabled; this.earlyFlakeDetectionSettings = earlyFlakeDetectionSettings; this.itrCorrelationId = itrCorrelationId; this.skippableTests = skippableTests; this.skippableTestsCoverage = skippableTestsCoverage; this.flakyTests = flakyTests; this.knownTests = knownTests; + this.pullRequestDiff = pullRequestDiff; } /** @@ -82,6 +92,10 @@ public boolean isFlakyTestRetriesEnabled() { return flakyTestRetriesEnabled; } + public boolean isImpactedTestsDetectionEnabled() { + return impactedTestsDetectionEnabled; + } + @Nonnull public EarlyFlakeDetectionSettings getEarlyFlakeDetectionSettings() { return earlyFlakeDetectionSettings; @@ -117,6 +131,11 @@ public Collection getFlakyTests() { return flakyTests; } + @Nonnull + public Diff getPullRequestDiff() { + return pullRequestDiff; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -134,7 +153,8 @@ public boolean equals(Object o) { && Objects.equals(skippableTests, that.skippableTests) && Objects.equals(skippableTestsCoverage, that.skippableTestsCoverage) && Objects.equals(flakyTests, that.flakyTests) - && Objects.equals(knownTests, that.knownTests); + && Objects.equals(knownTests, that.knownTests) + && Objects.equals(pullRequestDiff, that.pullRequestDiff); } @Override @@ -148,7 +168,8 @@ public int hashCode() { skippableTests, skippableTestsCoverage, flakyTests, - knownTests); + knownTests, + pullRequestDiff); } public static class ExecutionSettingsSerializer { @@ -157,6 +178,7 @@ public static class ExecutionSettingsSerializer { private static final int CODE_COVERAGE_ENABLED_FLAG = 2; private static final int TEST_SKIPPING_ENABLED_FLAG = 4; private static final int FLAKY_TEST_RETRIES_ENABLED_FLAG = 8; + private static final int IMPACTED_TESTS_DETECTION_ENABLED_FLAG = 16; public static ByteBuffer serialize(ExecutionSettings settings) { Serializer s = new Serializer(); @@ -166,7 +188,10 @@ public static ByteBuffer serialize(ExecutionSettings settings) { ((settings.itrEnabled ? ITR_ENABLED_FLAG : 0) | (settings.codeCoverageEnabled ? CODE_COVERAGE_ENABLED_FLAG : 0) | (settings.testSkippingEnabled ? TEST_SKIPPING_ENABLED_FLAG : 0) - | (settings.flakyTestRetriesEnabled ? FLAKY_TEST_RETRIES_ENABLED_FLAG : 0)); + | (settings.flakyTestRetriesEnabled ? FLAKY_TEST_RETRIES_ENABLED_FLAG : 0) + | (settings.impactedTestsDetectionEnabled + ? IMPACTED_TESTS_DETECTION_ENABLED_FLAG + : 0)); s.write(flags); EarlyFlakeDetectionSettingsSerializer.serialize(s, settings.earlyFlakeDetectionSettings); @@ -177,13 +202,12 @@ public static ByteBuffer serialize(ExecutionSettings settings) { TestIdentifierSerializer::serialize, TestMetadataSerializer::serialize); - s.write( - settings.skippableTestsCoverage, - Serializer::write, - ExecutionSettingsSerializer::writeBitSet); + s.write(settings.skippableTestsCoverage, Serializer::write, Serializer::write); s.write(settings.flakyTests, TestIdentifierSerializer::serialize); s.write(settings.knownTests, TestIdentifierSerializer::serialize); + Diff.SERIALIZER.serialize(settings.pullRequestDiff, s); + return s.flush(); } @@ -193,6 +217,7 @@ public static ExecutionSettings deserialize(ByteBuffer buffer) { boolean codeCoverageEnabled = (flags & CODE_COVERAGE_ENABLED_FLAG) != 0; boolean testSkippingEnabled = (flags & TEST_SKIPPING_ENABLED_FLAG) != 0; boolean flakyTestRetriesEnabled = (flags & FLAKY_TEST_RETRIES_ENABLED_FLAG) != 0; + boolean impactedTestsDetectionEnabled = (flags & IMPACTED_TESTS_DETECTION_ENABLED_FLAG) != 0; EarlyFlakeDetectionSettings earlyFlakeDetectionSettings = EarlyFlakeDetectionSettingsSerializer.deserialize(buffer); @@ -204,37 +229,27 @@ public static ExecutionSettings deserialize(ByteBuffer buffer) { buffer, TestIdentifierSerializer::deserialize, TestMetadataSerializer::deserialize); Map skippableTestsCoverage = - Serializer.readMap( - buffer, Serializer::readString, ExecutionSettingsSerializer::readBitSet); + Serializer.readMap(buffer, Serializer::readString, Serializer::readBitSet); Collection flakyTests = Serializer.readSet(buffer, TestIdentifierSerializer::deserialize); Collection knownTests = Serializer.readSet(buffer, TestIdentifierSerializer::deserialize); + Diff diff = Diff.SERIALIZER.deserialize(buffer); + return new ExecutionSettings( itrEnabled, codeCoverageEnabled, testSkippingEnabled, flakyTestRetriesEnabled, + impactedTestsDetectionEnabled, earlyFlakeDetectionSettings, itrCorrelationId, skippableTests, skippableTestsCoverage, flakyTests, - knownTests); - } - - private static void writeBitSet(Serializer serializer, BitSet bitSet) { - if (bitSet != null) { - serializer.write(bitSet.toByteArray()); - } else { - serializer.write((byte[]) null); - } - } - - private static BitSet readBitSet(ByteBuffer byteBuffer) { - byte[] bytes = Serializer.readByteArray(byteBuffer); - return bytes != null ? BitSet.valueOf(bytes) : null; + knownTests, + diff); } } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ExecutionSettingsFactoryImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ExecutionSettingsFactoryImpl.java index d1b345c1808..85564b55280 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ExecutionSettingsFactoryImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/ExecutionSettingsFactoryImpl.java @@ -7,7 +7,13 @@ import datadog.trace.api.civisibility.config.TestMetadata; import datadog.trace.api.git.GitInfo; import datadog.trace.api.git.GitInfoProvider; +import datadog.trace.civisibility.ci.PullRequestInfo; +import datadog.trace.civisibility.diff.Diff; +import datadog.trace.civisibility.diff.FileDiff; +import datadog.trace.civisibility.diff.LineDiff; +import datadog.trace.civisibility.git.tree.GitClient; import datadog.trace.civisibility.git.tree.GitDataUploader; +import datadog.trace.civisibility.git.tree.GitRepoUnshallow; import java.nio.file.Path; import java.nio.file.Paths; import java.util.BitSet; @@ -20,7 +26,6 @@ import java.util.concurrent.TimeUnit; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,17 +43,26 @@ public class ExecutionSettingsFactoryImpl implements ExecutionSettingsFactory { private final Config config; private final ConfigurationApi configurationApi; + private final GitClient gitClient; + private final GitRepoUnshallow gitRepoUnshallow; private final GitDataUploader gitDataUploader; + private final PullRequestInfo pullRequestInfo; private final String repositoryRoot; public ExecutionSettingsFactoryImpl( Config config, ConfigurationApi configurationApi, + GitClient gitClient, + GitRepoUnshallow gitRepoUnshallow, GitDataUploader gitDataUploader, + PullRequestInfo pullRequestInfo, String repositoryRoot) { this.config = config; this.configurationApi = configurationApi; + this.gitClient = gitClient; + this.gitRepoUnshallow = gitRepoUnshallow; this.gitDataUploader = gitDataUploader; + this.pullRequestInfo = pullRequestInfo; this.repositoryRoot = repositoryRoot; } @@ -98,13 +112,14 @@ private TracerEnvironment buildTracerEnvironment( .build(); } - private @NotNull Map create(TracerEnvironment tracerEnvironment) { + private @Nonnull Map create(TracerEnvironment tracerEnvironment) { CiVisibilitySettings ciVisibilitySettings = getCiVisibilitySettings(tracerEnvironment); boolean itrEnabled = isItrEnabled(ciVisibilitySettings); boolean codeCoverageEnabled = isCodeCoverageEnabled(ciVisibilitySettings); boolean testSkippingEnabled = isTestSkippingEnabled(ciVisibilitySettings); boolean flakyTestRetriesEnabled = isFlakyTestRetriesEnabled(ciVisibilitySettings); + boolean impactedTestsDetectionEnabled = isImpactedTestsDetectionEnabled(ciVisibilitySettings); boolean earlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled(ciVisibilitySettings); LOGGER.info( @@ -113,6 +128,7 @@ private TracerEnvironment buildTracerEnvironment( + "Per-test code coverage - {},\n" + "Tests skipping - {},\n" + "Early flakiness detection - {},\n" + + "Impacted tests detection - {},\n" + "Auto test retries - {}", repositoryRoot, tracerEnvironment.getConfigurations().getRuntimeName(), @@ -122,6 +138,7 @@ private TracerEnvironment buildTracerEnvironment( codeCoverageEnabled, testSkippingEnabled, earlyFlakeDetectionEnabled, + impactedTestsDetectionEnabled, flakyTestRetriesEnabled); String itrCorrelationId = null; @@ -161,6 +178,8 @@ private TracerEnvironment buildTracerEnvironment( moduleNames.addAll(knownTestsByModule.keySet()); } + Diff pullRequestDiff = getPullRequestDiff(impactedTestsDetectionEnabled, tracerEnvironment); + Map settingsByModule = new HashMap<>(); for (String moduleName : moduleNames) { settingsByModule.put( @@ -170,11 +189,8 @@ private TracerEnvironment buildTracerEnvironment( codeCoverageEnabled, testSkippingEnabled, flakyTestRetriesEnabled, - // knownTests being null covers the following cases: - // - early flake detection is disabled in remote settings - // - early flake detection is disabled via local config killswitch - // - the list of known tests could not be obtained - knownTestsByModule != null + impactedTestsDetectionEnabled, + earlyFlakeDetectionEnabled ? ciVisibilitySettings.getEarlyFlakeDetectionSettings() : EarlyFlakeDetectionSettings.DEFAULT, itrCorrelationId, @@ -183,7 +199,8 @@ private TracerEnvironment buildTracerEnvironment( flakyTestsByModule != null ? flakyTestsByModule.getOrDefault(moduleName, Collections.emptyList()) : null, - knownTestsByModule != null ? knownTestsByModule.get(moduleName) : null)); + knownTestsByModule != null ? knownTestsByModule.get(moduleName) : null, + pullRequestDiff)); } return settingsByModule; } @@ -227,6 +244,11 @@ private boolean isFlakyTestRetriesEnabled(CiVisibilitySettings ciVisibilitySetti && config.isCiVisibilityFlakyRetryEnabled(); } + private boolean isImpactedTestsDetectionEnabled(CiVisibilitySettings ciVisibilitySettings) { + return ciVisibilitySettings.isImpactedTestsDetectionEnabled() + && config.isCiVisibilityImpactedTestsDetectionEnabled(); + } + private boolean isEarlyFlakeDetectionEnabled(CiVisibilitySettings ciVisibilitySettings) { return ciVisibilitySettings.getEarlyFlakeDetectionSettings().isEnabled() && config.isCiVisibilityEarlyFlakeDetectionEnabled(); @@ -266,8 +288,7 @@ private Map> getFlakyTestsByModule( return configurationApi.getFlakyTestsByModule(tracerEnvironment); } catch (Exception e) { - LOGGER.error( - "Could not obtain list of flaky tests, flaky test retries will not be available", e); + LOGGER.error("Could not obtain list of flaky tests", e); return Collections.emptyMap(); } } @@ -278,10 +299,63 @@ private Map> getKnownTestsByModule( return configurationApi.getKnownTestsByModule(tracerEnvironment); } catch (Exception e) { - LOGGER.error( - "Could not obtain list of known tests, early flakiness detection will not be available", - e); + LOGGER.error("Could not obtain list of known tests", e); return null; } } + + @Nonnull + private Diff getPullRequestDiff( + boolean impactedTestsDetectionEnabled, TracerEnvironment tracerEnvironment) { + if (!impactedTestsDetectionEnabled) { + return LineDiff.EMPTY; + } + + try { + if (repositoryRoot != null) { + // ensure repo is not shallow before attempting to get git diff + gitRepoUnshallow.unshallow(); + Diff diff = + gitClient.getGitDiff( + pullRequestInfo.getPullRequestBaseBranchSha(), + pullRequestInfo.getGitCommitHeadSha()); + if (diff != null) { + return diff; + } + } + + } catch (InterruptedException e) { + LOGGER.error("Interrupted while getting git diff for PR: {}", pullRequestInfo, e); + Thread.currentThread().interrupt(); + + } catch (Exception e) { + LOGGER.error("Could not get git diff for PR: {}", pullRequestInfo, e); + } + + try { + ChangedFiles changedFiles = configurationApi.getChangedFiles(tracerEnvironment); + + // attempting to use base SHA returned by the backend to calculate git diff + if (repositoryRoot != null) { + // ensure repo is not shallow before attempting to get git diff + gitRepoUnshallow.unshallow(); + Diff diff = gitClient.getGitDiff(changedFiles.getBaseSha(), tracerEnvironment.getSha()); + if (diff != null) { + return diff; + } + } + + // falling back to file-level granularity + return new FileDiff(changedFiles.getFiles()); + + } catch (InterruptedException e) { + LOGGER.error("Interrupted while getting git diff for: {}", tracerEnvironment, e); + Thread.currentThread().interrupt(); + + } catch (Exception e) { + LOGGER.error("Could not get git diff for: {}", tracerEnvironment, e); + } + + return LineDiff.EMPTY; + } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/JvmInfo.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/JvmInfo.java index 7426dc8b989..919a53df2ed 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/JvmInfo.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/JvmInfo.java @@ -2,7 +2,7 @@ import datadog.trace.api.Config; import datadog.trace.api.civisibility.CiVisibilityWellKnownTags; -import datadog.trace.civisibility.ipc.Serializer; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; import java.util.Objects; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TestIdentifierSerializer.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TestIdentifierSerializer.java index 73a6e113ca4..27c57702a00 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TestIdentifierSerializer.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TestIdentifierSerializer.java @@ -1,7 +1,7 @@ package datadog.trace.civisibility.config; import datadog.trace.api.civisibility.config.TestIdentifier; -import datadog.trace.civisibility.ipc.Serializer; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; public abstract class TestIdentifierSerializer { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TestMetadataSerializer.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TestMetadataSerializer.java index 9ed129068ba..949c4aa3737 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TestMetadataSerializer.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TestMetadataSerializer.java @@ -1,7 +1,7 @@ package datadog.trace.civisibility.config; import datadog.trace.api.civisibility.config.TestMetadata; -import datadog.trace.civisibility.ipc.Serializer; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; public abstract class TestMetadataSerializer { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TracerEnvironment.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TracerEnvironment.java index 319c95a454e..57bceaea094 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TracerEnvironment.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/config/TracerEnvironment.java @@ -36,10 +36,40 @@ private TracerEnvironment( this.configurations = configurations; } + public String getSha() { + return sha; + } + public Configurations getConfigurations() { return configurations; } + @Override + public String toString() { + return "TracerEnvironment{" + + "service='" + + service + + '\'' + + ", env='" + + env + + '\'' + + ", repositoryUrl='" + + repositoryUrl + + '\'' + + ", branch='" + + branch + + '\'' + + ", sha='" + + sha + + '\'' + + ", testLevel='" + + testLevel + + '\'' + + ", configurations=" + + configurations + + '}'; + } + public static Builder builder() { return new Builder(); } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/diff/Diff.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/diff/Diff.java new file mode 100644 index 00000000000..dfccfa41351 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/diff/Diff.java @@ -0,0 +1,12 @@ +package datadog.trace.civisibility.diff; + +import datadog.trace.civisibility.ipc.serialization.PolymorphicSerializer; +import datadog.trace.civisibility.ipc.serialization.SerializableType; + +public interface Diff extends SerializableType { + + PolymorphicSerializer SERIALIZER = + new PolymorphicSerializer<>(LineDiff.class, FileDiff.class); + + boolean contains(String relativePath, int startLine, int endLine); +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/diff/FileDiff.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/diff/FileDiff.java new file mode 100644 index 00000000000..d404aba92ea --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/diff/FileDiff.java @@ -0,0 +1,53 @@ +package datadog.trace.civisibility.diff; + +import datadog.trace.civisibility.ipc.serialization.Serializer; +import java.nio.ByteBuffer; +import java.util.Objects; +import java.util.Set; +import javax.annotation.Nonnull; + +/** Diff data with per-file granularity. */ +public class FileDiff implements Diff { + + private final @Nonnull Set changedFiles; + + public FileDiff(@Nonnull Set changedFiles) { + this.changedFiles = changedFiles; + } + + @Override + public boolean contains(String relativePath, int startLine, int endLine) { + return changedFiles.contains(relativePath); + } + + @Override + public void serialize(Serializer s) { + s.write(changedFiles); + } + + public static FileDiff deserialize(ByteBuffer buffer) { + return new FileDiff(Serializer.readSet(buffer, Serializer::readString)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + FileDiff diff = (FileDiff) o; + return Objects.equals(changedFiles, diff.changedFiles); + } + + @Override + public int hashCode() { + return Objects.hashCode(changedFiles); + } + + @Override + public String toString() { + return "FileDiff{changedFiles=" + changedFiles + '}'; + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/diff/LineDiff.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/diff/LineDiff.java new file mode 100644 index 00000000000..3d08b8be53b --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/diff/LineDiff.java @@ -0,0 +1,68 @@ +package datadog.trace.civisibility.diff; + +import datadog.trace.civisibility.ipc.serialization.Serializer; +import java.nio.ByteBuffer; +import java.util.BitSet; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +/** Diff data with per-line granularity. */ +public class LineDiff implements Diff { + + public static final LineDiff EMPTY = new LineDiff(Collections.emptyMap()); + + private final Map linesByRelativePath; + + public LineDiff(Map linesByRelativePath) { + this.linesByRelativePath = linesByRelativePath; + } + + public Map getLinesByRelativePath() { + return Collections.unmodifiableMap(linesByRelativePath); + } + + @Override + public boolean contains(String relativePath, int startLine, int endLine) { + BitSet lines = linesByRelativePath.get(relativePath); + if (lines == null) { + return false; + } + + int changedLine = lines.nextSetBit(startLine); + return changedLine != -1 && changedLine <= endLine; + } + + @Override + public void serialize(Serializer s) { + s.write(linesByRelativePath, Serializer::write, Serializer::write); + } + + public static LineDiff deserialize(ByteBuffer buffer) { + Map linesByRelativePath = + Serializer.readMap(buffer, Serializer::readString, Serializer::readBitSet); + return new LineDiff(linesByRelativePath); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LineDiff diff = (LineDiff) o; + return Objects.equals(linesByRelativePath, diff.linesByRelativePath); + } + + @Override + public int hashCode() { + return Objects.hashCode(linesByRelativePath); + } + + @Override + public String toString() { + return "LineDiff{linesByRelativePath=" + linesByRelativePath + '}'; + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestFrameworkModule.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestFrameworkModule.java index 6c98136fa7e..62e1e48aec9 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestFrameworkModule.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestFrameworkModule.java @@ -1,6 +1,7 @@ package datadog.trace.civisibility.domain; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; import javax.annotation.Nonnull; @@ -27,6 +28,8 @@ TestSuiteImpl testSuiteStart( boolean isFlaky(TestIdentifier test); + boolean isModified(TestSourceData testSourceData); + /** * Checks if a given test should be skipped with Intelligent Test Runner or not * @@ -45,7 +48,7 @@ TestSuiteImpl testSuiteStart( boolean skip(TestIdentifier test); @Nonnull - TestRetryPolicy retryPolicy(TestIdentifier test); + TestRetryPolicy retryPolicy(TestIdentifier test, TestSourceData testSource); void end(Long startTime); } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java index 8300bb9afd8..47446da810b 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/TestImpl.java @@ -18,6 +18,7 @@ import datadog.trace.api.civisibility.telemetry.CiVisibilityMetricCollector; import datadog.trace.api.civisibility.telemetry.tag.BrowserDriver; import datadog.trace.api.civisibility.telemetry.tag.EventType; +import datadog.trace.api.civisibility.telemetry.tag.IsModified; import datadog.trace.api.civisibility.telemetry.tag.IsNew; import datadog.trace.api.civisibility.telemetry.tag.IsRetry; import datadog.trace.api.civisibility.telemetry.tag.IsRum; @@ -54,6 +55,7 @@ public class TestImpl implements DDTest { private final long suiteId; private final Consumer onSpanFinish; private final TestContext context; + private final TestIdentifier identifier; public TestImpl( AgentSpanContext moduleSpanContext, @@ -82,7 +84,7 @@ public TestImpl( this.suiteId = suiteId; this.onSpanFinish = onSpanFinish; - TestIdentifier identifier = new TestIdentifier(testSuiteName, testName, testParameters); + this.identifier = new TestIdentifier(testSuiteName, testName, testParameters); CoverageStore coverageStore = coverageStoreFactory.create(identifier); CoveragePerTestBridge.setThreadLocalCoverageProbes(coverageStore.getProbes()); @@ -179,6 +181,10 @@ private void populateSourceDataTags( } } + public TestIdentifier getIdentifier() { + return identifier; + } + @Override public void setTag(String key, Object value) { span.setTag(key, value); @@ -257,6 +263,7 @@ public void end(@Nullable Long endTime) { instrumentation, EventType.TEST, span.getTag(Tags.TEST_IS_NEW) != null ? IsNew.TRUE : null, + span.getTag(Tags.TEST_IS_MODIFIED) != null ? IsModified.TRUE : null, span.getTag(Tags.TEST_IS_RETRY) != null ? IsRetry.TRUE : null, span.getTag(Tags.TEST_IS_RUM_ACTIVE) != null ? IsRum.TRUE : null, CIConstants.SELENIUM_BROWSER_DRIVER.equals(span.getTag(Tags.TEST_BROWSER_DRIVER)) diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/buildsystem/BuildSystemModuleImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/buildsystem/BuildSystemModuleImpl.java index 9e460bea0a9..4dd03dfabce 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/buildsystem/BuildSystemModuleImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/buildsystem/BuildSystemModuleImpl.java @@ -158,6 +158,11 @@ private Map getPropertiesPropagatedToChildProcess( CiVisibilityConfig.CIVISIBILITY_FLAKY_RETRY_ENABLED), Boolean.toString(executionSettings.isFlakyTestRetriesEnabled())); + propagatedSystemProperties.put( + Strings.propertyNameToSystemPropertyName( + CiVisibilityConfig.CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED), + Boolean.toString(executionSettings.isImpactedTestsDetectionEnabled())); + propagatedSystemProperties.put( Strings.propertyNameToSystemPropertyName( CiVisibilityConfig.CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED), diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/buildsystem/ProxyTestModule.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/buildsystem/ProxyTestModule.java index 0ea5235c523..3e903a07c51 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/buildsystem/ProxyTestModule.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/buildsystem/ProxyTestModule.java @@ -3,6 +3,7 @@ import datadog.trace.api.Config; import datadog.trace.api.DDTraceId; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.coverage.CoverageStore; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.api.civisibility.telemetry.CiVisibilityMetricCollector; @@ -93,6 +94,11 @@ public boolean isFlaky(TestIdentifier test) { return executionStrategy.isFlaky(test); } + @Override + public boolean isModified(TestSourceData testSourceData) { + return executionStrategy.isModified(testSourceData); + } + @Override public boolean shouldBeSkipped(TestIdentifier test) { return executionStrategy.shouldBeSkipped(test); @@ -105,8 +111,8 @@ public boolean skip(TestIdentifier test) { @Override @Nonnull - public TestRetryPolicy retryPolicy(TestIdentifier test) { - return executionStrategy.retryPolicy(test); + public TestRetryPolicy retryPolicy(TestIdentifier test, TestSourceData testSource) { + return executionStrategy.retryPolicy(test, testSource); } @Override @@ -136,7 +142,7 @@ private void sendModuleExecutionResult() { executionSettings.getEarlyFlakeDetectionSettings(); boolean earlyFlakeDetectionEnabled = earlyFlakeDetectionSettings.isEnabled(); boolean earlyFlakeDetectionFaulty = - earlyFlakeDetectionEnabled && executionStrategy.isEarlyFlakeDetectionLimitReached(); + earlyFlakeDetectionEnabled && executionStrategy.isEFDLimitReached(); long testsSkippedTotal = executionStrategy.getTestsSkipped(); signalClient.send( diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/headless/HeadlessTestModule.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/headless/HeadlessTestModule.java index 50d81c9c932..00833d65b7e 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/headless/HeadlessTestModule.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/domain/headless/HeadlessTestModule.java @@ -4,6 +4,7 @@ import datadog.trace.api.DDTags; import datadog.trace.api.civisibility.CIConstants; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.coverage.CoverageStore; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.api.civisibility.telemetry.CiVisibilityMetricCollector; @@ -78,6 +79,11 @@ public boolean isFlaky(TestIdentifier test) { return executionStrategy.isFlaky(test); } + @Override + public boolean isModified(TestSourceData testSourceData) { + return executionStrategy.isModified(testSourceData); + } + @Override public boolean shouldBeSkipped(TestIdentifier test) { return executionStrategy.shouldBeSkipped(test); @@ -90,8 +96,8 @@ public boolean skip(TestIdentifier test) { @Override @Nonnull - public TestRetryPolicy retryPolicy(TestIdentifier test) { - return executionStrategy.retryPolicy(test); + public TestRetryPolicy retryPolicy(TestIdentifier test, TestSourceData testSource) { + return executionStrategy.retryPolicy(test, testSource); } @Override @@ -116,7 +122,7 @@ public void end(@Nullable Long endTime) { executionSettings.getEarlyFlakeDetectionSettings(); if (earlyFlakeDetectionSettings.isEnabled()) { setTag(Tags.TEST_EARLY_FLAKE_ENABLED, true); - if (executionStrategy.isEarlyFlakeDetectionLimitReached()) { + if (executionStrategy.isEFDLimitReached()) { setTag(Tags.TEST_EARLY_FLAKE_ABORT_REASON, CIConstants.EFD_ABORT_REASON_FAULTY); } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/NoOpTestEventsHandler.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/NoOpTestEventsHandler.java index 283ed9b7366..9cdec4df5b5 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/NoOpTestEventsHandler.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/NoOpTestEventsHandler.java @@ -3,13 +3,14 @@ import datadog.trace.api.civisibility.DDTest; import datadog.trace.api.civisibility.DDTestSuite; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestEventsHandler; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; import datadog.trace.bootstrap.ContextStore; import datadog.trace.civisibility.retry.NeverRetry; -import java.lang.reflect.Method; import java.util.Collection; +import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.jetbrains.annotations.NotNull; @@ -49,15 +50,12 @@ public void onTestSuiteFinish(SuiteKey descriptor, @Nullable Long endTime) { public void onTestStart( SuiteKey suiteDescriptor, TestKey descriptor, - String testSuiteName, String testName, @Nullable String testFramework, @Nullable String testFrameworkVersion, @Nullable String testParameters, @Nullable Collection categories, - @Nullable Class testClass, - @Nullable String testMethodName, - @Nullable Method testMethod, + @Nonnull TestSourceData testSourceData, boolean isRetry, @Nullable Long startTime) { // do nothing @@ -82,15 +80,12 @@ public void onTestFinish(TestKey descriptor, @Nullable Long endTime) { public void onTestIgnore( SuiteKey suiteDescriptor, TestKey testDescriptor, - String testSuiteName, String testName, @Nullable String testFramework, @Nullable String testFrameworkVersion, @Nullable String testParameters, @Nullable Collection categories, - @Nullable Class testClass, - @Nullable String testMethodName, - @Nullable Method testMethod, + @Nonnull TestSourceData testSourceData, @Nullable String reason) { // do nothing } @@ -107,7 +102,7 @@ public boolean shouldBeSkipped(TestIdentifier test) { @NotNull @Override - public TestRetryPolicy retryPolicy(TestIdentifier test) { + public TestRetryPolicy retryPolicy(TestIdentifier test, TestSourceData source) { return NeverRetry.INSTANCE; } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java index 14ff1f2f1ce..cfbe534a2f0 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/events/TestEventsHandlerImpl.java @@ -6,6 +6,7 @@ import datadog.trace.api.civisibility.DDTestSuite; import datadog.trace.api.civisibility.InstrumentationBridge; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestEventsHandler; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.api.civisibility.telemetry.CiVisibilityCountMetric; @@ -18,7 +19,6 @@ import datadog.trace.civisibility.domain.TestFrameworkSession; import datadog.trace.civisibility.domain.TestImpl; import datadog.trace.civisibility.domain.TestSuiteImpl; -import java.lang.reflect.Method; import java.util.Collection; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -131,18 +131,15 @@ public void onTestSuiteFailure(SuiteKey descriptor, @Nullable Throwable throwabl public void onTestStart( final SuiteKey suiteDescriptor, final TestKey descriptor, - final String testSuiteName, final String testName, final @Nullable String testFramework, final @Nullable String testFrameworkVersion, final @Nullable String testParameters, final @Nullable Collection categories, - final @Nullable Class testClass, - final @Nullable String testMethodName, - final @Nullable Method testMethod, + final @Nonnull TestSourceData testSourceData, final boolean isRetry, @Nullable Long startTime) { - if (skipTrace(testClass)) { + if (skipTrace(testSourceData.getTestClass())) { return; } @@ -155,13 +152,18 @@ public void onTestStart( + descriptor); } - TestImpl test = testSuite.testStart(testName, testParameters, testMethod, startTime); + TestImpl test = + testSuite.testStart(testName, testParameters, testSourceData.getTestMethod(), startTime); - TestIdentifier thisTest = new TestIdentifier(testSuiteName, testName, testParameters); + TestIdentifier thisTest = test.getIdentifier(); if (testModule.isNew(thisTest)) { test.setTag(Tags.TEST_IS_NEW, true); } + if (testModule.isModified(testSourceData)) { + test.setTag(Tags.TEST_IS_MODIFIED, true); + } + if (testFramework != null) { test.setTag(Tags.TEST_FRAMEWORK, testFramework); if (testFrameworkVersion != null) { @@ -171,8 +173,11 @@ public void onTestStart( if (testParameters != null) { test.setTag(Tags.TEST_PARAMETERS, testParameters); } - if (testMethodName != null && testMethod != null) { - test.setTag(Tags.TEST_SOURCE_METHOD, testMethodName + Type.getMethodDescriptor(testMethod)); + if (testSourceData.getTestMethodName() != null && testSourceData.getTestMethod() != null) { + test.setTag( + Tags.TEST_SOURCE_METHOD, + testSourceData.getTestMethodName() + + Type.getMethodDescriptor(testSourceData.getTestMethod())); } if (categories != null && !categories.isEmpty()) { test.setTag(Tags.TEST_TRAITS, getTestTraits(categories)); @@ -232,28 +237,22 @@ public void onTestFinish(TestKey descriptor, @Nullable Long endTime) { public void onTestIgnore( final SuiteKey suiteDescriptor, final TestKey testDescriptor, - final String testSuiteName, final String testName, final @Nullable String testFramework, final @Nullable String testFrameworkVersion, final @Nullable String testParameters, final @Nullable Collection categories, - final @Nullable Class testClass, - final @Nullable String testMethodName, - final @Nullable Method testMethod, + @Nonnull TestSourceData testSourceData, final @Nullable String reason) { onTestStart( suiteDescriptor, testDescriptor, - testSuiteName, testName, testFramework, testFrameworkVersion, testParameters, categories, - testClass, - testMethodName, - testMethod, + testSourceData, false, null); onTestSkip(testDescriptor, reason); @@ -272,8 +271,8 @@ public boolean shouldBeSkipped(TestIdentifier test) { @Override @Nonnull - public TestRetryPolicy retryPolicy(TestIdentifier test) { - return testModule.retryPolicy(test); + public TestRetryPolicy retryPolicy(TestIdentifier test, TestSourceData testSource) { + return testModule.retryPolicy(test, testSource); } @Override diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitClient.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitClient.java index 2f8d73815d3..a6bffa37771 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitClient.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitClient.java @@ -1,582 +1,82 @@ package datadog.trace.civisibility.git.tree; -import datadog.communication.util.IOUtils; -import datadog.trace.api.Config; -import datadog.trace.api.civisibility.telemetry.CiVisibilityCountMetric; -import datadog.trace.api.civisibility.telemetry.CiVisibilityDistributionMetric; -import datadog.trace.api.civisibility.telemetry.CiVisibilityMetricCollector; -import datadog.trace.api.civisibility.telemetry.tag.Command; -import datadog.trace.api.civisibility.telemetry.tag.ExitCode; -import datadog.trace.civisibility.utils.ShellCommandExecutor; -import datadog.trace.util.Strings; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.io.File; +import datadog.trace.civisibility.diff.LineDiff; import java.io.IOException; -import java.nio.charset.Charset; -import java.nio.file.FileStore; -import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collection; -import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.concurrent.TimeoutException; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** Client for fetching data and performing operations on a local Git repository. */ -public class GitClient { +public interface GitClient { - public static final String HEAD = "HEAD"; + String HEAD = "HEAD"; - private static final String DD_TEMP_DIRECTORY_PREFIX = "dd-ci-vis-"; + boolean isShallow() throws IOException, TimeoutException, InterruptedException; - private final CiVisibilityMetricCollector metricCollector; - private final String repoRoot; - private final String latestCommitsSince; - private final int latestCommitsLimit; - private final ShellCommandExecutor commandExecutor; + void unshallow(@Nullable String remoteCommitReference) + throws IOException, TimeoutException, InterruptedException; - /** - * Creates a new git client - * - * @param metricCollector Telemetry metrics collector - * @param repoRoot Absolute path to Git repository root - * @param latestCommitsSince How far into the past the client should be looking when fetching Git - * data, e.g. {@code "1 month ago"} or {@code "2 years ago"} - * @param latestCommitsLimit Maximum client of commits that the client should be considering when - * fetching commit data - * @param timeoutMillis Timeout in milliseconds that is applied to executed Git commands - */ - GitClient( - CiVisibilityMetricCollector metricCollector, - String repoRoot, - String latestCommitsSince, - int latestCommitsLimit, - long timeoutMillis) { - this.metricCollector = metricCollector; - this.repoRoot = repoRoot; - this.latestCommitsSince = latestCommitsSince; - this.latestCommitsLimit = latestCommitsLimit; - commandExecutor = new ShellCommandExecutor(new File(repoRoot), timeoutMillis); - } - - /** - * Checks whether the repo that the client is associated with is shallow - * - * @return {@code true} if current repo is shallow, {@code false} otherwise - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public boolean isShallow() throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.CHECK_SHALLOW, - () -> { - String output = - commandExecutor - .executeCommand(IOUtils::readFully, "git", "rev-parse", "--is-shallow-repository") - .trim(); - return Boolean.parseBoolean(output); - }); - } - - /** - * Returns the SHA of the head commit of the upstream (remote tracking) branch for the currently - * checked-out local branch. If the local branch is not tracking any remote branches, a {@link - * datadog.trace.civisibility.utils.ShellCommandExecutor.ShellCommandFailedException} exception - * will be thrown. - * - * @return The name of the upstream branch if the current local branch is tracking any. - * @throws ShellCommandExecutor.ShellCommandFailedException If the Git command fails with an error - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public String getUpstreamBranchSha() throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "rev-parse", "@{upstream}") - .trim()); - } - - /** - * "Unshallows" the repo that the client is associated with by fetching missing commit data from - * the server. - * - * @param remoteCommitReference The commit to fetch from the remote repository, so local repo will - * be updated with this commit and its ancestors. If {@code null}, everything will be fetched. - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public void unshallow(@Nullable String remoteCommitReference) - throws IOException, TimeoutException, InterruptedException { - executeCommand( - Command.UNSHALLOW, - () -> { - String remote = - commandExecutor - .executeCommand( - IOUtils::readFully, - "git", - "config", - "--default", - "origin", - "--get", - "clone.defaultRemoteName") - .trim(); + @Nullable + String getGitFolder() throws IOException, TimeoutException, InterruptedException; - // refetch data from the server for the given period of time - if (remoteCommitReference != null) { - String headSha = getSha(remoteCommitReference); - commandExecutor.executeCommand( - ShellCommandExecutor.OutputParser.IGNORE, - "git", - "fetch", - String.format("--shallow-since=='%s'", latestCommitsSince), - "--update-shallow", - "--filter=blob:none", - "--recurse-submodules=no", - remote, - headSha); - } else { - commandExecutor.executeCommand( - ShellCommandExecutor.OutputParser.IGNORE, - "git", - "fetch", - String.format("--shallow-since=='%s'", latestCommitsSince), - "--update-shallow", - "--filter=blob:none", - "--recurse-submodules=no", - remote); - } + @Nullable + String getRepoRoot() throws IOException, TimeoutException, InterruptedException; - return (Void) null; - }); - } - - /** - * Returns the absolute path of the .git directory. - * - * @return absolute path - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getGitFolder() throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "rev-parse", "--absolute-git-dir") - .trim()); - } - - /** - * Returns URL of the remote with the given name - * - * @param remoteName Name of the remote - * @return URL of the given remote - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public String getRemoteUrl(String remoteName) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.GET_REPOSITORY, - () -> - commandExecutor - .executeCommand( - IOUtils::readFully, "git", "config", "--get", "remote." + remoteName + ".url") - .trim()); - } + @Nullable + String getRemoteUrl(String remoteName) throws IOException, TimeoutException, InterruptedException; - /** - * Returns current branch, or an empty string if HEAD is not pointing to a branch - * - * @return current branch, or an empty string if HEAD is not pointing to a branch - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getCurrentBranch() - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.GET_BRANCH, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "branch", "--show-current") - .trim()); - } + @Nullable + String getUpstreamBranchSha() throws IOException, TimeoutException, InterruptedException; - /** - * Returns list of tags that provided commit points to - * - * @param commit Commit SHA or reference (HEAD, branch name, etc) to check - * @return list of tags that the commit is pointing to, or empty list if there are no such tags - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull List getTags(String commit) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> { - try { - return commandExecutor.executeCommand( - IOUtils::readLines, "git", "describe", "--tags", "--exact-match", commit); - } catch (ShellCommandExecutor.ShellCommandFailedException e) { - // if provided commit is not tagged, - // command will fail because "--exact-match" is specified - return Collections.emptyList(); - } - }); - } + @Nullable + String getCurrentBranch() throws IOException, TimeoutException, InterruptedException; - /** - * Returns SHA of the provided reference - * - * @param reference Reference (HEAD, branch name, etc) to check - * @return full SHA of the provided reference - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getSha(String reference) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "rev-parse", reference) - .trim()); - } + @Nonnull + List getTags(String commit) throws IOException, TimeoutException, InterruptedException; - /** - * Returns full message of the provided commit - * - * @param commit Commit SHA or reference (HEAD, branch name, etc) to check - * @return full message of the provided commit - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getFullMessage(String commit) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%B", commit) - .trim()); - } + @Nullable + String getSha(String reference) throws IOException, TimeoutException, InterruptedException; - /** - * Returns author name for the provided commit - * - * @param commit Commit SHA or reference (HEAD, branch name, etc) to check - * @return author name for the provided commit - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getAuthorName(String commit) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%an", commit) - .trim()); - } + @Nullable + String getFullMessage(String commit) throws IOException, TimeoutException, InterruptedException; - /** - * Returns author email for the provided commit - * - * @param commit Commit SHA or reference (HEAD, branch name, etc) to check - * @return author email for the provided commit - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getAuthorEmail(String commit) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%ae", commit) - .trim()); - } + @Nullable + String getAuthorName(String commit) throws IOException, TimeoutException, InterruptedException; - /** - * Returns author date in strict ISO 8601 format for the provided commit - * - * @param commit Commit SHA or reference (HEAD, branch name, etc) to check - * @return author date in strict ISO 8601 format - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getAuthorDate(String commit) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%aI", commit) - .trim()); - } + @Nullable + String getAuthorEmail(String commit) throws IOException, TimeoutException, InterruptedException; - /** - * Returns committer name for the provided commit - * - * @param commit Commit SHA or reference (HEAD, branch name, etc) to check - * @return committer name for the provided commit - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getCommitterName(String commit) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%cn", commit) - .trim()); - } + @Nullable + String getAuthorDate(String commit) throws IOException, TimeoutException, InterruptedException; - /** - * Returns committer email for the provided commit - * - * @param commit Commit SHA or reference (HEAD, branch name, etc) to check - * @return committer email for the provided commit - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getCommitterEmail(String commit) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%ce", commit) - .trim()); - } + @Nullable + String getCommitterName(String commit) throws IOException, TimeoutException, InterruptedException; - /** - * Returns committer date in strict ISO 8601 format for the provided commit - * - * @param commit Commit SHA or reference (HEAD, branch name, etc) to check - * @return committer date in strict ISO 8601 format - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public @NonNull String getCommitterDate(String commit) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.OTHER, - () -> - commandExecutor - .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%cI", commit) - .trim()); - } + @Nullable + String getCommitterEmail(String commit) + throws IOException, TimeoutException, InterruptedException; - /** - * Returns SHAs of the latest commits in the current branch. Maximum number of commits and how far - * into the past to look are configured when the client is created. - * - * @return SHAs of latest commits - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public List getLatestCommits() - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.GET_LOCAL_COMMITS, - () -> - commandExecutor.executeCommand( - IOUtils::readLines, - "git", - "log", - "--format=%H", - "-n", - String.valueOf(latestCommitsLimit), - String.format("--since='%s'", latestCommitsSince))); - } + @Nullable + String getCommitterDate(String commit) throws IOException, TimeoutException, InterruptedException; - /** - * Returns SHAs of all Git objects in the current branch. Lookup is started from HEAD commit. - * Maximum number of commits and how far into the past to look are configured when the client is - * created. - * - * @param commitsToSkip List of commits to skip - * @return SHAs of relevant Git objects - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public List getObjects( - Collection commitsToSkip, Collection commitsToInclude) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.GET_OBJECTS, - () -> { - String[] command = new String[6 + commitsToSkip.size() + commitsToInclude.size()]; - command[0] = "git"; - command[1] = "rev-list"; - command[2] = "--objects"; - command[3] = "--no-object-names"; - command[4] = "--filter=blob:none"; - command[5] = String.format("--since='%s'", latestCommitsSince); + @Nonnull + List getLatestCommits() throws IOException, TimeoutException, InterruptedException; - int count = 6; - for (String commitToSkip : commitsToSkip) { - command[count++] = "^" + commitToSkip; - } - for (String commitToInclude : commitsToInclude) { - command[count++] = commitToInclude; - } - - return commandExecutor.executeCommand(IOUtils::readLines, command); - }); - } - - /** - * Creates Git .pack files that include provided objects. The files are created in a temporary - * folder. - * - * @param objectHashes SHAs of objects that should be included in the pack files - * @return Path to temporary folder where created pack files are located - * @throws IOException If an error was encountered while writing command input or reading output - * @throws TimeoutException If timeout was reached while waiting for Git command to finish - * @throws InterruptedException If current thread was interrupted while waiting for Git command to - * finish - */ - public Path createPackFiles(List objectHashes) - throws IOException, TimeoutException, InterruptedException { - return executeCommand( - Command.PACK_OBJECTS, - () -> { - byte[] input = String.join("\n", objectHashes).getBytes(Charset.defaultCharset()); - - Path tempDirectory = createTempDirectory(); - String basename = Strings.random(8); - String path = tempDirectory.toString() + File.separator + basename; - - commandExecutor.executeCommand( - ShellCommandExecutor.OutputParser.IGNORE, - input, - "git", - "pack-objects", - "--compression=9", - "--max-pack-size=3m", - path); - return tempDirectory; - }); - } - - private Path createTempDirectory() throws IOException { - Path repoRootDirectory = Paths.get(repoRoot); - FileStore repoRootFileStore = Files.getFileStore(repoRootDirectory); - - Path tempDirectory = Files.createTempDirectory(DD_TEMP_DIRECTORY_PREFIX); - FileStore tempDirectoryStore = Files.getFileStore(tempDirectory); - - if (Objects.equals(tempDirectoryStore, repoRootFileStore)) { - return tempDirectory; - } else { - // default temp-file directory and repo root are located on different devices, - // so we have to create our temp dir inside repo root - // otherwise git command will fail - Files.delete(tempDirectory); - return Files.createTempDirectory(repoRootDirectory, DD_TEMP_DIRECTORY_PREFIX); - } - } - - @Override - public String toString() { - return "GitClient{" + repoRoot + "}"; - } - - private interface GitCommand { - T execute() throws IOException, TimeoutException, InterruptedException; - } - - private T executeCommand(Command commandType, GitCommand command) - throws IOException, TimeoutException, InterruptedException { - long startTime = System.currentTimeMillis(); - try { - return command.execute(); - - } catch (IOException | TimeoutException | InterruptedException e) { - metricCollector.add( - CiVisibilityCountMetric.GIT_COMMAND_ERRORS, 1, commandType, getExitCode(e)); - throw e; - - } finally { - metricCollector.add(CiVisibilityCountMetric.GIT_COMMAND, 1, commandType); - metricCollector.add( - CiVisibilityDistributionMetric.GIT_COMMAND_MS, - (int) (System.currentTimeMillis() - startTime), - commandType); - } - } - - private static ExitCode getExitCode(Exception e) { - if (e instanceof ShellCommandExecutor.ShellCommandFailedException) { - ShellCommandExecutor.ShellCommandFailedException scfe = - (ShellCommandExecutor.ShellCommandFailedException) e; - return ExitCode.from(scfe.getExitCode()); - - } else { - String m = e.getMessage(); - if (m != null && m.toLowerCase().contains("no such file or directory")) { - return ExitCode.EXECUTABLE_MISSING; - } else { - return ExitCode.CODE_UNKNOWN; - } - } - } + @Nonnull + List getObjects(Collection commitsToSkip, Collection commitsToInclude) + throws IOException, TimeoutException, InterruptedException; - public static class Factory { - private final Config config; - private final CiVisibilityMetricCollector metricCollector; + Path createPackFiles(List objectHashes) + throws IOException, TimeoutException, InterruptedException; - public Factory(Config config, CiVisibilityMetricCollector metricCollector) { - this.config = config; - this.metricCollector = metricCollector; - } + @Nullable + LineDiff getGitDiff(String baseCommit, String targetCommit) + throws IOException, TimeoutException, InterruptedException; - public GitClient create(String repoRoot) { - long commandTimeoutMillis = config.getCiVisibilityGitCommandTimeoutMillis(); - return new GitClient(metricCollector, repoRoot, "1 month ago", 1000, commandTimeoutMillis); - } + interface Factory { + GitClient create(String repoRoot); } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDataUploaderImpl.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDataUploaderImpl.java index 80fb6ccd9cb..5cbc46a51c9 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDataUploaderImpl.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDataUploaderImpl.java @@ -7,9 +7,7 @@ import datadog.trace.api.git.GitInfoProvider; import datadog.trace.api.telemetry.LogCollector; import datadog.trace.civisibility.utils.FileUtils; -import datadog.trace.civisibility.utils.ShellCommandExecutor; import datadog.trace.util.AgentThreadFactory; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; @@ -31,6 +29,7 @@ public class GitDataUploaderImpl implements GitDataUploader { private final CiVisibilityMetricCollector metricCollector; private final GitDataApi gitDataApi; private final GitClient gitClient; + private final GitRepoUnshallow gitRepoUnshallow; private final GitInfoProvider gitInfoProvider; private final String repoRoot; private final String remoteName; @@ -42,6 +41,7 @@ public GitDataUploaderImpl( CiVisibilityMetricCollector metricCollector, GitDataApi gitDataApi, GitClient gitClient, + GitRepoUnshallow gitRepoUnshallow, GitInfoProvider gitInfoProvider, String repoRoot, String remoteName) { @@ -49,6 +49,7 @@ public GitDataUploaderImpl( this.metricCollector = metricCollector; this.gitDataApi = gitDataApi; this.gitClient = gitClient; + this.gitRepoUnshallow = gitRepoUnshallow; this.gitInfoProvider = gitInfoProvider; this.repoRoot = repoRoot; this.remoteName = remoteName; @@ -89,10 +90,8 @@ private void uploadGitData() { try { LOGGER.debug("Starting git data upload, {}", gitClient); - if (config.isCiVisibilityGitUnshallowEnabled() - && !config.isCiVisibilityGitUnshallowDefer() - && gitClient.isShallow()) { - unshallowRepository(); + if (!config.isCiVisibilityGitUnshallowDefer()) { + gitRepoUnshallow.unshallow(); } GitInfo gitInfo = gitInfoProvider.getGitInfo(repoRoot); @@ -113,10 +112,7 @@ private void uploadGitData() { return; } - if (config.isCiVisibilityGitUnshallowEnabled() - && config.isCiVisibilityGitUnshallowDefer() - && gitClient.isShallow()) { - unshallowRepository(); + if (config.isCiVisibilityGitUnshallowDefer() && gitRepoUnshallow.unshallow()) { latestCommits = gitClient.getLatestCommits(); commitsToSkip = gitDataApi.searchCommits(remoteUrl, latestCommits); } @@ -184,29 +180,6 @@ private void removeShutdownHook() { } } - private void unshallowRepository() throws IOException, TimeoutException, InterruptedException { - long unshallowStart = System.currentTimeMillis(); - try { - gitClient.unshallow(GitClient.HEAD); - return; - } catch (ShellCommandExecutor.ShellCommandFailedException e) { - LOGGER.debug( - "Could not unshallow using HEAD - assuming HEAD points to a local commit that does not exist in the remote repo", - e); - } - - try { - String upstreamBranch = gitClient.getUpstreamBranchSha(); - gitClient.unshallow(upstreamBranch); - } catch (ShellCommandExecutor.ShellCommandFailedException e) { - LOGGER.debug( - "Could not unshallow using upstream branch - assuming currently checked out local branch does not track any remote branch", - e); - gitClient.unshallow(null); - } - LOGGER.debug("Repository unshallowing took {} ms", System.currentTimeMillis() - unshallowStart); - } - private void waitForUploadToFinish() { try { long uploadTimeoutMillis = config.getCiVisibilityGitUploadTimeoutMillis(); diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDiffParser.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDiffParser.java new file mode 100644 index 00000000000..53ba6cbae29 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitDiffParser.java @@ -0,0 +1,64 @@ +package datadog.trace.civisibility.git.tree; + +import datadog.trace.civisibility.diff.LineDiff; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.BitSet; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class GitDiffParser { + + private static final Pattern CHANGED_FILE_PATTERN = + Pattern.compile("^diff --git a/(?.+) b/(?.+)$"); + private static final Pattern CHANGED_LINES_PATTERN = + Pattern.compile("^@@ -\\d+(,\\d+)? \\+(?\\d+)(,(?\\d+))? @@"); + + public static @NonNull LineDiff parse(InputStream input) throws IOException { + Map linesByRelativePath = new HashMap<>(); + + BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(input, Charset.defaultCharset())); + String changedFile = null; + BitSet changedLines = null; + + String line; + while ((line = bufferedReader.readLine()) != null) { + Matcher changedFileMatcher = CHANGED_FILE_PATTERN.matcher(line); + if (changedFileMatcher.matches()) { + if (changedFile != null) { + linesByRelativePath.put(changedFile, changedLines); + } + changedFile = changedFileMatcher.group("newfilename"); + changedLines = new BitSet(); + + } else { + Matcher changedLinesMatcher = CHANGED_LINES_PATTERN.matcher(line); + while (changedLinesMatcher.find()) { + int startLine = Integer.parseInt(changedLinesMatcher.group("startline")); + String stringCount = changedLinesMatcher.group("count"); + int count = stringCount != null ? Integer.parseInt(stringCount) : 1; + if (changedLines == null) { + throw new IllegalStateException( + "Line " + + line + + " contains changed lines information, but no changed file info is available"); + } + changedLines.set(startLine, startLine + count); + } + } + } + + if (changedFile != null) { + linesByRelativePath.put(changedFile, changedLines); + } + + return new LineDiff(linesByRelativePath); + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitRepoUnshallow.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitRepoUnshallow.java new file mode 100644 index 00000000000..bc6acd5c9ec --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/GitRepoUnshallow.java @@ -0,0 +1,48 @@ +package datadog.trace.civisibility.git.tree; + +import datadog.trace.api.Config; +import datadog.trace.civisibility.utils.ShellCommandExecutor; +import java.io.IOException; +import java.util.concurrent.TimeoutException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class GitRepoUnshallow { + + private static final Logger LOGGER = LoggerFactory.getLogger(GitRepoUnshallow.class); + + private final Config config; + private final GitClient gitClient; + + public GitRepoUnshallow(Config config, GitClient gitClient) { + this.config = config; + this.gitClient = gitClient; + } + + public boolean unshallow() throws IOException, InterruptedException, TimeoutException { + if (!config.isCiVisibilityGitUnshallowEnabled() || !gitClient.isShallow()) { + return false; + } + + long unshallowStart = System.currentTimeMillis(); + try { + gitClient.unshallow(GitClient.HEAD); + } catch (ShellCommandExecutor.ShellCommandFailedException e) { + LOGGER.debug( + "Could not unshallow using HEAD - assuming HEAD points to a local commit that does not exist in the remote repo", + e); + } + + try { + String upstreamBranch = gitClient.getUpstreamBranchSha(); + gitClient.unshallow(upstreamBranch); + } catch (ShellCommandExecutor.ShellCommandFailedException e) { + LOGGER.debug( + "Could not unshallow using upstream branch - assuming currently checked out local branch does not track any remote branch", + e); + gitClient.unshallow(null); + } + LOGGER.debug("Repository unshallowing took {} ms", System.currentTimeMillis() - unshallowStart); + return true; + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/NoOpGitClient.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/NoOpGitClient.java new file mode 100644 index 00000000000..2e09358c40b --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/NoOpGitClient.java @@ -0,0 +1,134 @@ +package datadog.trace.civisibility.git.tree; + +import datadog.trace.civisibility.diff.LineDiff; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class NoOpGitClient implements GitClient { + + public static final GitClient INSTANCE = new NoOpGitClient(); + + private NoOpGitClient() {} + + @Override + public boolean isShallow() { + return false; + } + + @Override + public void unshallow(@Nullable String remoteCommitReference) { + // no op + } + + @Nullable + @Override + public String getGitFolder() { + return null; + } + + @Nullable + @Override + public String getRepoRoot() { + return null; + } + + @Nullable + @Override + public String getRemoteUrl(String remoteName) { + return null; + } + + @Nullable + @Override + public String getUpstreamBranchSha() { + return null; + } + + @Nullable + @Override + public String getCurrentBranch() { + return null; + } + + @NotNull + @Override + public List getTags(String commit) { + return Collections.emptyList(); + } + + @Nullable + @Override + public String getSha(String reference) { + return null; + } + + @Nullable + @Override + public String getFullMessage(String commit) { + return null; + } + + @Nullable + @Override + public String getAuthorName(String commit) { + return null; + } + + @Nullable + @Override + public String getAuthorEmail(String commit) { + return null; + } + + @Nullable + @Override + public String getAuthorDate(String commit) { + return null; + } + + @Nullable + @Override + public String getCommitterName(String commit) { + return null; + } + + @Nullable + @Override + public String getCommitterEmail(String commit) { + return null; + } + + @Nullable + @Override + public String getCommitterDate(String commit) { + return null; + } + + @NotNull + @Override + public List getLatestCommits() { + return Collections.emptyList(); + } + + @NotNull + @Override + public List getObjects( + Collection commitsToSkip, Collection commitsToInclude) { + return Collections.emptyList(); + } + + @Override + public Path createPackFiles(List objectHashes) { + return null; + } + + @Nullable + @Override + public LineDiff getGitDiff(String baseCommit, String targetCommit) { + return null; + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java new file mode 100644 index 00000000000..56c3cb4949d --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/git/tree/ShellGitClient.java @@ -0,0 +1,660 @@ +package datadog.trace.civisibility.git.tree; + +import datadog.communication.util.IOUtils; +import datadog.trace.api.Config; +import datadog.trace.api.civisibility.telemetry.CiVisibilityCountMetric; +import datadog.trace.api.civisibility.telemetry.CiVisibilityDistributionMetric; +import datadog.trace.api.civisibility.telemetry.CiVisibilityMetricCollector; +import datadog.trace.api.civisibility.telemetry.tag.Command; +import datadog.trace.civisibility.diff.LineDiff; +import datadog.trace.civisibility.utils.ShellCommandExecutor; +import datadog.trace.util.Strings; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.FileStore; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeoutException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ShellGitClient implements GitClient { + + private static final Logger LOGGER = LoggerFactory.getLogger(ShellGitClient.class); + + private static final String DD_TEMP_DIRECTORY_PREFIX = "dd-ci-vis-"; + + private final CiVisibilityMetricCollector metricCollector; + private final String repoRoot; + private final String latestCommitsSince; + private final int latestCommitsLimit; + private final ShellCommandExecutor commandExecutor; + + /** + * Creates a new git client + * + * @param metricCollector Telemetry metrics collector + * @param repoRoot Absolute path to Git repository root + * @param latestCommitsSince How far into the past the client should be looking when fetching Git + * data, e.g. {@code "1 month ago"} or {@code "2 years ago"} + * @param latestCommitsLimit Maximum client of commits that the client should be considering when + * fetching commit data + * @param timeoutMillis Timeout in milliseconds that is applied to executed Git commands + */ + ShellGitClient( + CiVisibilityMetricCollector metricCollector, + String repoRoot, + String latestCommitsSince, + int latestCommitsLimit, + long timeoutMillis) { + this.metricCollector = metricCollector; + this.repoRoot = repoRoot; + this.latestCommitsSince = latestCommitsSince; + this.latestCommitsLimit = latestCommitsLimit; + commandExecutor = new ShellCommandExecutor(new File(repoRoot), timeoutMillis); + } + + /** + * Checks whether the repo that the client is associated with is shallow + * + * @return {@code true} if current repo is shallow, {@code false} otherwise + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Override + public boolean isShallow() throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.CHECK_SHALLOW, + () -> { + String output = + commandExecutor + .executeCommand(IOUtils::readFully, "git", "rev-parse", "--is-shallow-repository") + .trim(); + return Boolean.parseBoolean(output); + }); + } + + /** + * Returns the SHA of the head commit of the upstream (remote tracking) branch for the currently + * checked-out local branch. If the local branch is not tracking any remote branches, a {@link + * datadog.trace.civisibility.utils.ShellCommandExecutor.ShellCommandFailedException} exception + * will be thrown. + * + * @return The name of the upstream branch if the current local branch is tracking any. + * @throws ShellCommandExecutor.ShellCommandFailedException If the Git command fails with an error + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getUpstreamBranchSha() throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "rev-parse", "@{upstream}") + .trim()); + } + + /** + * "Unshallows" the repo that the client is associated with by fetching missing commit data from + * the server. + * + * @param remoteCommitReference The commit to fetch from the remote repository, so local repo will + * be updated with this commit and its ancestors. If {@code null}, everything will be fetched. + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Override + public void unshallow(@Nullable String remoteCommitReference) + throws IOException, TimeoutException, InterruptedException { + executeCommand( + Command.UNSHALLOW, + () -> { + String remote = + commandExecutor + .executeCommand( + IOUtils::readFully, + "git", + "config", + "--default", + "origin", + "--get", + "clone.defaultRemoteName") + .trim(); + + // refetch data from the server for the given period of time + if (remoteCommitReference != null) { + String headSha = getSha(remoteCommitReference); + commandExecutor.executeCommand( + ShellCommandExecutor.OutputParser.IGNORE, + "git", + "fetch", + String.format("--shallow-since=='%s'", latestCommitsSince), + "--update-shallow", + "--filter=blob:none", + "--recurse-submodules=no", + remote, + headSha); + } else { + commandExecutor.executeCommand( + ShellCommandExecutor.OutputParser.IGNORE, + "git", + "fetch", + String.format("--shallow-since=='%s'", latestCommitsSince), + "--update-shallow", + "--filter=blob:none", + "--recurse-submodules=no", + remote); + } + + return (Void) null; + }); + } + + /** + * Returns the absolute path of the .git directory. + * + * @return absolute path + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getGitFolder() throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "rev-parse", "--absolute-git-dir") + .trim()); + } + + /** + * Returns the absolute path to Git repository + * + * @return absolute path + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getRepoRoot() throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "rev-parse", "--show-toplevel") + .trim()); + } + + /** + * Returns URL of the remote with the given name + * + * @param remoteName Name of the remote + * @return URL of the given remote + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getRemoteUrl(String remoteName) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.GET_REPOSITORY, + () -> + commandExecutor + .executeCommand( + IOUtils::readFully, "git", "config", "--get", "remote." + remoteName + ".url") + .trim()); + } + + /** + * Returns current branch, or an empty string if HEAD is not pointing to a branch + * + * @return current branch, or an empty string if HEAD is not pointing to a branch + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getCurrentBranch() throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.GET_BRANCH, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "branch", "--show-current") + .trim()); + } + + /** + * Returns list of tags that provided commit points to + * + * @param commit Commit SHA or reference (HEAD, branch name, etc) to check + * @return list of tags that the commit is pointing to, or empty list if there are no such tags + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nonnull + @Override + public List getTags(String commit) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> { + try { + return commandExecutor.executeCommand( + IOUtils::readLines, "git", "describe", "--tags", "--exact-match", commit); + } catch (ShellCommandExecutor.ShellCommandFailedException e) { + // if provided commit is not tagged, + // command will fail because "--exact-match" is specified + return Collections.emptyList(); + } + }); + } + + /** + * Returns SHA of the provided reference + * + * @param reference Reference (HEAD, branch name, etc) to check + * @return full SHA of the provided reference + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getSha(String reference) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "rev-parse", reference) + .trim()); + } + + /** + * Returns full message of the provided commit + * + * @param commit Commit SHA or reference (HEAD, branch name, etc) to check + * @return full message of the provided commit + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getFullMessage(String commit) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%B", commit) + .trim()); + } + + /** + * Returns author name for the provided commit + * + * @param commit Commit SHA or reference (HEAD, branch name, etc) to check + * @return author name for the provided commit + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getAuthorName(String commit) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%an", commit) + .trim()); + } + + /** + * Returns author email for the provided commit + * + * @param commit Commit SHA or reference (HEAD, branch name, etc) to check + * @return author email for the provided commit + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getAuthorEmail(String commit) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%ae", commit) + .trim()); + } + + /** + * Returns author date in strict ISO 8601 format for the provided commit + * + * @param commit Commit SHA or reference (HEAD, branch name, etc) to check + * @return author date in strict ISO 8601 format + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getAuthorDate(String commit) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%aI", commit) + .trim()); + } + + /** + * Returns committer name for the provided commit + * + * @param commit Commit SHA or reference (HEAD, branch name, etc) to check + * @return committer name for the provided commit + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getCommitterName(String commit) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%cn", commit) + .trim()); + } + + /** + * Returns committer email for the provided commit + * + * @param commit Commit SHA or reference (HEAD, branch name, etc) to check + * @return committer email for the provided commit + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getCommitterEmail(String commit) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%ce", commit) + .trim()); + } + + /** + * Returns committer date in strict ISO 8601 format for the provided commit + * + * @param commit Commit SHA or reference (HEAD, branch name, etc) to check + * @return committer date in strict ISO 8601 format + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public String getCommitterDate(String commit) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.OTHER, + () -> + commandExecutor + .executeCommand(IOUtils::readFully, "git", "log", "-n", "1", "--format=%cI", commit) + .trim()); + } + + /** + * Returns SHAs of the latest commits in the current branch. Maximum number of commits and how far + * into the past to look are configured when the client is created. + * + * @return SHAs of latest commits + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nonnull + @Override + public List getLatestCommits() + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.GET_LOCAL_COMMITS, + () -> + commandExecutor.executeCommand( + IOUtils::readLines, + "git", + "log", + "--format=%H", + "-n", + String.valueOf(latestCommitsLimit), + String.format("--since='%s'", latestCommitsSince))); + } + + /** + * Returns SHAs of all Git objects in the current branch. Lookup is started from HEAD commit. + * Maximum number of commits and how far into the past to look are configured when the client is + * created. + * + * @param commitsToSkip List of commits to skip + * @return SHAs of relevant Git objects + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nonnull + @Override + public List getObjects( + Collection commitsToSkip, Collection commitsToInclude) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.GET_OBJECTS, + () -> { + String[] command = new String[6 + commitsToSkip.size() + commitsToInclude.size()]; + command[0] = "git"; + command[1] = "rev-list"; + command[2] = "--objects"; + command[3] = "--no-object-names"; + command[4] = "--filter=blob:none"; + command[5] = String.format("--since='%s'", latestCommitsSince); + + int count = 6; + for (String commitToSkip : commitsToSkip) { + command[count++] = "^" + commitToSkip; + } + for (String commitToInclude : commitsToInclude) { + command[count++] = commitToInclude; + } + + return commandExecutor.executeCommand(IOUtils::readLines, command); + }); + } + + /** + * Creates Git .pack files that include provided objects. The files are created in a temporary + * folder. + * + * @param objectHashes SHAs of objects that should be included in the pack files + * @return Path to temporary folder where created pack files are located + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Override + public Path createPackFiles(List objectHashes) + throws IOException, TimeoutException, InterruptedException { + return executeCommand( + Command.PACK_OBJECTS, + () -> { + byte[] input = String.join("\n", objectHashes).getBytes(Charset.defaultCharset()); + + Path tempDirectory = createTempDirectory(); + String basename = Strings.random(8); + String path = tempDirectory.toString() + File.separator + basename; + + commandExecutor.executeCommand( + ShellCommandExecutor.OutputParser.IGNORE, + input, + "git", + "pack-objects", + "--compression=9", + "--max-pack-size=3m", + path); + return tempDirectory; + }); + } + + private Path createTempDirectory() throws IOException { + Path repoRootDirectory = Paths.get(repoRoot); + FileStore repoRootFileStore = Files.getFileStore(repoRootDirectory); + + Path tempDirectory = Files.createTempDirectory(DD_TEMP_DIRECTORY_PREFIX); + FileStore tempDirectoryStore = Files.getFileStore(tempDirectory); + + if (Objects.equals(tempDirectoryStore, repoRootFileStore)) { + return tempDirectory; + } else { + // default temp-file directory and repo root are located on different devices, + // so we have to create our temp dir inside repo root + // otherwise git command will fail + Files.delete(tempDirectory); + return Files.createTempDirectory(repoRootDirectory, DD_TEMP_DIRECTORY_PREFIX); + } + } + + /** + * Returns Git diff between two commits. + * + * @param baseCommit Commit SHA or reference (HEAD, branch name, etc) of the base commit + * @param targetCommit Commit SHA or reference (HEAD, branch name, etc) of the target commit + * @return Diff between two commits + * @throws IOException If an error was encountered while writing command input or reading output + * @throws TimeoutException If timeout was reached while waiting for Git command to finish + * @throws InterruptedException If current thread was interrupted while waiting for Git command to + * finish + */ + @Nullable + @Override + public LineDiff getGitDiff(String baseCommit, String targetCommit) + throws IOException, TimeoutException, InterruptedException { + if (Strings.isNotBlank(baseCommit) && Strings.isNotBlank(targetCommit)) { + return executeCommand( + Command.DIFF, + () -> + commandExecutor.executeCommand( + GitDiffParser::parse, + "git", + "diff", + "-U0", + "--word-diff=porcelain", + baseCommit, + targetCommit)); + } else { + LOGGER.debug( + "Base commit and/or target commit info is not available, returning empty git diff: {}/{}", + baseCommit, + targetCommit); + return null; + } + } + + @Override + public String toString() { + return "GitClient{" + repoRoot + "}"; + } + + private interface GitCommand { + T execute() throws IOException, TimeoutException, InterruptedException; + } + + private T executeCommand(Command commandType, GitCommand command) + throws IOException, TimeoutException, InterruptedException { + long startTime = System.currentTimeMillis(); + try { + return command.execute(); + + } catch (IOException | TimeoutException | InterruptedException e) { + metricCollector.add( + CiVisibilityCountMetric.GIT_COMMAND_ERRORS, + 1, + commandType, + ShellCommandExecutor.getExitCode(e)); + throw e; + + } finally { + metricCollector.add(CiVisibilityCountMetric.GIT_COMMAND, 1, commandType); + metricCollector.add( + CiVisibilityDistributionMetric.GIT_COMMAND_MS, + (int) (System.currentTimeMillis() - startTime), + commandType); + } + } + + public static class Factory implements GitClient.Factory { + private final Config config; + private final CiVisibilityMetricCollector metricCollector; + + public Factory(Config config, CiVisibilityMetricCollector metricCollector) { + this.config = config; + this.metricCollector = metricCollector; + } + + @Override + public GitClient create(String repoRoot) { + long commandTimeoutMillis = config.getCiVisibilityGitCommandTimeoutMillis(); + return new ShellGitClient( + metricCollector, repoRoot, "1 month ago", 1000, commandTimeoutMillis); + } + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ErrorResponse.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ErrorResponse.java index 9fa98389558..73e4af57a8c 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ErrorResponse.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ErrorResponse.java @@ -1,5 +1,6 @@ package datadog.trace.civisibility.ipc; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; public class ErrorResponse implements SignalResponse { diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ExecutionSettingsRequest.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ExecutionSettingsRequest.java index 63c57f11fc2..778e0b8c341 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ExecutionSettingsRequest.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ExecutionSettingsRequest.java @@ -1,6 +1,7 @@ package datadog.trace.civisibility.ipc; import datadog.trace.civisibility.config.JvmInfo; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; import java.util.Objects; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ModuleCoverageDataJacoco.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ModuleCoverageDataJacoco.java index c386316716b..5e0da399514 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ModuleCoverageDataJacoco.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ModuleCoverageDataJacoco.java @@ -1,6 +1,7 @@ package datadog.trace.civisibility.ipc; import datadog.trace.api.DDTraceId; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Objects; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ModuleExecutionResult.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ModuleExecutionResult.java index 9cec6f2711a..9473acaddb9 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ModuleExecutionResult.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/ModuleExecutionResult.java @@ -1,6 +1,7 @@ package datadog.trace.civisibility.ipc; import datadog.trace.api.DDTraceId; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; import java.util.Collection; import java.util.Objects; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/TestFramework.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/TestFramework.java index fe064eb2409..3402c8227eb 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/TestFramework.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/TestFramework.java @@ -1,5 +1,6 @@ package datadog.trace.civisibility.ipc; +import datadog.trace.civisibility.ipc.serialization.Serializer; import java.nio.ByteBuffer; import java.util.Objects; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/serialization/PolymorphicSerializer.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/serialization/PolymorphicSerializer.java new file mode 100644 index 00000000000..25cb32ddc86 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/serialization/PolymorphicSerializer.java @@ -0,0 +1,82 @@ +package datadog.trace.civisibility.ipc.serialization; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +public class PolymorphicSerializer { + private final Map> deserializers = + new HashMap<>(); + private final Map, Byte> ids = new HashMap<>(); + + @SafeVarargs + public PolymorphicSerializer(Class... types) { + for (Class type : types) { + register(type); + } + } + + private void register(Class type) { + byte id = (byte) (ids.size() + 1); + Function deserializer = findDeserializer(type); + deserializers.put(id, deserializer); + ids.put(type, id); + } + + private Function findDeserializer(Class type) { + for (Method method : type.getDeclaredMethods()) { + boolean isStatic = (method.getModifiers() & Modifier.STATIC) != 0; + Class[] parameterTypes = method.getParameterTypes(); + if (isStatic + && method.getReturnType() == type + && parameterTypes.length == 1 + && parameterTypes[0] == ByteBuffer.class) { + return toDeserializer(method); + } + } + throw new IllegalArgumentException( + "Could not find a static method that accepts ByteBuffer and returns " + + type.getName() + + "in " + + type.getName()); + } + + @SuppressWarnings("unchecked") + private Function toDeserializer(Method method) { + return bb -> { + try { + return (T) method.invoke(null, bb); + } catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + + public void serialize(ParentType o, Serializer s) { + if (o == null) { + s.write(0); + return; + } + Byte id = ids.get(o.getClass()); + if (id == null) { + throw new IllegalArgumentException("Unknown type: " + o.getClass().getName()); + } + s.write(id); + o.serialize(s); + } + + public ParentType deserialize(ByteBuffer buffer) { + byte id = Serializer.readByte(buffer); + if (id == 0) { + return null; + } + Function deserializer = deserializers.get(id); + if (deserializer == null) { + throw new IllegalArgumentException("Unknown type ID: " + id); + } + return deserializer.apply(buffer); + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/serialization/SerializableType.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/serialization/SerializableType.java new file mode 100644 index 00000000000..bfbb03f07cc --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/serialization/SerializableType.java @@ -0,0 +1,6 @@ +package datadog.trace.civisibility.ipc.serialization; + +public interface SerializableType { + + void serialize(Serializer s); +} diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/Serializer.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/serialization/Serializer.java similarity index 91% rename from dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/Serializer.java rename to dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/serialization/Serializer.java index e23207a11d9..3c03dad6e21 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/Serializer.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/ipc/serialization/Serializer.java @@ -1,9 +1,10 @@ -package datadog.trace.civisibility.ipc; +package datadog.trace.civisibility.ipc.serialization; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.BitSet; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -94,6 +95,14 @@ public void write( } } + public void write(BitSet bitSet) { + if (bitSet != null) { + write(bitSet.toByteArray()); + } else { + write((byte[]) null); + } + } + public int length() { return baos.size(); } @@ -179,4 +188,9 @@ public static Map readMap( } return m; } + + public static BitSet readBitSet(ByteBuffer byteBuffer) { + byte[] bytes = readByteArray(byteBuffer); + return bytes != null ? BitSet.valueOf(bytes) : null; + } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndex.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndex.java index 3f562f31a8a..06c3462404b 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndex.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/source/index/RepoIndex.java @@ -2,7 +2,7 @@ import datadog.trace.api.Config; import datadog.trace.api.civisibility.domain.Language; -import datadog.trace.civisibility.ipc.Serializer; +import datadog.trace.civisibility.ipc.serialization.Serializer; import datadog.trace.civisibility.source.SourceResolutionException; import datadog.trace.civisibility.source.Utils; import datadog.trace.util.ClassNameTrie; diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/test/ExecutionStrategy.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/test/ExecutionStrategy.java index 819a54fa2c9..ae3fcee1792 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/test/ExecutionStrategy.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/test/ExecutionStrategy.java @@ -3,30 +3,46 @@ import datadog.trace.api.Config; import datadog.trace.api.civisibility.config.TestIdentifier; import datadog.trace.api.civisibility.config.TestMetadata; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.civisibility.config.EarlyFlakeDetectionSettings; import datadog.trace.civisibility.config.ExecutionSettings; import datadog.trace.civisibility.retry.NeverRetry; import datadog.trace.civisibility.retry.RetryIfFailed; import datadog.trace.civisibility.retry.RetryNTimes; +import datadog.trace.civisibility.source.LinesResolver; +import datadog.trace.civisibility.source.SourcePathResolver; +import java.lang.reflect.Method; import java.util.Collection; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.LongAdder; import javax.annotation.Nonnull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ExecutionStrategy { + private static final Logger LOGGER = LoggerFactory.getLogger(ExecutionStrategy.class); + private final LongAdder testsSkipped = new LongAdder(); private final AtomicInteger earlyFlakeDetectionsUsed = new AtomicInteger(0); private final AtomicInteger autoRetriesUsed = new AtomicInteger(0); @Nonnull private final Config config; @Nonnull private final ExecutionSettings executionSettings; - - public ExecutionStrategy(@Nonnull Config config, @Nonnull ExecutionSettings executionSettings) { + @Nonnull private final SourcePathResolver sourcePathResolver; + @Nonnull private final LinesResolver linesResolver; + + public ExecutionStrategy( + @Nonnull Config config, + @Nonnull ExecutionSettings executionSettings, + @Nonnull SourcePathResolver sourcePathResolver, + @Nonnull LinesResolver linesResolver) { this.config = config; this.executionSettings = executionSettings; + this.sourcePathResolver = sourcePathResolver; + this.linesResolver = linesResolver; } @Nonnull @@ -72,42 +88,40 @@ public boolean skip(TestIdentifier test) { } @Nonnull - public TestRetryPolicy retryPolicy(TestIdentifier test) { - if (test != null) { - EarlyFlakeDetectionSettings earlyFlakeDetectionSettings = - executionSettings.getEarlyFlakeDetectionSettings(); - if (earlyFlakeDetectionSettings.isEnabled()) { - Collection knownTests = executionSettings.getKnownTests(); - if (knownTests != null - && !knownTests.contains(test.withoutParameters()) - && !isEarlyFlakeDetectionLimitReached()) { - // check-then-act with "earlyFlakeDetectionsUsed" is not atomic here, - // but we don't care if we go "a bit" over the limit, it does not have to be precise - earlyFlakeDetectionsUsed.incrementAndGet(); - return new RetryNTimes(earlyFlakeDetectionSettings); - } + public TestRetryPolicy retryPolicy(TestIdentifier test, TestSourceData testSource) { + if (test == null) { + return NeverRetry.INSTANCE; + } + + EarlyFlakeDetectionSettings efdSettings = executionSettings.getEarlyFlakeDetectionSettings(); + if (efdSettings.isEnabled() && !isEFDLimitReached()) { + if (isNew(test) || isModified(testSource)) { + // check-then-act with "earlyFlakeDetectionsUsed" is not atomic here, + // but we don't care if we go "a bit" over the limit, it does not have to be precise + earlyFlakeDetectionsUsed.incrementAndGet(); + return new RetryNTimes(efdSettings); } + } - if (executionSettings.isFlakyTestRetriesEnabled()) { - Collection flakyTests = executionSettings.getFlakyTests(); - if ((flakyTests == null || flakyTests.contains(test.withoutParameters())) - && autoRetriesUsed.get() < config.getCiVisibilityTotalFlakyRetryCount()) { - // check-then-act with "autoRetriesUsed" is not atomic here, - // but we don't care if we go "a bit" over the limit, it does not have to be precise - return new RetryIfFailed(config.getCiVisibilityFlakyRetryCount(), autoRetriesUsed); - } + if (executionSettings.isFlakyTestRetriesEnabled()) { + Collection flakyTests = executionSettings.getFlakyTests(); + if ((flakyTests == null || flakyTests.contains(test.withoutParameters())) + && autoRetriesUsed.get() < config.getCiVisibilityTotalFlakyRetryCount()) { + // check-then-act with "autoRetriesUsed" is not atomic here, + // but we don't care if we go "a bit" over the limit, it does not have to be precise + return new RetryIfFailed(config.getCiVisibilityFlakyRetryCount(), autoRetriesUsed); } } return NeverRetry.INSTANCE; } - public boolean isEarlyFlakeDetectionLimitReached() { - int detectionsUsed = earlyFlakeDetectionsUsed.get(); + public boolean isEFDLimitReached() { Collection knownTests = executionSettings.getKnownTests(); if (knownTests == null) { return false; } + int detectionsUsed = earlyFlakeDetectionsUsed.get(); int totalTests = knownTests.size() + detectionsUsed; EarlyFlakeDetectionSettings earlyFlakeDetectionSettings = executionSettings.getEarlyFlakeDetectionSettings(); @@ -118,4 +132,37 @@ public boolean isEarlyFlakeDetectionLimitReached() { return detectionsUsed > threshold; } + + public boolean isModified(TestSourceData testSourceData) { + Class testClass = testSourceData.getTestClass(); + if (testClass == null) { + return false; + } + try { + String sourcePath = sourcePathResolver.getSourcePath(testClass); + if (sourcePath == null) { + return false; + } + + LinesResolver.Lines lines = getLines(testSourceData.getTestMethod()); + return executionSettings + .getPullRequestDiff() + .contains(sourcePath, lines.getStartLineNumber(), lines.getEndLineNumber()); + + } catch (Exception e) { + LOGGER.error("Could not determine if {} was modified, assuming false", testSourceData, e); + return false; + } + } + + private LinesResolver.Lines getLines(Method testMethod) { + if (testMethod == null) { + // method for this test case could not be determined, + // so we fall back to lower granularity + // and assume that the test was modified if there were any changes in the file + return new LinesResolver.Lines(0, Integer.MAX_VALUE); + } else { + return linesResolver.getMethodLines(testMethod); + } + } } diff --git a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/utils/ShellCommandExecutor.java b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/utils/ShellCommandExecutor.java index 0cb2855da8e..402ef4ecccf 100644 --- a/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/utils/ShellCommandExecutor.java +++ b/dd-java-agent/agent-ci-visibility/src/main/java/datadog/trace/civisibility/utils/ShellCommandExecutor.java @@ -1,6 +1,7 @@ package datadog.trace.civisibility.utils; import datadog.communication.util.IOUtils; +import datadog.trace.api.civisibility.telemetry.tag.ExitCode; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import datadog.trace.context.TraceScope; import datadog.trace.util.AgentThreadFactory; @@ -228,4 +229,19 @@ public int getExitCode() { return exitCode; } } + + public static ExitCode getExitCode(Exception e) { + if (e instanceof ShellCommandFailedException) { + ShellCommandFailedException scfe = (ShellCommandFailedException) e; + return ExitCode.from(scfe.getExitCode()); + + } else { + String m = e.getMessage(); + if (m != null && m.toLowerCase().contains("no such file or directory")) { + return ExitCode.EXECUTABLE_MISSING; + } else { + return ExitCode.CODE_UNKNOWN; + } + } + } } diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/CiVisibilityRepoServicesTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/CiVisibilityRepoServicesTest.groovy index 8d3fda38037..0250b4452f2 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/CiVisibilityRepoServicesTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/CiVisibilityRepoServicesTest.groovy @@ -1,8 +1,6 @@ package datadog.trace.civisibility - import datadog.trace.api.Config -import datadog.trace.civisibility.ci.CIInfo import spock.lang.Specification import java.nio.file.Paths @@ -18,11 +16,8 @@ class CiVisibilityRepoServicesTest extends Specification { def repoRoot = "/path/to/repo/root/" def path = Paths.get(repoRoot + repoSubFolder) - def ciInfo = Stub(CIInfo) - ciInfo.getNormalizedCiWorkspace() >> repoRoot - expect: - CiVisibilityRepoServices.getModuleName(config, path, ciInfo) == moduleName + CiVisibilityRepoServices.getModuleName(config, repoRoot, path) == moduleName where: parentModuleName | repoSubFolder | serviceName | moduleName diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/TestUtils.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/TestUtils.groovy new file mode 100644 index 00000000000..65fd735b2eb --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/TestUtils.groovy @@ -0,0 +1,18 @@ +package datadog.trace.civisibility + +abstract class TestUtils { + + private TestUtils() {} + + static BitSet lines(int ... setBits) { + return bitset(setBits) + } + + static BitSet bitset(int ... setBits) { + BitSet bitSet = new BitSet() + for (int bit : setBits) { + bitSet.set(bit) + } + return bitSet + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/CITagsProviderTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/CITagsProviderTest.groovy index 5c0307c005b..03dad64ba09 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/CITagsProviderTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/CITagsProviderTest.groovy @@ -51,7 +51,7 @@ abstract class CITagsProviderTest extends Specification { def ciProviderInfo = ciProviderInfoFactory.createCIProviderInfo(getWorkspacePath()) def ciInfo = ciProviderInfo.buildCIInfo() def ciTagsProvider = ciTagsProvider() - def tags = ciTagsProvider.getCiTags(ciInfo) + def tags = ciTagsProvider.getCiTags(ciInfo, PullRequestInfo.EMPTY) then: if (isCi()) { @@ -76,7 +76,7 @@ abstract class CITagsProviderTest extends Specification { def ciProviderInfo = ciProviderInfoFactory.createCIProviderInfo(getWorkspacePath()) def ciInfo = ciProviderInfo.buildCIInfo() def ciTagsProvider = ciTagsProvider() - def tags = ciTagsProvider.getCiTags(ciInfo) + def tags = ciTagsProvider.getCiTags(ciInfo, PullRequestInfo.EMPTY) then: tags.get(Tags.GIT_COMMIT_SHA) == "1234567890123456789012345678901234567890" @@ -95,7 +95,7 @@ abstract class CITagsProviderTest extends Specification { def ciProviderInfo = ciProviderInfoFactory.createCIProviderInfo(getWorkspacePath()) def ciInfo = ciProviderInfo.buildCIInfo() def ciTagsProvider = ciTagsProvider() - def tags = ciTagsProvider.getCiTags(ciInfo) + def tags = ciTagsProvider.getCiTags(ciInfo, PullRequestInfo.EMPTY) then: tags.get(Tags.GIT_REPOSITORY_URL) == "local supplied repo url" @@ -112,7 +112,7 @@ abstract class CITagsProviderTest extends Specification { def ciProviderInfo = ciProviderInfoFactory.createCIProviderInfo(getWorkspacePath()) def ciInfo = ciProviderInfo.buildCIInfo() def ciTagsProvider = ciTagsProvider() - def tags = ciTagsProvider.getCiTags(ciInfo) + def tags = ciTagsProvider.getCiTags(ciInfo, PullRequestInfo.EMPTY) then: if (isWorkspaceAwareCi()) { @@ -143,7 +143,7 @@ abstract class CITagsProviderTest extends Specification { CIProviderInfoFactory ciProviderInfoFactory = new CIProviderInfoFactory(Config.get(), GIT_FOLDER_FOR_TESTS, new CiEnvironmentImpl(System.getenv())) def ciProviderInfo = ciProviderInfoFactory.createCIProviderInfo(getWorkspacePath()) def ciInfo = ciProviderInfo.buildCIInfo() - def tags = ciTagsProvider.getCiTags(ciInfo) + def tags = ciTagsProvider.getCiTags(ciInfo, PullRequestInfo.EMPTY) tags.get(Tags.GIT_REPOSITORY_URL) == "https://some-host/some-user/some-repo.git" tags.get(Tags.GIT_BRANCH) == "master" diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/GithubActionsInfoTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/GithubActionsInfoTest.groovy index c54503cb0a7..ca5e905e0a7 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/GithubActionsInfoTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/GithubActionsInfoTest.groovy @@ -28,7 +28,7 @@ class GithubActionsInfoTest extends CITagsProviderTest { return map } - def "test GitHub event parsing and additional tags"() { + def "test pull request info parsing"() { setup: def githubEvent = GithubActionsInfoTest.getResource("/ci/github-event.json") def githubEventPath = Paths.get(githubEvent.toURI()) @@ -37,13 +37,11 @@ class GithubActionsInfoTest extends CITagsProviderTest { environmentVariables.set(GithubActionsInfo.GITHUB_EVENT_PATH, githubEventPath.toString()) when: - def cIInfo = new GithubActionsInfo(new CiEnvironmentImpl(System.getenv())).buildCIInfo() + def pullRequestInfo = new GithubActionsInfo(new CiEnvironmentImpl(System.getenv())).buildPullRequestInfo() then: - cIInfo.getAdditionalTags() == [ - (GithubActionsInfo.GIT_PULL_REQUEST_BASE_BRANCH) : "base-ref", - (GithubActionsInfo.GIT_PULL_REQUEST_BASE_BRANCH_SHA) : "52e0974c74d41160a03d59ddc73bb9f5adab054b", - (GithubActionsInfo.GIT_COMMIT_HEAD_SHA) : "df289512a51123083a8e6931dd6f57bb3883d4c4", - ] + pullRequestInfo.pullRequestBaseBranch == "base-ref" + pullRequestInfo.pullRequestBaseBranchSha == "52e0974c74d41160a03d59ddc73bb9f5adab054b" + pullRequestInfo.gitCommitHeadSha == "df289512a51123083a8e6931dd6f57bb3883d4c4" } } diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/UnknownCIInfoTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/UnknownCIInfoTest.groovy index 77d1dda2f7b..22a7ce54825 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/UnknownCIInfoTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ci/UnknownCIInfoTest.groovy @@ -49,7 +49,7 @@ class UnknownCIInfoTest extends CITagsProviderTest { def ciProviderInfo = ciProviderInfoFactory.createCIProviderInfo(workspaceForTests) def ciInfo = ciProviderInfo.buildCIInfo() def ciTagsProvider = ciTagsProvider() - def ciTags = ciTagsProvider.getCiTags(ciInfo) + def ciTags = ciTagsProvider.getCiTags(ciInfo, PullRequestInfo.EMPTY) then: ciTags == expectedTags @@ -69,7 +69,7 @@ class UnknownCIInfoTest extends CITagsProviderTest { def ciProviderInfo = ciProviderInfoFactory.createCIProviderInfo(workspaceForTests) def ciInfo = ciProviderInfo.buildCIInfo() def ciTagsProvider = new CITagsProvider(gitInfoProvider) - def ciTags = ciTagsProvider.getCiTags(ciInfo) + def ciTags = ciTagsProvider.getCiTags(ciInfo, PullRequestInfo.EMPTY) then: ciTags.get("$Tags.CI_WORKSPACE_PATH") == null diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/ConfigurationApiImplTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/ConfigurationApiImplTest.groovy index a87c43025a1..e6e472245c2 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/ConfigurationApiImplTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/ConfigurationApiImplTest.groovy @@ -67,7 +67,7 @@ class ConfigurationApiImplTest extends Specification { def response = response if (expectedRequest) { - def requestBody = ('{' + + def responseBody = ('{' + ' "data": {' + ' "type": "ci_app_tracers_test_service_settings",' + ' "id": "uuid",' + @@ -76,6 +76,7 @@ class ConfigurationApiImplTest extends Specification { ' "tests_skipping": true,' + ' "require_git": true,' + ' "flaky_test_retries_enabled": true,' + + ' "impacted_tests_enabled": true,' + ' "early_flake_detection": {' + ' "enabled": true,' + ' "slow_test_retries": {' + @@ -94,10 +95,10 @@ class ConfigurationApiImplTest extends Specification { def gzipSupported = header != null && header.contains("gzip") if (gzipSupported) { response.addHeader("Content-Encoding", "gzip") - requestBody = gzip(requestBody) + responseBody = gzip(responseBody) } - response.status(200).send(requestBody) + response.status(200).send(responseBody) } else { response.status(400).send() } @@ -186,7 +187,7 @@ class ConfigurationApiImplTest extends Specification { } def response = response - def requestBody = """ + def responseBody = """ { "data": [ ${tests.join(',')} @@ -206,10 +207,10 @@ class ConfigurationApiImplTest extends Specification { def gzipSupported = header != null && header.contains("gzip") if (gzipSupported) { response.addHeader("Content-Encoding", "gzip") - requestBody = gzip(requestBody) + responseBody = gzip(responseBody) } - response.status(200).send(requestBody) + response.status(200).send(responseBody) } prefix("/api/v2/ci/libraries/tests/flaky") { @@ -295,7 +296,7 @@ class ConfigurationApiImplTest extends Specification { } def response = response - def requestBody = """ + def responseBody = """ { "data": [ ${tests.join(',')} @@ -315,10 +316,10 @@ class ConfigurationApiImplTest extends Specification { def gzipSupported = header != null && header.contains("gzip") if (gzipSupported) { response.addHeader("Content-Encoding", "gzip") - requestBody = gzip(requestBody) + responseBody = gzip(responseBody) } - response.status(200).send(requestBody) + response.status(200).send(responseBody) } prefix("/api/v2/ci/libraries/tests") { @@ -353,7 +354,7 @@ class ConfigurationApiImplTest extends Specification { def response = response if (expectedRequest) { - def requestBody = (""" + def responseBody = (""" { "data": { "id": "9p1jTQLXB8g", @@ -386,14 +387,62 @@ class ConfigurationApiImplTest extends Specification { def gzipSupported = header != null && header.contains("gzip") if (gzipSupported) { response.addHeader("Content-Encoding", "gzip") - requestBody = gzip(requestBody) + responseBody = gzip(responseBody) } - response.status(200).send(requestBody) + response.status(200).send(responseBody) } else { response.status(400).send() } } + + prefix("/api/v2/ci/tests/diffs") { + def requestJson = moshi.adapter(Map).fromJson(new String(request.body)) + + // assert request contents + def requestData = requestJson['data'] + if (requestData['type'] != "ci_app_tests_diffs_request") { + response.status(400).send() + return + } + + def requestAttrs = requestData['attributes'] + if (requestAttrs['service'] != "foo" + || requestAttrs['env'] != "foo_env" + || requestAttrs['repository_url'] != "https://github.com/DataDog/foo" + || requestAttrs['branch'] != "prod" + || requestAttrs['sha'] != "d64185e45d1722ab3a53c45be47accae" + ) { + response.status(400).send() + return + } + + def response = response + def responseBody = """ +{ + "data": { + "type": "ci_app_tests_diffs_response", + "id": "", + "attributes": { + "base_sha": "ef733331f7cee9b1c89d82df87942d8606edf3f7", + "files": [ + "domains/ci-app/apps/apis/rapid-ci-app/internal/itrapihttp/api.go", + "domains/ci-app/apps/apis/rapid-ci-app/internal/itrapihttp/api_test.go" + ] + } + } +} +""".bytes + + def header = request.getHeader("Accept-Encoding") + def gzipSupported = header != null && header.contains("gzip") + if (gzipSupported) { + response.addHeader("Content-Encoding", "gzip") + responseBody = gzip(responseBody) + } + + response.status(200).send(responseBody) + } } } @@ -419,6 +468,7 @@ class ConfigurationApiImplTest extends Specification { settings.testsSkippingEnabled settings.gitUploadRequired settings.flakyTestRetriesEnabled + settings.impactedTestsDetectionEnabled settings.earlyFlakeDetectionSettings.enabled settings.earlyFlakeDetectionSettings.faultySessionThreshold == 30 settings.earlyFlakeDetectionSettings.getExecutions(TimeUnit.SECONDS.toMillis(3)) == 10 @@ -564,6 +614,23 @@ class ConfigurationApiImplTest extends Specification { givenIntakeApi(true) | "intake, response compression enabled" } + def "test changed files request"() { + given: + def tracerEnvironment = givenTracerEnvironment() + + when: + def api = givenIntakeApi(true) + def configurationApi = new ConfigurationApiImpl(api, Stub(CiVisibilityMetricCollector), () -> "1234") + def changedFiles = configurationApi.getChangedFiles(tracerEnvironment) + + then: + changedFiles.baseSha == "ef733331f7cee9b1c89d82df87942d8606edf3f7" + changedFiles.files == new HashSet([ + "domains/ci-app/apps/apis/rapid-ci-app/internal/itrapihttp/api.go", + "domains/ci-app/apps/apis/rapid-ci-app/internal/itrapihttp/api_test.go" + ]) + } + private BackendApi givenEvpProxy(boolean responseCompression) { String traceId = "a-trace-id" HttpUrl proxyUrl = HttpUrl.get(intakeServer.address) diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/EarlyFlakeDetectionSettingsSerializerTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/EarlyFlakeDetectionSettingsSerializerTest.groovy index 85dd38257f5..f52551066d7 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/EarlyFlakeDetectionSettingsSerializerTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/EarlyFlakeDetectionSettingsSerializerTest.groovy @@ -1,7 +1,7 @@ package datadog.trace.civisibility.config -import datadog.trace.civisibility.ipc.Serializer +import datadog.trace.civisibility.ipc.serialization.Serializer import spock.lang.Specification class EarlyFlakeDetectionSettingsSerializerTest extends Specification { diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/ExecutionSettingsTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/ExecutionSettingsTest.groovy index 921ced909f6..76846445f30 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/ExecutionSettingsTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/ExecutionSettingsTest.groovy @@ -1,10 +1,12 @@ package datadog.trace.civisibility.config - import datadog.trace.api.civisibility.config.TestIdentifier import datadog.trace.api.civisibility.config.TestMetadata +import datadog.trace.civisibility.diff.LineDiff import spock.lang.Specification +import static datadog.trace.civisibility.TestUtils.lines + class ExecutionSettingsTest extends Specification { def "test serialization: #settings"() { @@ -22,24 +24,28 @@ class ExecutionSettingsTest extends Specification { false, false, false, + false, EarlyFlakeDetectionSettings.DEFAULT, null, [:], [:], null, - new HashSet<>([])), + new HashSet<>([]), + LineDiff.EMPTY), new ExecutionSettings( true, true, false, true, + true, new EarlyFlakeDetectionSettings(true, [], 10), "", [new TestIdentifier("bc", "def", "g"): new TestMetadata(true), new TestIdentifier("de", "f", null): new TestMetadata(false)], [:], new HashSet<>([new TestIdentifier("name", null, null)]), - new HashSet<>([new TestIdentifier("b", "c", "g")]) + new HashSet<>([new TestIdentifier("b", "c", "g")]), + new LineDiff(["path": lines()]) ), new ExecutionSettings( @@ -47,16 +53,18 @@ class ExecutionSettingsTest extends Specification { false, true, false, + true, new EarlyFlakeDetectionSettings(true, [new EarlyFlakeDetectionSettings.ExecutionsByDuration(10, 20)], 10), "itrCorrelationId", [:], - ["cov": BitSet.valueOf(new byte[]{ + ["cov" : BitSet.valueOf(new byte[]{ 1, 2, 3 }), "cov2": BitSet.valueOf(new byte[]{ 4, 5, 6 })], new HashSet<>([new TestIdentifier("name", null, "g"), new TestIdentifier("b", "c", null)]), new HashSet<>([new TestIdentifier("b", "c", null), new TestIdentifier("bb", "cc", null)]), + new LineDiff(["path": lines(1, 2, 3)]), ), new ExecutionSettings( @@ -64,16 +72,18 @@ class ExecutionSettingsTest extends Specification { true, true, true, + true, new EarlyFlakeDetectionSettings(true, [new EarlyFlakeDetectionSettings.ExecutionsByDuration(10, 20), new EarlyFlakeDetectionSettings.ExecutionsByDuration(30, 40)], 10), "itrCorrelationId", [new TestIdentifier("bc", "def", null): new TestMetadata(true), new TestIdentifier("de", "f", null): new TestMetadata(true)], - ["cov": BitSet.valueOf(new byte[]{ + ["cov" : BitSet.valueOf(new byte[]{ 1, 2, 3 }), "cov2": BitSet.valueOf(new byte[]{ 4, 5, 6 })], new HashSet<>([]), new HashSet<>([new TestIdentifier("b", "c", null), new TestIdentifier("bb", "cc", "g")]), + new LineDiff(["path": lines(1, 2, 3), "path-b": lines(1, 2, 128, 257, 999)]), ), ] } diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/TestIdentifierSerializerTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/TestIdentifierSerializerTest.groovy index ac3b2db9da2..1530e1e5657 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/TestIdentifierSerializerTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/config/TestIdentifierSerializerTest.groovy @@ -1,7 +1,7 @@ package datadog.trace.civisibility.config import datadog.trace.api.civisibility.config.TestIdentifier -import datadog.trace.civisibility.ipc.Serializer +import datadog.trace.civisibility.ipc.serialization.Serializer import spock.lang.Specification class TestIdentifierSerializerTest extends Specification { diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/diff/DiffTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/diff/DiffTest.groovy new file mode 100644 index 00000000000..8e9c155ec71 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/diff/DiffTest.groovy @@ -0,0 +1,32 @@ +package datadog.trace.civisibility.diff + +import datadog.trace.civisibility.ipc.serialization.Serializer +import spock.lang.Specification + +import static datadog.trace.civisibility.TestUtils.lines + +class DiffTest extends Specification { + + def "test diffs serialization: #diff"() { + when: + def s = new Serializer() + Diff.SERIALIZER.serialize(diff, s) + def buffer = s.flush() + def diffCopy = Diff.SERIALIZER.deserialize(buffer) + + then: + diff == diffCopy + + where: + diff << [ + new FileDiff(Collections.emptySet()), + new FileDiff(Collections.singleton("path")), + new FileDiff(new HashSet<>(["path-a", "path-b"])), + new LineDiff([:]), + new LineDiff(["path": lines(10)]), + new LineDiff(["path": lines(10, 11, 13)]), + new LineDiff(["path": lines(10, 11, 12, 13), "path-b": lines()]), + new LineDiff(["path": lines(10, 11, 12, 13), "path-b": lines(8, 18)]) + ] + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/diff/FileDiffTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/diff/FileDiffTest.groovy new file mode 100644 index 00000000000..31bfbd1e2b2 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/diff/FileDiffTest.groovy @@ -0,0 +1,39 @@ +package datadog.trace.civisibility.diff + +import datadog.trace.civisibility.ipc.serialization.Serializer +import spock.lang.Specification + +class FileDiffTest extends Specification { + + def "test diff contains file"() { + when: + def diff = new FileDiff(new HashSet<>(paths)) + + then: + diff.contains(path, 0, Integer.MAX_VALUE) == result + + where: + paths | path | result + ["path-a"] | "path-a" | true + ["path-a"] | "path-b" | false + ["path-a", "path-b"] | "path-a" | true + ["path-a", "path-b"] | "path-b" | true + ["path-a", "path-b"] | "path-c" | false + } + + def "test serialization: #paths"() { + given: + def diff = new FileDiff(new HashSet<>(paths)) + + when: + def serializer = new Serializer() + diff.serialize(serializer) + def buf = serializer.flush() + + then: + FileDiff.deserialize(buf) == diff + + where: + paths << [[], ["path-a"], ["path-a", "path-b"]] + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/diff/LineDiffTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/diff/LineDiffTest.groovy new file mode 100644 index 00000000000..a2ee9d266b9 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/diff/LineDiffTest.groovy @@ -0,0 +1,61 @@ +package datadog.trace.civisibility.diff + + +import datadog.trace.civisibility.ipc.serialization.Serializer +import spock.lang.Specification + +import static datadog.trace.civisibility.TestUtils.lines + +class LineDiffTest extends Specification { + + def "test diff contains line interval"() { + when: + def diff = new LineDiff(lines) + + then: + diff.contains(path, interval[0], interval[1]) == result + + where: + lines | path | interval | result + ["path": lines(10)] | "path-b" | [0, 9999] | false + ["path": lines(10)] | "path-b" | [10, 10] | false + ["path": lines(10)] | "path" | [0, 9999] | true + ["path": lines(10)] | "path" | [10, 10] | true + ["path": lines(10)] | "path" | [9, 9] | false + ["path": lines(10)] | "path" | [11, 11] | false + ["path": lines(10)] | "path" | [9, 11] | true + ["path": lines(10, 11, 13)] | "path" | [9, 11] | true + ["path": lines(10, 11, 13)] | "path" | [12, 12] | false + ["path": lines(10, 11, 13)] | "path" | [12, 14] | true + ["path": lines(10, 11, 13)] | "path" | [9, 14] | true + ["path": lines(10, 11, 12, 13)] | "path" | [9, 11] | true + ["path": lines(10, 11, 12, 13)] | "path" | [9, 14] | true + ["path": lines(10, 11, 12, 13)] | "path" | [11, 12] | true + ["path": lines(10, 11, 12, 13)] | "path" | [12, 14] | true + ["path": lines(10, 11, 12, 13), "path-b": lines()] | "path-b" | [0, 9999] | false + ["path": lines(10, 11, 12, 13), "path-b": lines(8)] | "path-b" | [0, 9999] | true + ["path": lines(10, 11, 12, 13), "path-b": lines(8)] | "path-b" | [7, 8] | true + } + + def "test serialization: #lines"() { + given: + def diff = new LineDiff(lines) + + when: + def serializer = new Serializer() + diff.serialize(serializer) + def buf = serializer.flush() + + then: + LineDiff.deserialize(buf) == diff + + where: + lines << [ + [:], + ["path": lines(10)], + ["path": lines(10, 11, 13)], + ["path": lines(10, 11, 12, 13), "path-b": lines()], + ["path": lines(10, 11, 12, 13), "path-b": lines(8, 18)] + ] + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/domain/buildsystem/ProxyTestModuleTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/domain/buildsystem/ProxyTestModuleTest.groovy index 8f360f60f7e..5e6f4bdef74 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/domain/buildsystem/ProxyTestModuleTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/domain/buildsystem/ProxyTestModuleTest.groovy @@ -2,6 +2,7 @@ package datadog.trace.civisibility.domain.buildsystem import datadog.trace.api.Config import datadog.trace.api.DDTraceId +import datadog.trace.api.civisibility.config.TestSourceData import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext import datadog.trace.civisibility.config.EarlyFlakeDetectionSettings import datadog.trace.api.civisibility.config.TestIdentifier @@ -29,7 +30,7 @@ class ProxyTestModuleTest extends DDSpecification { config.getCiVisibilityFlakyRetryCount() >> 2 // this counts all executions of a test case (first attempt is counted too) config.getCiVisibilityTotalFlakyRetryCount() >> 2 // this counts retries across all tests (first attempt is not a retry, so it is not counted) - def executionStrategy = new ExecutionStrategy(config, executionSettings) + def executionStrategy = new ExecutionStrategy(config, executionSettings, Stub(SourcePathResolver), Stub(LinesResolver)) def traceId = Stub(DDTraceId) traceId.toLong() >> 123 @@ -55,21 +56,21 @@ class ProxyTestModuleTest extends DDSpecification { ) when: - def retryPolicy1 = proxyTestModule.retryPolicy(new TestIdentifier("suite", "test-1", null)) + def retryPolicy1 = proxyTestModule.retryPolicy(new TestIdentifier("suite", "test-1", null), TestSourceData.UNKNOWN) then: retryPolicy1.retry(false, 1L) // 2nd test execution, 1st retry globally !retryPolicy1.retry(false, 1L) // asking for 3rd test execution - local limit reached when: - def retryPolicy2 = proxyTestModule.retryPolicy(new TestIdentifier("suite", "test-2", null)) + def retryPolicy2 = proxyTestModule.retryPolicy(new TestIdentifier("suite", "test-2", null), TestSourceData.UNKNOWN) then: retryPolicy2.retry(false, 1L) // 2nd test execution, 2nd retry globally (since previous test was retried too) !retryPolicy2.retry(false, 1L) // asking for 3rd test execution - local limit reached when: - def retryPolicy3 = proxyTestModule.retryPolicy(new TestIdentifier("suite", "test-3", null)) + def retryPolicy3 = proxyTestModule.retryPolicy(new TestIdentifier("suite", "test-3", null), TestSourceData.UNKNOWN) then: !retryPolicy3.retry(false, 1L) // asking for 3rd retry globally - global limit reached diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/domain/headless/HeadlessTestModuleTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/domain/headless/HeadlessTestModuleTest.groovy index af803cdbf2d..4daf9dc7c44 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/domain/headless/HeadlessTestModuleTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/domain/headless/HeadlessTestModuleTest.groovy @@ -2,6 +2,7 @@ package datadog.trace.civisibility.domain.headless import datadog.trace.api.Config import datadog.trace.api.civisibility.config.TestIdentifier +import datadog.trace.api.civisibility.config.TestSourceData import datadog.trace.api.civisibility.coverage.CoverageStore import datadog.trace.api.civisibility.telemetry.CiVisibilityMetricCollector import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext @@ -28,7 +29,7 @@ class HeadlessTestModuleTest extends DDSpecification { config.getCiVisibilityTotalFlakyRetryCount() >> 2 // this counts retries across all tests (first attempt is not a retry, so it is not counted) - def executionStrategy = new ExecutionStrategy(config, executionSettings) + def executionStrategy = new ExecutionStrategy(config, executionSettings, Stub(SourcePathResolver), Stub(LinesResolver)) given: def headlessTestModule = new HeadlessTestModule( @@ -47,21 +48,21 @@ class HeadlessTestModuleTest extends DDSpecification { ) when: - def retryPolicy1 = headlessTestModule.retryPolicy(new TestIdentifier("suite", "test-1", null)) + def retryPolicy1 = headlessTestModule.retryPolicy(new TestIdentifier("suite", "test-1", null), TestSourceData.UNKNOWN) then: retryPolicy1.retry(false, 1L) // 2nd test execution, 1st retry globally !retryPolicy1.retry(false, 1L) // asking for 3rd test execution - local limit reached when: - def retryPolicy2 = headlessTestModule.retryPolicy(new TestIdentifier("suite", "test-2", null)) + def retryPolicy2 = headlessTestModule.retryPolicy(new TestIdentifier("suite", "test-2", null), TestSourceData.UNKNOWN) then: retryPolicy2.retry(false, 1L) // 2nd test execution, 2nd retry globally (since previous test was retried too) !retryPolicy2.retry(false, 1L) // asking for 3rd test execution - local limit reached when: - def retryPolicy3 = headlessTestModule.retryPolicy(new TestIdentifier("suite", "test-3", null)) + def retryPolicy3 = headlessTestModule.retryPolicy(new TestIdentifier("suite", "test-3", null), TestSourceData.UNKNOWN) then: !retryPolicy3.retry(false, 1L) // asking for 3rd retry globally - global limit reached diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/GitClientGitInfoBuilderTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/GitClientGitInfoBuilderTest.groovy index 62ecf600bec..9d2dc02a2ee 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/GitClientGitInfoBuilderTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/GitClientGitInfoBuilderTest.groovy @@ -2,7 +2,7 @@ package datadog.trace.civisibility.git import datadog.trace.api.Config import datadog.trace.civisibility.telemetry.CiVisibilityMetricCollectorImpl -import datadog.trace.civisibility.git.tree.GitClient +import datadog.trace.civisibility.git.tree.ShellGitClient import datadog.communication.util.IOUtils import spock.lang.Specification import spock.lang.TempDir @@ -29,7 +29,7 @@ class GitClientGitInfoBuilderTest extends Specification { config.getCiVisibilityGitRemoteName() >> "origin" config.getCiVisibilityGitCommandTimeoutMillis() >> GIT_COMMAND_TIMEOUT_MILLIS - def gitClientFactory = new GitClient.Factory(config, metricCollector) + def gitClientFactory = new ShellGitClient.Factory(config, metricCollector) def infoBuilder = new GitClientGitInfoBuilder(config, gitClientFactory) when: diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy index 88147a4c934..6dbb26c2565 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitClientTest.groovy @@ -11,6 +11,8 @@ import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths +import static datadog.trace.civisibility.TestUtils.lines + class GitClientTest extends Specification { private static final int GIT_COMMAND_TIMEOUT_MILLIS = 10_000 @@ -44,6 +46,18 @@ class GitClientTest extends Specification { shallow } + def "test repo root"() { + given: + givenGitRepo() + + when: + def gitClient = givenGitClient() + def repoRoot = gitClient.getRepoRoot() + + then: + repoRoot == tempDir.toRealPath().toString() + } + def "test get upstream branch SHA"() { given: givenGitRepo("ci/git/shallow/git") @@ -301,6 +315,20 @@ class GitClientTest extends Specification { gitPackObject.getType() == GitObject.COMMIT_TYPE } + def "test git diff"() { + given: + givenGitRepo() + + when: + def gitClient = givenGitClient() + def diff = gitClient.getGitDiff("10599ae3c17d66d642f9f143b1ff3dd236111e2a", "6aaa4085c10d16b63a910043e35dbd35d2ef7f1c") + + then: + diff.linesByRelativePath == [ + "src/Datadog.Trace/Logging/DatadogLogging.cs": lines(26, 32, 91, 95, 159, 160) + ] + } + private void givenGitRepo() { givenGitRepo("ci/git/with_pack/git") } @@ -314,6 +342,6 @@ class GitClientTest extends Specification { private givenGitClient() { def metricCollector = Stub(CiVisibilityMetricCollectorImpl) - new GitClient(metricCollector, tempDir.toString(), "25 years ago", 10, GIT_COMMAND_TIMEOUT_MILLIS) + new ShellGitClient(metricCollector, tempDir.toString(), "25 years ago", 10, GIT_COMMAND_TIMEOUT_MILLIS) } } diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitDataUploaderImplTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitDataUploaderImplTest.groovy index 31010087c96..04cbb090c37 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitDataUploaderImplTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitDataUploaderImplTest.groovy @@ -39,8 +39,9 @@ class GitDataUploaderImplTest extends Specification { def gitInfoProvider = Stub(GitInfoProvider) gitInfoProvider.getGitInfo(repoRoot) >> new GitInfo(repoUrl, null, null, null) - def gitClient = new GitClient(metricCollector, repoRoot, "25 years ago", 3, TIMEOUT_MILLIS) - def uploader = new GitDataUploaderImpl(config, metricCollector, api, gitClient, gitInfoProvider, repoRoot, "origin") + def gitClient = new ShellGitClient(metricCollector, repoRoot, "25 years ago", 3, TIMEOUT_MILLIS) + def unshallow = new GitRepoUnshallow(config, gitClient) + def uploader = new GitDataUploaderImpl(config, metricCollector, api, gitClient, unshallow, gitInfoProvider, repoRoot, "origin") when: def future = uploader.startOrObserveGitDataUpload() diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitDiffParserTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitDiffParserTest.groovy new file mode 100644 index 00000000000..5e0a4318143 --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/git/tree/GitDiffParserTest.groovy @@ -0,0 +1,32 @@ +package datadog.trace.civisibility.git.tree + +import spock.lang.Specification + +import static datadog.trace.civisibility.TestUtils.lines + +class GitDiffParserTest extends Specification { + + def "test git diff parsing: #filename"() { + when: + def diff + try (def gitDiff = GitDiffParserTest.getResourceAsStream(filename)) { + diff = GitDiffParser.parse(gitDiff) + } + + then: + diff.linesByRelativePath == result + + where: + filename | result + "git-diff.txt" | [ + "java/maven-junit4/pom.xml": lines(10), + "java/maven-junit5/pom.xml": lines(14, 41), + ] + "larger-git-diff.txt" | [ + "java/maven-junit5/pom.xml" : lines(14, 41), + "java/maven-junit5/module-a/pom.xml": lines(8, 9, 10, 13, 14, 15, 20, 21, 22, 27, 28, 36, 40), + "java/maven-junit5/module-b/pom.xml": lines(), + "java/maven-junit4/pom.xml" : lines(10) + ] + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/ModuleSettingsRequestTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/ExecutionSettingsRequestTest.groovy similarity index 95% rename from dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/ModuleSettingsRequestTest.groovy rename to dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/ExecutionSettingsRequestTest.groovy index 10785af9ede..2bac9fb1573 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/ModuleSettingsRequestTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/ExecutionSettingsRequestTest.groovy @@ -5,7 +5,7 @@ import spock.lang.Specification import java.nio.file.Paths -class ModuleSettingsRequestTest extends Specification { +class ExecutionSettingsRequestTest extends Specification { def "test serialization and deserialization: #signal"() { when: diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/serialization/PolymorphicSerializerTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/serialization/PolymorphicSerializerTest.groovy new file mode 100644 index 00000000000..d9f15395c9e --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/serialization/PolymorphicSerializerTest.groovy @@ -0,0 +1,77 @@ +package datadog.trace.civisibility.ipc.serialization + +import spock.lang.Specification + +import java.nio.ByteBuffer + +class PolymorphicSerializerTest extends Specification { + + def "test polymorphic serialization"() { + given: + PolymorphicSerializer serializer = new PolymorphicSerializer<>(ChildA, ChildB) + + when: + def s = new Serializer() + serializer.serialize(original, s) + def buffer = s.flush() + def copy = serializer.deserialize(buffer) + + then: + copy == original + + where: + original << [null, new ChildA(123), new ChildB("test")] + } + + private static interface Parent extends SerializableType {} + + private static class ChildA implements Parent { + private final int intField + + ChildA(int intField) { + this.intField = intField + } + + @Override + void serialize(Serializer s) { + s.write(intField) + } + + static ChildA deserialize(ByteBuffer buffer) { + return new ChildA(Serializer.readInt(buffer)) + } + + boolean equals(o) { + return o != null && getClass() == o.class && intField == ((ChildA) o).intField + } + + int hashCode() { + return intField + } + } + + private static class ChildB implements Parent { + private final String stringField + + ChildB(String stringField) { + this.stringField = stringField + } + + @Override + void serialize(Serializer s) { + s.write(stringField) + } + + static ChildB deserialize(ByteBuffer buffer) { + return new ChildB(Serializer.readString(buffer)) + } + + boolean equals(o) { + return o != null && getClass() == o.class && stringField == ((ChildB) o).stringField + } + + int hashCode() { + return stringField.hashCode() + } + } +} diff --git a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/SerializerTest.groovy b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/serialization/SerializerTest.groovy similarity index 89% rename from dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/SerializerTest.groovy rename to dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/serialization/SerializerTest.groovy index de3b4aeb4d5..efb45a67799 100644 --- a/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/SerializerTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/test/groovy/datadog/trace/civisibility/ipc/serialization/SerializerTest.groovy @@ -1,9 +1,12 @@ -package datadog.trace.civisibility.ipc +package datadog.trace.civisibility.ipc.serialization + import spock.lang.Specification import java.nio.ByteBuffer +import static datadog.trace.civisibility.TestUtils.bitset + class SerializerTest extends Specification { def "test int serialization: #i"() { @@ -145,6 +148,21 @@ class SerializerTest extends Specification { Serializer.readStringMap(buf) == ["a": "b", "1": "2"] } + def "test bitset serialization: #bitset"() { + given: + def serializer = new Serializer() + + when: + serializer.write((BitSet) bitset) + def buf = serializer.flush() + + then: + Serializer.readBitSet(buf) == bitset + + where: + bitset << [null, bitset(), bitset(1), bitset(999), bitset(1, 2, 3), bitset(1, 2, 3, 18, 157, 956, 1234567)] + } + private static final class MyPojo { private final int a private final String b diff --git a/dd-java-agent/agent-ci-visibility/src/test/resources/datadog/trace/civisibility/git/tree/git-diff.txt b/dd-java-agent/agent-ci-visibility/src/test/resources/datadog/trace/civisibility/git/tree/git-diff.txt new file mode 100644 index 00000000000..d79dc73c99a --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/resources/datadog/trace/civisibility/git/tree/git-diff.txt @@ -0,0 +1,33 @@ +diff --git a/java/maven-junit4/pom.xml b/java/maven-junit4/pom.xml +index 6d73cda..2a1f220 100644 +--- a/java/maven-junit4/pom.xml ++++ b/java/maven-junit4/pom.xml +@@ -10 +10 @@ + +-java-maven-junit4 ++java-maven-junit4-test-project +~ +diff --git a/java/maven-junit5/pom.xml b/java/maven-junit5/pom.xml +index 7b92d64..834a61c 100644 +--- a/java/maven-junit5/pom.xml ++++ b/java/maven-junit5/pom.xml +@@ -14 +14 @@ + +-module-b ++module-c +~ +@@ -18,3 +17,0 @@ +- 8 +~ +- 8 +~ +- UTF-8 +~ +@@ -34 +30,0 @@ +- test +~ +@@ -45 +41 @@ + +-1 ++2 +~ diff --git a/dd-java-agent/agent-ci-visibility/src/test/resources/datadog/trace/civisibility/git/tree/larger-git-diff.txt b/dd-java-agent/agent-ci-visibility/src/test/resources/datadog/trace/civisibility/git/tree/larger-git-diff.txt new file mode 100644 index 00000000000..41e227c58eb --- /dev/null +++ b/dd-java-agent/agent-ci-visibility/src/test/resources/datadog/trace/civisibility/git/tree/larger-git-diff.txt @@ -0,0 +1,117 @@ +diff --git a/java/maven-junit4/pom.xml b/java/maven-junit4/pom.xml +index 6d73cda..2a1f220 100644 +--- a/java/maven-junit4/pom.xml ++++ b/java/maven-junit4/pom.xml +@@ -10 +10 @@ + +-java-maven-junit4 ++java-maven-junit4-test-project +~ +diff --git a/java/maven-junit5/module-a/pom.xml b/java/maven-junit5/module-a/pom.xml +index 29a3a73..4567037 100644 +--- a/java/maven-junit5/module-a/pom.xml ++++ b/java/maven-junit5/module-a/pom.xml +@@ -8,3 +8,3 @@ + +-com.datadog.ci.test +~ +- java-maven-junit5 +~ +- 1.0-SNAPSHOT ++com.datadog.ci.test +~ ++ java-maven-junit5 +~ ++ 1.0-SNAPSHOT +~ +@@ -12,0 +13,3 @@ +~ ++sssss +~ +~ +@@ -16,0 +20,3 @@ + ++123 +~ ++ <12312 +~ +~ +@@ -21,2 +27,2 @@ + +-maven-surefire-plugin +~ +- ++maven-surefire-plugin +~ ++ +~ +@@ -30 +36 @@ + +- ++ +~ +@@ -34 +40 @@ + +~ +diff --git a/java/maven-junit5/module-b/pom.xml b/java/maven-junit5/module-b/pom.xml +deleted file mode 100644 +index f18dd09..0000000 +--- a/java/maven-junit5/module-b/pom.xml ++++ /dev/null +@@ -1,17 +0,0 @@ +- +~ +- +~ +- 4.0.0 +~ +- +~ +- com.datadog.ci.test +~ +- java-maven-junit5 +~ +- 1.0-SNAPSHOT +~ +- +~ +~ +- com.datadog.ci.test +~ +- java-maven-junit5-module-b +~ +- 1.0-SNAPSHOT +~ +- module-b +~ +~ +- +~ +diff --git a/java/maven-junit5/pom.xml b/java/maven-junit5/pom.xml +index 7b92d64..834a61c 100644 +--- a/java/maven-junit5/pom.xml ++++ b/java/maven-junit5/pom.xml +@@ -14 +14 @@ + +-module-b ++module-c +~ +@@ -18,3 +17,0 @@ +- 8 +~ +- 8 +~ +- UTF-8 +~ +@@ -34 +30,0 @@ +- test +~ +@@ -45 +41 @@ + +-1 ++2 +~ diff --git a/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityInstrumentationTest.groovy b/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityInstrumentationTest.groovy index d1c0fc936be..e9362935bf4 100644 --- a/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityInstrumentationTest.groovy +++ b/dd-java-agent/agent-ci-visibility/src/testFixtures/groovy/datadog/trace/civisibility/CiVisibilityInstrumentationTest.groovy @@ -19,15 +19,13 @@ import datadog.trace.api.config.CiVisibilityConfig import datadog.trace.api.config.GeneralConfig import datadog.trace.bootstrap.ContextStore import datadog.trace.civisibility.codeowners.Codeowners -import datadog.trace.civisibility.config.EarlyFlakeDetectionSettings -import datadog.trace.civisibility.config.ExecutionSettings -import datadog.trace.civisibility.config.ExecutionSettingsFactory -import datadog.trace.civisibility.config.JvmInfo -import datadog.trace.civisibility.config.JvmInfoFactoryImpl +import datadog.trace.civisibility.config.* import datadog.trace.civisibility.coverage.file.FileCoverageStore import datadog.trace.civisibility.coverage.percentage.NoOpCoverageCalculator import datadog.trace.civisibility.decorator.TestDecorator import datadog.trace.civisibility.decorator.TestDecoratorImpl +import datadog.trace.civisibility.diff.Diff +import datadog.trace.civisibility.diff.LineDiff import datadog.trace.civisibility.domain.BuildSystemSession import datadog.trace.civisibility.domain.TestFrameworkModule import datadog.trace.civisibility.domain.TestFrameworkSession @@ -72,9 +70,13 @@ abstract class CiVisibilityInstrumentationTest extends AgentTestRunner { private static final List skippableTests = new ArrayList<>() private static final List flakyTests = new ArrayList<>() private static final List knownTests = new ArrayList<>() + private static volatile Diff diff = LineDiff.EMPTY + private static volatile boolean itrEnabled = false private static volatile boolean flakyRetryEnabled = false private static volatile boolean earlyFlakinessDetectionEnabled = false + private static volatile boolean impactedTestsDetectionEnabled = false + public static final int SLOW_TEST_THRESHOLD_MILLIS = 1000 public static final int VERY_SLOW_TEST_THRESHOLD_MILLIS = 2000 @@ -120,12 +122,14 @@ abstract class CiVisibilityInstrumentationTest extends AgentTestRunner { false, itrEnabled, flakyRetryEnabled, + impactedTestsDetectionEnabled, earlyFlakinessDetectionSettings, itrEnabled ? "itrCorrelationId" : null, skippableTestsWithMetadata, [:], flakyTests, - earlyFlakinessDetectionEnabled || CIConstants.FAIL_FAST_TEST_ORDER.equalsIgnoreCase(Config.get().ciVisibilityTestOrder) ? knownTests : null) + earlyFlakinessDetectionEnabled || CIConstants.FAIL_FAST_TEST_ORDER.equalsIgnoreCase(Config.get().ciVisibilityTestOrder) ? knownTests : null, + diff) } } @@ -145,7 +149,11 @@ abstract class CiVisibilityInstrumentationTest extends AgentTestRunner { codeowners, linesResolver, coverageStoreFactory, - new ExecutionStrategy(config, executionSettingsFactory.create(JvmInfo.CURRENT_JVM, "")) + new ExecutionStrategy( + config, + executionSettingsFactory.create(JvmInfo.CURRENT_JVM, ""), + sourcePathResolver, + linesResolver) ) } @@ -213,9 +221,11 @@ abstract class CiVisibilityInstrumentationTest extends AgentTestRunner { skippableTests.clear() flakyTests.clear() knownTests.clear() + diff = LineDiff.EMPTY itrEnabled = false flakyRetryEnabled = false earlyFlakinessDetectionEnabled = false + impactedTestsDetectionEnabled = false } def givenSkippableTests(List tests) { @@ -231,6 +241,10 @@ abstract class CiVisibilityInstrumentationTest extends AgentTestRunner { knownTests.addAll(tests) } + def givenDiff(Diff diff) { + this.diff = diff + } + def givenFlakyRetryEnabled(boolean flakyRetryEnabled) { this.flakyRetryEnabled = flakyRetryEnabled } @@ -239,6 +253,10 @@ abstract class CiVisibilityInstrumentationTest extends AgentTestRunner { this.earlyFlakinessDetectionEnabled = earlyFlakinessDetectionEnabled } + def givenImpactedTestsDetectionEnabled(boolean impactedTestsDetectionEnabled) { + this.impactedTestsDetectionEnabled = impactedTestsDetectionEnabled + } + def givenTestsOrder(String testsOrder) { injectSysConfig(CiVisibilityConfig.CIVISIBILITY_TEST_ORDER, testsOrder) } @@ -347,4 +365,12 @@ abstract class CiVisibilityInstrumentationTest extends AgentTestRunner { abstract String instrumentedLibraryName() abstract String instrumentedLibraryVersion() + + BitSet lines(int ... setBits) { + BitSet bitSet = new BitSet() + for (int bit : setBits) { + bitSet.set(bit) + } + return bitSet + } } diff --git a/dd-java-agent/instrumentation/junit-4.10/cucumber-junit-4/src/main/java/datadog/trace/instrumentation/junit4/CucumberTracingListener.java b/dd-java-agent/instrumentation/junit-4.10/cucumber-junit-4/src/main/java/datadog/trace/instrumentation/junit4/CucumberTracingListener.java index 697321bded7..13aaa1bd94a 100644 --- a/dd-java-agent/instrumentation/junit-4.10/cucumber-junit-4/src/main/java/datadog/trace/instrumentation/junit4/CucumberTracingListener.java +++ b/dd-java-agent/instrumentation/junit-4.10/cucumber-junit-4/src/main/java/datadog/trace/instrumentation/junit4/CucumberTracingListener.java @@ -1,5 +1,6 @@ package datadog.trace.instrumentation.junit4; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.coverage.CoveragePerTestBridge; import datadog.trace.api.civisibility.events.TestDescriptor; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; @@ -74,15 +75,12 @@ public void testStarted(final Description description) { TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( new TestSuiteDescriptor(testSuiteName, null), CucumberUtils.toTestDescriptor(description), - testSuiteName, testName, FRAMEWORK_NAME, FRAMEWORK_VERSION, null, categories, - null, - null, - null, + TestSourceData.UNKNOWN, retryPolicy != null && retryPolicy.currentExecutionIsRetry(), null); @@ -167,15 +165,12 @@ public void testIgnored(final Description description) { TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( new TestSuiteDescriptor(testSuiteName, null), CucumberUtils.toTestDescriptor(description), - testSuiteName, testName, FRAMEWORK_NAME, FRAMEWORK_VERSION, null, categories, - null, - null, - null, + TestSourceData.UNKNOWN, reason); } } diff --git a/dd-java-agent/instrumentation/junit-4.10/cucumber-junit-4/src/main/java/datadog/trace/instrumentation/junit4/retry/Cucumber4RetryInstrumentation.java b/dd-java-agent/instrumentation/junit-4.10/cucumber-junit-4/src/main/java/datadog/trace/instrumentation/junit4/retry/Cucumber4RetryInstrumentation.java index c8e7e925489..06de5a7d397 100644 --- a/dd-java-agent/instrumentation/junit-4.10/cucumber-junit-4/src/main/java/datadog/trace/instrumentation/junit4/retry/Cucumber4RetryInstrumentation.java +++ b/dd-java-agent/instrumentation/junit-4.10/cucumber-junit-4/src/main/java/datadog/trace/instrumentation/junit4/retry/Cucumber4RetryInstrumentation.java @@ -9,6 +9,7 @@ import datadog.trace.agent.tooling.muzzle.Reference; import datadog.trace.api.Config; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.instrumentation.junit4.CucumberUtils; @@ -93,7 +94,8 @@ public static Boolean retryIfNeeded( Description description = CucumberUtils.getPickleRunnerDescription(pickleRunner); TestIdentifier testIdentifier = CucumberUtils.toTestIdentifier(description); TestRetryPolicy retryPolicy = - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy(testIdentifier); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy( + testIdentifier, TestSourceData.UNKNOWN); if (!retryPolicy.retriesLeft()) { // retries not applicable, run original method return null; diff --git a/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/main/java/datadog/trace/instrumentation/junit4/MUnitTracingListener.java b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/main/java/datadog/trace/instrumentation/junit4/MUnitTracingListener.java index 43524046393..499a7b57b0b 100644 --- a/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/main/java/datadog/trace/instrumentation/junit4/MUnitTracingListener.java +++ b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/main/java/datadog/trace/instrumentation/junit4/MUnitTracingListener.java @@ -74,23 +74,18 @@ public void testSuiteFinished(final Description description) { public void testStarted(final Description description) { TestSuiteDescriptor suiteDescriptor = MUnitUtils.toSuiteDescriptor(description); TestDescriptor testDescriptor = MUnitUtils.toTestDescriptor(description); - String testSuiteName = description.getClassName(); - Class testClass = description.getTestClass(); String testName = description.getMethodName(); List categories = getCategories(description); TestRetryPolicy retryPolicy = retryPolicies.get(description); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( suiteDescriptor, testDescriptor, - testSuiteName, testName, FRAMEWORK_NAME, FRAMEWORK_VERSION, null, categories, - testClass, - null, - null, + JUnit4Utils.toTestSourceData(description), retryPolicy != null && retryPolicy.currentExecutionIsRetry(), null); } @@ -147,7 +142,6 @@ public void testAssumptionFailure(final Failure failure) { @Override public void testIgnored(final Description description) { Class testClass = description.getTestClass(); - String testSuiteName = description.getClassName(); String testName = description.getMethodName(); if (Strings.isNotBlank(testName)) { @@ -160,15 +154,12 @@ public void testIgnored(final Description description) { TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( suiteDescriptor, testDescriptor, - testSuiteName, testName, FRAMEWORK_NAME, FRAMEWORK_VERSION, null, categories, - testClass, - null, - null, + JUnit4Utils.toTestSourceData(description), false, null); } @@ -181,6 +172,7 @@ public void testIgnored(final Description description) { boolean suiteStarted = isSpanInProgress(InternalSpanTypes.TEST_SUITE_END); if (!suiteStarted) { // there is a bug in MUnit 1.0.1+: start/finish events are not fired for skipped suites + String testSuiteName = description.getClassName(); List categories = getCategories(description); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestSuiteStart( suiteDescriptor, @@ -218,22 +210,17 @@ private static boolean isSpanInProgress(UTF8BytesString type) { private void testCaseIgnored(final Description description) { TestSuiteDescriptor suiteDescriptor = MUnitUtils.toSuiteDescriptor(description); TestDescriptor testDescriptor = MUnitUtils.toTestDescriptor(description); - String testSuiteName = description.getClassName(); String testName = description.getMethodName(); - Class testClass = description.getTestClass(); List categories = getCategories(description); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( suiteDescriptor, testDescriptor, - testSuiteName, testName, FRAMEWORK_NAME, FRAMEWORK_VERSION, null, categories, - testClass, - null, - null, + JUnit4Utils.toTestSourceData(description), null); } diff --git a/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/main/java/datadog/trace/instrumentation/junit4/retry/MUnitRetryInstrumentation.java b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/main/java/datadog/trace/instrumentation/junit4/retry/MUnitRetryInstrumentation.java index 775fbc609c3..640d6983b82 100644 --- a/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/main/java/datadog/trace/instrumentation/junit4/retry/MUnitRetryInstrumentation.java +++ b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/main/java/datadog/trace/instrumentation/junit4/retry/MUnitRetryInstrumentation.java @@ -8,6 +8,7 @@ import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.api.Config; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.instrumentation.junit4.JUnit4Utils; @@ -84,8 +85,10 @@ public static Future retryIfNeeded( Description description = MUnitUtils.createDescription(runner, test); TestIdentifier testIdentifier = JUnit4Utils.toTestIdentifier(description); + TestSourceData testSourceData = JUnit4Utils.toTestSourceData(description); + TestRetryPolicy retryPolicy = - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy(testIdentifier); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy(testIdentifier, testSourceData); if (!retryPolicy.retriesLeft()) { // retries not applicable, run original method return null; diff --git a/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/groovy/MUnitTest.groovy b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/groovy/MUnitTest.groovy index 43ac216a46b..670da04d98e 100644 --- a/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/groovy/MUnitTest.groovy +++ b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/groovy/MUnitTest.groovy @@ -1,6 +1,8 @@ import datadog.trace.api.DisableTestTrace import datadog.trace.api.civisibility.config.TestIdentifier import datadog.trace.civisibility.CiVisibilityInstrumentationTest +import datadog.trace.civisibility.diff.FileDiff +import datadog.trace.civisibility.diff.LineDiff import datadog.trace.instrumentation.junit4.MUnitTracingListener import datadog.trace.instrumentation.junit4.TestEventsHandlerHolder import org.example.TestFailedAssumptionMUnit @@ -60,6 +62,23 @@ class MUnitTest extends CiVisibilityInstrumentationTest { "test-efd-new-slow-test" | [TestSucceedMUnitSlow] | 3 | [] // is executed only twice } + def "test impacted tests detection #testcaseName"() { + givenImpactedTestsDetectionEnabled(true) + givenDiff(prDiff) + + runTests(tests) + + assertSpansData(testcaseName, expectedTracesCount) + + where: + testcaseName | tests | expectedTracesCount | prDiff + "test-succeed" | [TestSucceedMUnit] | 2 | LineDiff.EMPTY + "test-succeed" | [TestSucceedMUnit] | 2 | new FileDiff(new HashSet()) + "test-succeed-impacted" | [TestSucceedMUnit] | 2 | new FileDiff(new HashSet([DUMMY_SOURCE_PATH])) + "test-succeed" | [TestSucceedMUnit] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines()]) + "test-succeed-impacted" | [TestSucceedMUnit] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines(DUMMY_TEST_METHOD_START)]) + } + private void runTests(Collection> tests) { TestEventsHandlerHolder.start() try { diff --git a/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/resources/test-succeed-impacted/coverages.ftl b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/resources/test-succeed-impacted/coverages.ftl new file mode 100644 index 00000000000..8878e547a79 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/resources/test-succeed-impacted/coverages.ftl @@ -0,0 +1 @@ +[ ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/resources/test-succeed-impacted/events.ftl b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/resources/test-succeed-impacted/events.ftl new file mode 100644 index 00000000000..5fa9c5b5e53 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-4.10/munit-junit-4/src/test/resources/test-succeed-impacted/events.ftl @@ -0,0 +1,149 @@ +[ { + "content" : { + "duration" : ${content_duration}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid}, + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test_session_end", + "test.command" : "munit-junit-4", + "test.framework" : "munit", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id} + }, + "name" : "junit.test_session", + "resource" : "munit-junit-4", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_session_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_2}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_2}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_module_end", + "test.framework" : "munit", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "munit-junit-4", + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2} + }, + "name" : "junit.test_module", + "resource" : "munit-junit-4", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_2}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_module_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_3}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_3}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_suite_end", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "munit", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "munit-junit-4", + "test.source.file" : "dummy_source_path", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceedMUnit", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3}, + "test.source.end" : 19, + "test.source.start" : 11 + }, + "name" : "junit.test_suite", + "resource" : "org.example.TestSucceedMUnit", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_3}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} + }, + "type" : "test_suite_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_4}, + "error" : 0, + "meta" : { + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "munit", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.is_modified" : "true", + "test.module" : "munit-junit-4", + "test.name" : "Calculator.add", + "test.source.file" : "dummy_source_path", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceedMUnit", + "test.traits" : "{\"category\":[\"myTag\"]}", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id} + }, + "name" : "junit.test", + "parent_id" : ${content_parent_id}, + "resource" : "org.example.TestSucceedMUnit.Calculator.add", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "span_id" : ${content_span_id}, + "start" : ${content_start_4}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id}, + "trace_id" : ${content_trace_id} + }, + "type" : "test", + "version" : 2 +} ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4TracingListener.java b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4TracingListener.java index 2fe70d0fd97..351d0668a64 100644 --- a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4TracingListener.java +++ b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4TracingListener.java @@ -1,5 +1,6 @@ package datadog.trace.instrumentation.junit4; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestDescriptor; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; import datadog.trace.api.civisibility.retry.TestRetryPolicy; @@ -67,26 +68,23 @@ public void testStarted(final Description description) { TestSuiteDescriptor suiteDescriptor = JUnit4Utils.toSuiteDescriptor(description); TestDescriptor testDescriptor = JUnit4Utils.toTestDescriptor(description); - Class testClass = description.getTestClass(); - Method testMethod = JUnit4Utils.getTestMethod(description); - String testMethodName = testMethod != null ? testMethod.getName() : null; - String testSuiteName = JUnit4Utils.getSuiteName(testClass, description); - String testName = JUnit4Utils.getTestName(description, testMethod); + TestSourceData testSourceData = JUnit4Utils.toTestSourceData(description); + + String testName = JUnit4Utils.getTestName(description, testSourceData.getTestMethod()); String testParameters = JUnit4Utils.getParameters(description); - List categories = JUnit4Utils.getCategories(testClass, testMethod); + List categories = + JUnit4Utils.getCategories(testSourceData.getTestClass(), testSourceData.getTestMethod()); TestRetryPolicy retryPolicy = retryPolicies.get(description); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( suiteDescriptor, testDescriptor, - testSuiteName, testName, FRAMEWORK_NAME, FRAMEWORK_VERSION, testParameters, categories, - testClass, - testMethodName, - testMethod, + testSourceData, retryPolicy != null && retryPolicy.currentExecutionIsRetry(), null); } @@ -197,24 +195,23 @@ private void testIgnored(Description description, Method testMethod, String reas TestSuiteDescriptor suiteDescriptor = JUnit4Utils.toSuiteDescriptor(description); TestDescriptor testDescriptor = JUnit4Utils.toTestDescriptor(description); + Class testClass = description.getTestClass(); String testMethodName = testMethod != null ? testMethod.getName() : null; - String testSuiteName = JUnit4Utils.getSuiteName(testClass, description); + TestSourceData testSourceData = new TestSourceData(testClass, testMethod, testMethodName); + String testName = JUnit4Utils.getTestName(description, testMethod); String testParameters = JUnit4Utils.getParameters(description); List categories = JUnit4Utils.getCategories(testClass, testMethod); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( suiteDescriptor, testDescriptor, - testSuiteName, testName, FRAMEWORK_NAME, FRAMEWORK_VERSION, testParameters, categories, - testClass, - testMethodName, - testMethod, + testSourceData, reason); } } diff --git a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java index 4d4c82675e0..ef221174480 100644 --- a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java +++ b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/JUnit4Utils.java @@ -3,6 +3,7 @@ import static datadog.json.JsonMapper.toJson; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestDescriptor; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; import datadog.trace.util.MethodHandles; @@ -292,25 +293,31 @@ public static Description getSkippedDescription(Description description) { } public static TestIdentifier toTestIdentifier(Description description) { - Method testMethod = JUnit4Utils.getTestMethod(description); + Method testMethod = getTestMethod(description); String suite = description.getClassName(); - String name = JUnit4Utils.getTestName(description, testMethod); - String parameters = JUnit4Utils.getParameters(description); + String name = getTestName(description, testMethod); + String parameters = getParameters(description); return new TestIdentifier(suite, name, parameters); } + public static TestSourceData toTestSourceData(Description description) { + Class testClass = description.getTestClass(); + Method testMethod = getTestMethod(description); + return new TestSourceData(testClass, testMethod); + } + public static TestDescriptor toTestDescriptor(Description description) { Class testClass = description.getTestClass(); - Method testMethod = JUnit4Utils.getTestMethod(description); - String testSuiteName = JUnit4Utils.getSuiteName(testClass, description); - String testName = JUnit4Utils.getTestName(description, testMethod); - String testParameters = JUnit4Utils.getParameters(description); + Method testMethod = getTestMethod(description); + String testSuiteName = getSuiteName(testClass, description); + String testName = getTestName(description, testMethod); + String testParameters = getParameters(description); return new TestDescriptor(testSuiteName, testClass, testName, testParameters, null); } public static TestSuiteDescriptor toSuiteDescriptor(Description description) { Class testClass = description.getTestClass(); - String testSuiteName = JUnit4Utils.getSuiteName(testClass, description); + String testSuiteName = getSuiteName(testClass, description); // relying exclusively on class name: some runners (such as PowerMock) may redefine test classes return new TestSuiteDescriptor(testSuiteName, null); } diff --git a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/retry/JUnit4RetryInstrumentation.java b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/retry/JUnit4RetryInstrumentation.java index 35d5749e201..02e662fd40a 100644 --- a/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/retry/JUnit4RetryInstrumentation.java +++ b/dd-java-agent/instrumentation/junit-4.10/src/main/java/datadog/trace/instrumentation/junit4/retry/JUnit4RetryInstrumentation.java @@ -9,6 +9,7 @@ import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.api.Config; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.instrumentation.junit4.JUnit4Utils; @@ -95,8 +96,9 @@ public static Boolean retryIfNeeded( } TestIdentifier testIdentifier = JUnit4Utils.toTestIdentifier(description); + TestSourceData testSourceData = JUnit4Utils.toTestSourceData(description); TestRetryPolicy retryPolicy = - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy(testIdentifier); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy(testIdentifier, testSourceData); if (!retryPolicy.retriesLeft()) { // retries not applicable, run original method return null; diff --git a/dd-java-agent/instrumentation/junit-4.10/src/test/groovy/JUnit4Test.groovy b/dd-java-agent/instrumentation/junit-4.10/src/test/groovy/JUnit4Test.groovy index 38f51a32850..c3f7792ed0a 100644 --- a/dd-java-agent/instrumentation/junit-4.10/src/test/groovy/JUnit4Test.groovy +++ b/dd-java-agent/instrumentation/junit-4.10/src/test/groovy/JUnit4Test.groovy @@ -1,6 +1,8 @@ import datadog.trace.api.DisableTestTrace import datadog.trace.api.civisibility.config.TestIdentifier import datadog.trace.civisibility.CiVisibilityInstrumentationTest +import datadog.trace.civisibility.diff.FileDiff +import datadog.trace.civisibility.diff.LineDiff import datadog.trace.instrumentation.junit4.TestEventsHandlerHolder import junit.runner.Version import org.example.TestAssumption @@ -133,6 +135,23 @@ class JUnit4Test extends CiVisibilityInstrumentationTest { "test-efd-faulty-session-threshold" | [TestFailedAndSucceed] | 8 | [] } + def "test impacted tests detection #testcaseName"() { + givenImpactedTestsDetectionEnabled(true) + givenDiff(prDiff) + + runTests(tests) + + assertSpansData(testcaseName, expectedTracesCount) + + where: + testcaseName | tests | expectedTracesCount | prDiff + "test-succeed" | [TestSucceed] | 2 | LineDiff.EMPTY + "test-succeed" | [TestSucceed] | 2 | new FileDiff(new HashSet()) + "test-succeed-impacted" | [TestSucceed] | 2 | new FileDiff(new HashSet([DUMMY_SOURCE_PATH])) + "test-succeed" | [TestSucceed] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines()]) + "test-succeed-impacted" | [TestSucceed] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines(DUMMY_TEST_METHOD_START)]) + } + private void runTests(Collection> tests) { TestEventsHandlerHolder.start() try { diff --git a/dd-java-agent/instrumentation/junit-4.10/src/test/resources/test-succeed-impacted/coverages.ftl b/dd-java-agent/instrumentation/junit-4.10/src/test/resources/test-succeed-impacted/coverages.ftl new file mode 100644 index 00000000000..8878e547a79 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-4.10/src/test/resources/test-succeed-impacted/coverages.ftl @@ -0,0 +1 @@ +[ ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/junit-4.10/src/test/resources/test-succeed-impacted/events.ftl b/dd-java-agent/instrumentation/junit-4.10/src/test/resources/test-succeed-impacted/events.ftl new file mode 100644 index 00000000000..d3f0ae0144a --- /dev/null +++ b/dd-java-agent/instrumentation/junit-4.10/src/test/resources/test-succeed-impacted/events.ftl @@ -0,0 +1,151 @@ +[ { + "content" : { + "duration" : ${content_duration}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid}, + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test_session_end", + "test.command" : "junit-4.10", + "test.framework" : "junit4", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id} + }, + "name" : "junit.test_session", + "resource" : "junit-4.10", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_session_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_2}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_2}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_module_end", + "test.framework" : "junit4", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "junit-4.10", + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2} + }, + "name" : "junit.test_module", + "resource" : "junit-4.10", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_2}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_module_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_3}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_3}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_suite_end", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "junit4", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "junit-4.10", + "test.source.file" : "dummy_source_path", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3}, + "test.source.end" : 19, + "test.source.start" : 11 + }, + "name" : "junit.test_suite", + "resource" : "org.example.TestSucceed", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_3}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} + }, + "type" : "test_suite_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_4}, + "error" : 0, + "meta" : { + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "junit4", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.is_modified" : "true", + "test.module" : "junit-4.10", + "test.name" : "test_succeed", + "test.source.file" : "dummy_source_path", + "test.source.method" : "test_succeed()V", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id}, + "test.source.end" : 18, + "test.source.start" : 12 + }, + "name" : "junit.test", + "parent_id" : ${content_parent_id}, + "resource" : "org.example.TestSucceed.test_succeed", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "span_id" : ${content_span_id}, + "start" : ${content_start_4}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id}, + "trace_id" : ${content_trace_id} + }, + "type" : "test", + "version" : 2 +} ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/CucumberTracingListener.java b/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/CucumberTracingListener.java index 79521c2bbfd..afc8c3f3147 100644 --- a/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/CucumberTracingListener.java +++ b/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/CucumberTracingListener.java @@ -1,6 +1,7 @@ package datadog.trace.instrumentation.junit5; import datadog.trace.api.Pair; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.coverage.CoveragePerTestBridge; import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; import java.util.List; @@ -110,22 +111,18 @@ private void testResourceExecutionStarted( String classpathResourceName = testSource.getClasspathResourceName(); Pair names = CucumberUtils.getFeatureAndScenarioNames(testDescriptor, classpathResourceName); - String testSuiteName = names.getLeft(); String testName = names.getRight(); List tags = testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( suiteDescriptor, testDescriptor, - testSuiteName, testName, testFramework, testFrameworkVersion, null, tags, - null, - null, - null, + TestSourceData.UNKNOWN, JUnitPlatformUtils.isRetry(testDescriptor), null); @@ -168,22 +165,18 @@ private void testResourceExecutionSkipped( String classpathResourceName = testSource.getClasspathResourceName(); Pair names = CucumberUtils.getFeatureAndScenarioNames(testDescriptor, classpathResourceName); - String testSuiteName = names.getLeft(); String testName = names.getRight(); List tags = testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( suiteDescriptor, testDescriptor, - testSuiteName, testName, testFramework, testFrameworkVersion, null, tags, - null, - null, - null, + TestSourceData.UNKNOWN, reason); } } diff --git a/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java b/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java index 43a7a5e0ee6..1e04e935a1f 100644 --- a/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java +++ b/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/CucumberUtils.java @@ -2,6 +2,7 @@ import datadog.trace.api.Pair; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import java.io.InputStream; import java.util.ArrayDeque; import java.util.Deque; @@ -19,7 +20,8 @@ public abstract class CucumberUtils { static { - TestIdentifierFactory.register("cucumber", CucumberUtils::toTestIdentifier); + TestDataFactory.register( + "cucumber", CucumberUtils::toTestIdentifier, d -> TestSourceData.UNKNOWN); } public static @Nullable String getCucumberVersion(TestEngine cucumberEngine) { diff --git a/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java index 7366f38bbdf..2975e92d759 100644 --- a/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java +++ b/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberInstrumentation.java @@ -37,7 +37,7 @@ public String instrumentedType() { @Override public String[] helperClassNames() { return new String[] { - packageName + ".TestIdentifierFactory", + packageName + ".TestDataFactory", packageName + ".JUnitPlatformUtils", packageName + ".CucumberUtils", packageName + ".TestEventsHandlerHolder", diff --git a/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberItrInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberItrInstrumentation.java index a2bd1bc899b..8491e1617af 100644 --- a/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberItrInstrumentation.java +++ b/dd-java-agent/instrumentation/junit-5.3/cucumber-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5CucumberItrInstrumentation.java @@ -55,7 +55,7 @@ public ElementMatcher hierarchyMatcher() { @Override public String[] helperClassNames() { return new String[] { - packageName + ".TestIdentifierFactory", + packageName + ".TestDataFactory", packageName + ".JUnitPlatformUtils", packageName + ".CucumberUtils", packageName + ".TestEventsHandlerHolder", diff --git a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java index 74319ce2353..1960a6c254f 100644 --- a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java +++ b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockInstrumentation.java @@ -38,7 +38,7 @@ public String instrumentedType() { public String[] helperClassNames() { return new String[] { packageName + ".JUnitPlatformUtils", - packageName + ".TestIdentifierFactory", + packageName + ".TestDataFactory", packageName + ".SpockUtils", packageName + ".TestEventsHandlerHolder", packageName + ".SpockTracingListener", diff --git a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockItrInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockItrInstrumentation.java index 52adeb14940..f12efa51f98 100644 --- a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockItrInstrumentation.java +++ b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/JUnit5SpockItrInstrumentation.java @@ -55,7 +55,7 @@ public ElementMatcher hierarchyMatcher() { public String[] helperClassNames() { return new String[] { packageName + ".JUnitPlatformUtils", - packageName + ".TestIdentifierFactory", + packageName + ".TestDataFactory", packageName + ".SpockUtils", packageName + ".TestEventsHandlerHolder", }; diff --git a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/SpockTracingListener.java b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/SpockTracingListener.java index 583630a324f..450f351ffb2 100644 --- a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/SpockTracingListener.java +++ b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/SpockTracingListener.java @@ -1,7 +1,7 @@ package datadog.trace.instrumentation.junit5; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; -import java.lang.reflect.Method; import java.util.List; import java.util.stream.Collectors; import org.junit.platform.engine.EngineExecutionListener; @@ -109,26 +109,21 @@ private void testCaseExecutionStarted(final TestDescriptor testDescriptor) { private void testMethodExecutionStarted(TestDescriptor testDescriptor, MethodSource testSource) { TestDescriptor suiteDescriptor = SpockUtils.getSpecDescriptor(testDescriptor); - String testSuitName = testSource.getClassName(); String displayName = testDescriptor.getDisplayName(); String testParameters = JUnitPlatformUtils.getParameters(testSource, displayName); List tags = testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); - Class testClass = testSource.getJavaClass(); - Method testMethod = SpockUtils.getTestMethod(testSource); - String testMethodName = testSource.getMethodName(); + TestSourceData testSourceData = SpockUtils.toTestSourceData(testDescriptor); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( suiteDescriptor, testDescriptor, - testSuitName, displayName, testFramework, testFrameworkVersion, testParameters, tags, - testClass, - testMethodName, - testMethod, + testSourceData, JUnitPlatformUtils.isRetry(testDescriptor), null); } @@ -202,26 +197,21 @@ private void containerExecutionSkipped( private void testMethodExecutionSkipped( final TestDescriptor testDescriptor, final MethodSource methodSource, final String reason) { TestDescriptor suiteDescriptor = SpockUtils.getSpecDescriptor(testDescriptor); - String testSuiteName = methodSource.getClassName(); String displayName = testDescriptor.getDisplayName(); String testParameters = JUnitPlatformUtils.getParameters(methodSource, displayName); List tags = testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); - Class testClass = methodSource.getJavaClass(); - Method testMethod = SpockUtils.getTestMethod(methodSource); - String testMethodName = methodSource.getMethodName(); + TestSourceData testSourceData = SpockUtils.toTestSourceData(testDescriptor); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( suiteDescriptor, testDescriptor, - testSuiteName, displayName, testFramework, testFrameworkVersion, testParameters, tags, - testClass, - testMethodName, - testMethod, + testSourceData, reason); } } diff --git a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java index 0a1a235d4ff..e70b7560946 100644 --- a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java +++ b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/main/java/datadog/trace/instrumentation/junit5/SpockUtils.java @@ -2,6 +2,7 @@ import datadog.trace.api.civisibility.InstrumentationBridge; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; import java.util.ArrayList; @@ -35,7 +36,7 @@ public class SpockUtils { METHOD_HANDLES.method("org.spockframework.runtime.model.TestTag", "getValue"); static { - TestIdentifierFactory.register("spock", SpockUtils::toTestIdentifier); + TestDataFactory.register("spock", SpockUtils::toTestIdentifier, SpockUtils::toTestSourceData); } /* @@ -66,7 +67,7 @@ public static Collection getTags(SpockNode spockNode) { } public static boolean isUnskippable(SpockNode spockNode) { - Collection tags = SpockUtils.getTags(spockNode); + Collection tags = getTags(spockNode); for (TestTag tag : tags) { if (InstrumentationBridge.ITR_UNSKIPPABLE_TAG.equals(tag.getName())) { return true; @@ -75,7 +76,35 @@ public static boolean isUnskippable(SpockNode spockNode) { return false; } - public static Method getTestMethod(MethodSource methodSource) { + public static TestIdentifier toTestIdentifier(TestDescriptor testDescriptor) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (testSource instanceof MethodSource && testDescriptor instanceof SpockNode) { + SpockNode spockNode = (SpockNode) testDescriptor; + MethodSource methodSource = (MethodSource) testSource; + String testSuiteName = methodSource.getClassName(); + String displayName = spockNode.getDisplayName(); + String testParameters = JUnitPlatformUtils.getParameters(methodSource, displayName); + return new TestIdentifier(testSuiteName, displayName, testParameters); + + } else { + return null; + } + } + + public static TestSourceData toTestSourceData(TestDescriptor testDescriptor) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (testSource instanceof MethodSource) { + MethodSource methodSource = (MethodSource) testSource; + Class testClass = methodSource.getJavaClass(); + Method testMethod = getTestMethod(methodSource); + String testMethodName = methodSource.getMethodName(); + return new TestSourceData(testClass, testMethod, testMethodName); + } else { + return TestSourceData.UNKNOWN; + } + } + + private static Method getTestMethod(MethodSource methodSource) { String methodName = methodSource.getMethodName(); if (methodName == null) { return null; @@ -104,21 +133,6 @@ public static Method getTestMethod(MethodSource methodSource) { return null; } - public static TestIdentifier toTestIdentifier(TestDescriptor testDescriptor) { - TestSource testSource = testDescriptor.getSource().orElse(null); - if (testSource instanceof MethodSource && testDescriptor instanceof SpockNode) { - SpockNode spockNode = (SpockNode) testDescriptor; - MethodSource methodSource = (MethodSource) testSource; - String testSuiteName = methodSource.getClassName(); - String displayName = spockNode.getDisplayName(); - String testParameters = JUnitPlatformUtils.getParameters(methodSource, displayName); - return new TestIdentifier(testSuiteName, displayName, testParameters); - - } else { - return null; - } - } - public static boolean isSpec(TestDescriptor testDescriptor) { UniqueId uniqueId = testDescriptor.getUniqueId(); List segments = uniqueId.getSegments(); diff --git a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/groovy/SpockTest.groovy b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/groovy/SpockTest.groovy index 6d316ee4de1..5957544b95a 100644 --- a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/groovy/SpockTest.groovy +++ b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/groovy/SpockTest.groovy @@ -2,6 +2,8 @@ import datadog.trace.api.DisableTestTrace import datadog.trace.api.civisibility.CIConstants import datadog.trace.api.civisibility.config.TestIdentifier import datadog.trace.civisibility.CiVisibilityInstrumentationTest +import datadog.trace.civisibility.diff.FileDiff +import datadog.trace.civisibility.diff.LineDiff import datadog.trace.instrumentation.junit5.TestEventsHandlerHolder import org.example.TestFailedParameterizedSpock import org.example.TestFailedSpock @@ -113,6 +115,23 @@ class SpockTest extends CiVisibilityInstrumentationTest { "test-efd-faulty-session-threshold" | [TestSucceedAndFailedSpock] | 8 | [] } + def "test impacted tests detection #testcaseName"() { + givenImpactedTestsDetectionEnabled(true) + givenDiff(prDiff) + + runTests(tests) + + assertSpansData(testcaseName, expectedTracesCount) + + where: + testcaseName | tests | expectedTracesCount | prDiff + "test-succeed" | [TestSucceedSpock] | 2 | LineDiff.EMPTY + "test-succeed" | [TestSucceedSpock] | 2 | new FileDiff(new HashSet()) + "test-succeed-impacted" | [TestSucceedSpock] | 2 | new FileDiff(new HashSet([DUMMY_SOURCE_PATH])) + "test-succeed" | [TestSucceedSpock] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines()]) + "test-succeed-impacted" | [TestSucceedSpock] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines(DUMMY_TEST_METHOD_START)]) + } + private static void runTests(List> classes) { TestEventsHandlerHolder.startForcefully() diff --git a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/resources/test-succeed-impacted/coverages.ftl b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/resources/test-succeed-impacted/coverages.ftl new file mode 100644 index 00000000000..8878e547a79 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/resources/test-succeed-impacted/coverages.ftl @@ -0,0 +1 @@ +[ ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/resources/test-succeed-impacted/events.ftl b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/resources/test-succeed-impacted/events.ftl new file mode 100644 index 00000000000..547148c6398 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/spock-junit-5/src/test/resources/test-succeed-impacted/events.ftl @@ -0,0 +1,152 @@ +[ { + "content" : { + "duration" : ${content_duration}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_suite_end", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "spock", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "spock-junit-5", + "test.source.file" : "dummy_source_path", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceedSpock", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}, + "test.source.end" : 19, + "test.source.start" : 11 + }, + "name" : "junit.test_suite", + "resource" : "org.example.TestSucceedSpock", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} + }, + "type" : "test_suite_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_2}, + "error" : 0, + "meta" : { + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "spock", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.is_modified" : "true", + "test.is_new" : "true", + "test.module" : "spock-junit-5", + "test.name" : "test success", + "test.source.file" : "dummy_source_path", + "test.source.method" : "test success()V", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceedSpock", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id}, + "test.source.end" : 18, + "test.source.start" : 12 + }, + "name" : "junit.test", + "parent_id" : ${content_parent_id}, + "resource" : "org.example.TestSucceedSpock.test success", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "span_id" : ${content_span_id}, + "start" : ${content_start_2}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id}, + "trace_id" : ${content_trace_id} + }, + "type" : "test", + "version" : 2 +}, { + "content" : { + "duration" : ${content_duration_3}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_2}, + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test_session_end", + "test.command" : "spock-junit-5", + "test.framework" : "spock", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id} + }, + "name" : "junit.test_session", + "resource" : "spock-junit-5", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_3}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_session_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_4}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_3}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_module_end", + "test.framework" : "spock", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "spock-junit-5", + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4} + }, + "name" : "junit.test_module", + "resource" : "spock-junit-5", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_4}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_module_end", + "version" : 1 +} ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformUtils.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformUtils.java index 48b64fb5de7..ee2ce6bd6d8 100644 --- a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformUtils.java +++ b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/JUnitPlatformUtils.java @@ -3,6 +3,7 @@ import static datadog.json.JsonMapper.toJson; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; @@ -11,6 +12,8 @@ import java.lang.invoke.MethodHandle; import java.lang.reflect.Method; import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.ClassLoaderUtils; import org.junit.platform.commons.util.ReflectionUtils; @@ -50,7 +53,7 @@ private JUnitPlatformUtils() {} private static final MethodHandle GET_JAVA_METHOD = METHOD_HANDLES.method(MethodSource.class, "getJavaMethod"); - public static Class getTestClass(MethodSource methodSource) { + private static Class getTestClass(MethodSource methodSource) { Class javaClass = METHOD_HANDLES.invoke(GET_JAVA_CLASS, methodSource); if (javaClass != null) { return javaClass; @@ -58,7 +61,7 @@ public static Class getTestClass(MethodSource methodSource) { return ReflectionUtils.loadClass(methodSource.getClassName()).orElse(null); } - public static Method getTestMethod(MethodSource methodSource) { + private static Method getTestMethod(MethodSource methodSource) { Method javaMethod = METHOD_HANDLES.invoke(GET_JAVA_METHOD, methodSource); if (javaMethod != null) { return javaMethod; @@ -93,6 +96,7 @@ public static String getParameters(MethodSource methodSource, String displayName return "{\"metadata\":{\"test_name\":" + toJson(displayName) + "}}"; } + @Nullable public static TestIdentifier toTestIdentifier(TestDescriptor testDescriptor) { TestSource testSource = testDescriptor.getSource().orElse(null); if (testSource instanceof MethodSource) { @@ -108,6 +112,22 @@ public static TestIdentifier toTestIdentifier(TestDescriptor testDescriptor) { } } + @Nonnull + public static TestSourceData toTestSourceData(TestDescriptor testDescriptor) { + TestSource testSource = testDescriptor.getSource().orElse(null); + if (!(testSource instanceof MethodSource)) { + return TestSourceData.UNKNOWN; + } + + MethodSource methodSource = (MethodSource) testSource; + TestDescriptor suiteDescriptor = getSuiteDescriptor(testDescriptor); + Class testClass = + suiteDescriptor != null ? getJavaClass(suiteDescriptor) : getTestClass(methodSource); + Method testMethod = getTestMethod(methodSource); + String testMethodName = methodSource.getMethodName(); + return new TestSourceData(testClass, testMethod, testMethodName); + } + public static boolean isAssumptionFailure(Throwable throwable) { switch (throwable.getClass().getName()) { case "org.junit.AssumptionViolatedException": diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TestDataFactory.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TestDataFactory.java new file mode 100644 index 00000000000..2fa590bfd67 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TestDataFactory.java @@ -0,0 +1,55 @@ +package datadog.trace.instrumentation.junit5; + +import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; + +public abstract class TestDataFactory { + + private TestDataFactory() {} + + private static volatile Map> + TEST_IDENTIFIER_FACTORY_BY_ENGINE_ID = Collections.emptyMap(); + + private static volatile Map> + TEST_SOURCE_DATA_FACTORY_BY_ENGINE_ID = Collections.emptyMap(); + + public static synchronized void register( + String engineId, + Function testIdentifierFactory, + Function testSourceDataFactory) { + TEST_IDENTIFIER_FACTORY_BY_ENGINE_ID = + addEntry(TEST_IDENTIFIER_FACTORY_BY_ENGINE_ID, engineId, testIdentifierFactory); + TEST_SOURCE_DATA_FACTORY_BY_ENGINE_ID = + addEntry(TEST_SOURCE_DATA_FACTORY_BY_ENGINE_ID, engineId, testSourceDataFactory); + } + + private static Map addEntry(Map originalMap, K key, V value) { + Map updatedMap = new HashMap<>(originalMap); + updatedMap.put(key, value); + return updatedMap; + } + + public static TestIdentifier createTestIdentifier(TestDescriptor testDescriptor) { + UniqueId uniqueId = testDescriptor.getUniqueId(); + return uniqueId + .getEngineId() + .map(TEST_IDENTIFIER_FACTORY_BY_ENGINE_ID::get) + .orElse(JUnitPlatformUtils::toTestIdentifier) + .apply(testDescriptor); + } + + public static TestSourceData createTestSourceData(TestDescriptor testDescriptor) { + UniqueId uniqueId = testDescriptor.getUniqueId(); + return uniqueId + .getEngineId() + .map(TEST_SOURCE_DATA_FACTORY_BY_ENGINE_ID::get) + .orElse(JUnitPlatformUtils::toTestSourceData) + .apply(testDescriptor); + } +} diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TestIdentifierFactory.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TestIdentifierFactory.java deleted file mode 100644 index 4124df83c43..00000000000 --- a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TestIdentifierFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -package datadog.trace.instrumentation.junit5; - -import datadog.trace.api.civisibility.config.TestIdentifier; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; -import org.junit.platform.engine.TestDescriptor; -import org.junit.platform.engine.UniqueId; - -public abstract class TestIdentifierFactory { - - private TestIdentifierFactory() {} - - private static volatile Map> - FACTORY_BY_ENGINE_ID = Collections.emptyMap(); - - public static synchronized void register( - String engineId, Function factory) { - Map> updated = - new HashMap<>(FACTORY_BY_ENGINE_ID); - updated.put(engineId, factory); - FACTORY_BY_ENGINE_ID = updated; - } - - public static TestIdentifier createTestIdentifier(TestDescriptor testDescriptor) { - UniqueId uniqueId = testDescriptor.getUniqueId(); - return uniqueId - .getEngineId() - .map(FACTORY_BY_ENGINE_ID::get) - .orElse(JUnitPlatformUtils::toTestIdentifier) - .apply(testDescriptor); - } -} diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TracingListener.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TracingListener.java index ea47868cae3..3d5eb401581 100644 --- a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TracingListener.java +++ b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/TracingListener.java @@ -1,7 +1,7 @@ package datadog.trace.instrumentation.junit5; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; -import java.lang.reflect.Method; import java.util.List; import java.util.stream.Collectors; import org.junit.platform.engine.EngineExecutionListener; @@ -108,37 +108,22 @@ private void testCaseExecutionStarted(final TestDescriptor testDescriptor) { private void testMethodExecutionStarted(TestDescriptor testDescriptor, MethodSource testSource) { TestDescriptor suiteDescriptor = JUnitPlatformUtils.getSuiteDescriptor(testDescriptor); - Class testClass; - String testSuiteName; - if (suiteDescriptor != null) { - testClass = JUnitPlatformUtils.getJavaClass(suiteDescriptor); - testSuiteName = - testClass != null ? testClass.getName() : suiteDescriptor.getLegacyReportingName(); - } else { - testClass = JUnitPlatformUtils.getTestClass(testSource); - testSuiteName = testSource.getClassName(); - } - String displayName = testDescriptor.getDisplayName(); String testName = testSource.getMethodName(); String testParameters = JUnitPlatformUtils.getParameters(testSource, displayName); List tags = testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); - Method testMethod = JUnitPlatformUtils.getTestMethod(testSource); - String testMethodName = testSource.getMethodName(); + TestSourceData testSourceData = JUnitPlatformUtils.toTestSourceData(testDescriptor); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( suiteDescriptor, testDescriptor, - testSuiteName, testName, testFramework, testFrameworkVersion, testParameters, tags, - testClass, - testMethodName, - testMethod, + testSourceData, JUnitPlatformUtils.isRetry(testDescriptor), null); } @@ -214,37 +199,22 @@ private void testMethodExecutionSkipped( final TestDescriptor testDescriptor, final MethodSource testSource, final String reason) { TestDescriptor suiteDescriptor = JUnitPlatformUtils.getSuiteDescriptor(testDescriptor); - Class testClass; - String testSuiteName; - if (suiteDescriptor != null) { - testClass = JUnitPlatformUtils.getJavaClass(suiteDescriptor); - testSuiteName = - testClass != null ? testClass.getName() : suiteDescriptor.getLegacyReportingName(); - } else { - testClass = JUnitPlatformUtils.getTestClass(testSource); - testSuiteName = testSource.getClassName(); - } - String displayName = testDescriptor.getDisplayName(); String testName = testSource.getMethodName(); String testParameters = JUnitPlatformUtils.getParameters(testSource, displayName); List tags = testDescriptor.getTags().stream().map(TestTag::getName).collect(Collectors.toList()); - Method testMethod = JUnitPlatformUtils.getTestMethod(testSource); - String testMethodName = testSource.getMethodName(); + TestSourceData testSourceData = JUnitPlatformUtils.toTestSourceData(testDescriptor); TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( suiteDescriptor, testDescriptor, - testSuiteName, testName, testFramework, testFrameworkVersion, testParameters, tags, - testClass, - testMethodName, - testMethod, + testSourceData, reason); } } diff --git a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/retry/JUnit5RetryInstrumentation.java b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/retry/JUnit5RetryInstrumentation.java index d591f5b8e40..9d6ba1fd36c 100644 --- a/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/retry/JUnit5RetryInstrumentation.java +++ b/dd-java-agent/instrumentation/junit-5.3/src/main/java/datadog/trace/instrumentation/junit5/retry/JUnit5RetryInstrumentation.java @@ -12,12 +12,13 @@ import datadog.trace.agent.tooling.muzzle.ReferenceProvider; import datadog.trace.api.Config; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.bootstrap.CallDepthThreadLocalMap; import datadog.trace.bootstrap.InstrumentationContext; import datadog.trace.instrumentation.junit5.JUnitPlatformUtils; +import datadog.trace.instrumentation.junit5.TestDataFactory; import datadog.trace.instrumentation.junit5.TestEventsHandlerHolder; -import datadog.trace.instrumentation.junit5.TestIdentifierFactory; import datadog.trace.util.Strings; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import java.util.ArrayList; @@ -60,7 +61,7 @@ public String[] helperClassNames() { packageName + ".TestDescriptorHandle", packageName + ".ThrowableCollectorFactoryWrapper", parentPackageName + ".JUnitPlatformUtils", - parentPackageName + ".TestIdentifierFactory", + parentPackageName + ".TestDataFactory", parentPackageName + ".TestEventsHandlerHolder", }; } @@ -141,9 +142,10 @@ public static Boolean execute(@Advice.This HierarchicalTestExecutorService.TestT return null; } - TestIdentifier testIdentifier = TestIdentifierFactory.createTestIdentifier(testDescriptor); + TestIdentifier testIdentifier = TestDataFactory.createTestIdentifier(testDescriptor); + TestSourceData testSource = TestDataFactory.createTestSourceData(testDescriptor); TestRetryPolicy retryPolicy = - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy(testIdentifier); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy(testIdentifier, testSource); if (!retryPolicy.retriesLeft()) { return null; } diff --git a/dd-java-agent/instrumentation/junit-5.3/src/test/groovy/JUnit5Test.groovy b/dd-java-agent/instrumentation/junit-5.3/src/test/groovy/JUnit5Test.groovy index 8f58d5f18ec..bc658745ad9 100644 --- a/dd-java-agent/instrumentation/junit-5.3/src/test/groovy/JUnit5Test.groovy +++ b/dd-java-agent/instrumentation/junit-5.3/src/test/groovy/JUnit5Test.groovy @@ -1,6 +1,8 @@ import datadog.trace.api.DisableTestTrace import datadog.trace.api.civisibility.config.TestIdentifier import datadog.trace.civisibility.CiVisibilityInstrumentationTest +import datadog.trace.civisibility.diff.FileDiff +import datadog.trace.civisibility.diff.LineDiff import datadog.trace.instrumentation.junit5.TestEventsHandlerHolder import org.example.TestAssumption import org.example.TestAssumptionAndSucceed @@ -140,6 +142,23 @@ class JUnit5Test extends CiVisibilityInstrumentationTest { "test-efd-faulty-session-threshold" | [TestFailedAndSucceed] | 8 | [] } + def "test impacted tests detection #testcaseName"() { + givenImpactedTestsDetectionEnabled(true) + givenDiff(prDiff) + + runTests(tests) + + assertSpansData(testcaseName, expectedTracesCount) + + where: + testcaseName | tests | expectedTracesCount | prDiff + "test-succeed" | [TestSucceed] | 2 | LineDiff.EMPTY + "test-succeed" | [TestSucceed] | 2 | new FileDiff(new HashSet()) + "test-succeed-impacted" | [TestSucceed] | 2 | new FileDiff(new HashSet([DUMMY_SOURCE_PATH])) + "test-succeed" | [TestSucceed] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines()]) + "test-succeed-impacted" | [TestSucceed] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines(DUMMY_TEST_METHOD_START)]) + } + private static void runTests(List> tests) { TestEventsHandlerHolder.startForcefully() diff --git a/dd-java-agent/instrumentation/junit-5.3/src/test/resources/test-succeed-impacted/coverages.ftl b/dd-java-agent/instrumentation/junit-5.3/src/test/resources/test-succeed-impacted/coverages.ftl new file mode 100644 index 00000000000..8878e547a79 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/src/test/resources/test-succeed-impacted/coverages.ftl @@ -0,0 +1 @@ +[ ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/junit-5.3/src/test/resources/test-succeed-impacted/events.ftl b/dd-java-agent/instrumentation/junit-5.3/src/test/resources/test-succeed-impacted/events.ftl new file mode 100644 index 00000000000..61fbd06bc11 --- /dev/null +++ b/dd-java-agent/instrumentation/junit-5.3/src/test/resources/test-succeed-impacted/events.ftl @@ -0,0 +1,151 @@ +[ { + "content" : { + "duration" : ${content_duration}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid}, + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test_session_end", + "test.command" : "junit-5.3", + "test.framework" : "junit5", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id} + }, + "name" : "junit.test_session", + "resource" : "junit-5.3", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_session_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_2}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_2}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_module_end", + "test.framework" : "junit5", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "junit-5.3", + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2} + }, + "name" : "junit.test_module", + "resource" : "junit-5.3", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_2}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_module_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_3}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_3}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_suite_end", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "junit5", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "junit-5.3", + "test.source.file" : "dummy_source_path", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3}, + "test.source.end" : 19, + "test.source.start" : 11 + }, + "name" : "junit.test_suite", + "resource" : "org.example.TestSucceed", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_3}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} + }, + "type" : "test_suite_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_4}, + "error" : 0, + "meta" : { + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "junit", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "junit5", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.is_modified" : "true", + "test.module" : "junit-5.3", + "test.name" : "test_succeed", + "test.source.file" : "dummy_source_path", + "test.source.method" : "test_succeed()V", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id}, + "test.source.end" : 18, + "test.source.start" : 12 + }, + "name" : "junit.test", + "parent_id" : ${content_parent_id}, + "resource" : "org.example.TestSucceed.test_succeed", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "span_id" : ${content_span_id}, + "start" : ${content_start_4}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id}, + "trace_id" : ${content_trace_id} + }, + "type" : "test", + "version" : 2 +} ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateTracingHook.java b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateTracingHook.java index 777fb8cc761..c6058083ff7 100644 --- a/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateTracingHook.java +++ b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/KarateTracingHook.java @@ -16,6 +16,7 @@ import datadog.trace.api.Config; import datadog.trace.api.civisibility.InstrumentationBridge; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestDescriptor; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; @@ -96,7 +97,6 @@ public boolean beforeScenario(ScenarioRuntime sr) { return true; } Scenario scenario = sr.scenario; - Feature feature = scenario.getFeature(); // There are cases when Karate does not call "beforeFeature" hooks, // for example when using built-in retries @@ -108,7 +108,6 @@ public boolean beforeScenario(ScenarioRuntime sr) { TestSuiteDescriptor suiteDescriptor = KarateUtils.toSuiteDescriptor(sr.featureRuntime); TestDescriptor testDescriptor = KarateUtils.toTestDescriptor(sr); - String featureName = feature.getNameForReport(); String scenarioName = KarateUtils.getScenarioName(scenario); String parameters = KarateUtils.getParameters(scenario); Collection categories = scenario.getTagsEffective().getTagKeys(); @@ -120,15 +119,12 @@ public boolean beforeScenario(ScenarioRuntime sr) { TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestIgnore( suiteDescriptor, testDescriptor, - featureName, scenarioName, FRAMEWORK_NAME, FRAMEWORK_VERSION, parameters, categories, - null, - null, - null, + TestSourceData.UNKNOWN, InstrumentationBridge.ITR_SKIP_REASON); return false; } @@ -137,15 +133,12 @@ public boolean beforeScenario(ScenarioRuntime sr) { TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( suiteDescriptor, testDescriptor, - featureName, scenarioName, FRAMEWORK_NAME, FRAMEWORK_VERSION, parameters, categories, - null, - null, - null, + TestSourceData.UNKNOWN, sr.magicVariables.containsKey(KarateUtils.RETRY_MAGIC_VARIABLE), null); return true; diff --git a/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/RetryContext.java b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/RetryContext.java index 373fb6c6cf1..cdde1ea395a 100644 --- a/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/RetryContext.java +++ b/dd-java-agent/instrumentation/karate/src/main/java/datadog/trace/instrumentation/karate/RetryContext.java @@ -2,6 +2,7 @@ import com.intuit.karate.core.Scenario; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; public class RetryContext { @@ -39,6 +40,7 @@ public TestRetryPolicy getRetryPolicy() { public static RetryContext create(Scenario scenario) { TestIdentifier testIdentifier = KarateUtils.toTestIdentifier(scenario); return new RetryContext( - TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy(testIdentifier)); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy( + testIdentifier, TestSourceData.UNKNOWN)); } } diff --git a/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/DatadogReporter.java b/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/DatadogReporter.java index 214d8509e3a..86eb1a3a184 100644 --- a/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/DatadogReporter.java +++ b/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/DatadogReporter.java @@ -2,13 +2,13 @@ import datadog.trace.api.civisibility.InstrumentationBridge; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestDescriptor; import datadog.trace.api.civisibility.events.TestEventsHandler; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; import datadog.trace.instrumentation.scalatest.retry.SuppressedTestFailedException; -import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; import org.scalatest.events.Event; @@ -144,22 +144,17 @@ private static void onTestStart(TestStarting event) { categories = Collections.emptyList(); } Class testClass = ScalatestUtils.getClass(event.suiteClassName()); - String testMethodName = null; - Method testMethod = null; TestRetryPolicy retryPolicy = context.popRetryPolicy(testIdentifier); eventHandler.onTestStart( new TestSuiteDescriptor(testSuiteName, testClass), new TestDescriptor(testSuiteName, testClass, testName, testParameters, testQualifier), - testSuiteName, testName, TEST_FRAMEWORK, TEST_FRAMEWORK_VERSION, testParameters, categories, - testClass, - testMethodName, - testMethod, + new TestSourceData(testClass, null, null), retryPolicy != null && retryPolicy.currentExecutionIsRetry(), null); } @@ -207,8 +202,6 @@ private static void onTestIgnore(TestIgnored event) { String testParameters = null; Collection categories = Collections.emptyList(); Class testClass = ScalatestUtils.getClass(event.suiteClassName()); - String testMethodName = null; - Method testMethod = null; String reason; TestIdentifier skippableTest = new TestIdentifier(testSuiteName, testName, null); @@ -221,15 +214,12 @@ private static void onTestIgnore(TestIgnored event) { eventHandler.onTestIgnore( new TestSuiteDescriptor(testSuiteName, testClass), new TestDescriptor(testSuiteName, testClass, testName, testParameters, testQualifier), - testSuiteName, testName, TEST_FRAMEWORK, TEST_FRAMEWORK_VERSION, testParameters, categories, - testClass, - testMethodName, - testMethod, + new TestSourceData(testClass, null, null), reason); } diff --git a/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/RunContext.java b/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/RunContext.java index d62c6cb7898..64400fa7617 100644 --- a/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/RunContext.java +++ b/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/RunContext.java @@ -2,6 +2,7 @@ import datadog.trace.api.civisibility.InstrumentationBridge; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestDescriptor; import datadog.trace.api.civisibility.events.TestEventsHandler; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; @@ -116,8 +117,9 @@ public boolean isUnskippable(TestIdentifier test, Map> tags) return testTags != null && testTags.contains(InstrumentationBridge.ITR_UNSKIPPABLE_TAG); } - public TestRetryPolicy retryPolicy(TestIdentifier testIdentifier) { - return retryPolicies.computeIfAbsent(testIdentifier, eventHandler::retryPolicy); + public TestRetryPolicy retryPolicy(TestIdentifier testIdentifier, TestSourceData testSourceData) { + return retryPolicies.computeIfAbsent( + testIdentifier, test -> eventHandler.retryPolicy(test, testSourceData)); } @Nullable diff --git a/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/retry/ScalatestRetryInstrumentation.java b/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/retry/ScalatestRetryInstrumentation.java index ed2c9e24db3..bfe9c35f07e 100644 --- a/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/retry/ScalatestRetryInstrumentation.java +++ b/dd-java-agent/instrumentation/scalatest/src/main/java/datadog/trace/instrumentation/scalatest/retry/ScalatestRetryInstrumentation.java @@ -9,6 +9,7 @@ import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.api.Config; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.instrumentation.scalatest.RunContext; import datadog.trace.instrumentation.scalatest.ScalatestUtils; @@ -83,7 +84,8 @@ public static void beforeTest( int runStamp = args.tracker().nextOrdinal().runStamp(); RunContext context = RunContext.getOrCreate(runStamp); TestIdentifier testIdentifier = new TestIdentifier(suite.suiteId(), testName, null); - TestRetryPolicy retryPolicy = context.retryPolicy(testIdentifier); + TestSourceData testSourceData = new TestSourceData(suite.getClass(), null, null); + TestRetryPolicy retryPolicy = context.retryPolicy(testIdentifier, testSourceData); invokeWithFixture = new TestExecutionWrapper(invokeWithFixture, retryPolicy); } diff --git a/dd-java-agent/instrumentation/scalatest/src/test/groovy/ScalatestTest.groovy b/dd-java-agent/instrumentation/scalatest/src/test/groovy/ScalatestTest.groovy index 1646f5173ac..d63ec8f5085 100644 --- a/dd-java-agent/instrumentation/scalatest/src/test/groovy/ScalatestTest.groovy +++ b/dd-java-agent/instrumentation/scalatest/src/test/groovy/ScalatestTest.groovy @@ -1,5 +1,7 @@ import datadog.trace.api.civisibility.config.TestIdentifier import datadog.trace.civisibility.CiVisibilityInstrumentationTest +import datadog.trace.civisibility.diff.FileDiff +import datadog.trace.civisibility.diff.LineDiff import datadog.trace.instrumentation.scalatest.ScalatestUtils import org.example.TestFailed import org.example.TestFailedParameterized @@ -84,6 +86,23 @@ class ScalatestTest extends CiVisibilityInstrumentationTest { "test-efd-faulty-session-threshold" | [TestSucceedMoreCases] | 8 | [] } + def "test impacted tests detection #testcaseName"() { + givenImpactedTestsDetectionEnabled(true) + givenDiff(prDiff) + + runTests(tests) + + assertSpansData(testcaseName, expectedTracesCount) + + where: + testcaseName | tests | expectedTracesCount | prDiff + "test-succeed" | [TestSucceed] | 2 | LineDiff.EMPTY + "test-succeed" | [TestSucceed] | 2 | new FileDiff(new HashSet()) + "test-succeed-impacted" | [TestSucceed] | 2 | new FileDiff(new HashSet([DUMMY_SOURCE_PATH])) + "test-succeed" | [TestSucceed] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines()]) + "test-succeed-impacted" | [TestSucceed] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines(DUMMY_TEST_METHOD_START)]) + } + @Override String instrumentedLibraryName() { return "scalatest" diff --git a/dd-java-agent/instrumentation/scalatest/src/test/resources/test-succeed-impacted/coverages.ftl b/dd-java-agent/instrumentation/scalatest/src/test/resources/test-succeed-impacted/coverages.ftl new file mode 100644 index 00000000000..8878e547a79 --- /dev/null +++ b/dd-java-agent/instrumentation/scalatest/src/test/resources/test-succeed-impacted/coverages.ftl @@ -0,0 +1 @@ +[ ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/scalatest/src/test/resources/test-succeed-impacted/events.ftl b/dd-java-agent/instrumentation/scalatest/src/test/resources/test-succeed-impacted/events.ftl new file mode 100644 index 00000000000..dc3476e6a55 --- /dev/null +++ b/dd-java-agent/instrumentation/scalatest/src/test/resources/test-succeed-impacted/events.ftl @@ -0,0 +1,148 @@ +[ { + "content" : { + "duration" : ${content_duration}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid}, + "component" : "scalatest", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_suite_end", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "scalatest", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "scalatest", + "test.source.file" : "dummy_source_path", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}, + "test.source.end" : 19, + "test.source.start" : 11 + }, + "name" : "scalatest.test_suite", + "resource" : "org.example.TestSucceed", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} + }, + "type" : "test_suite_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_2}, + "error" : 0, + "meta" : { + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "scalatest", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "scalatest", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.is_modified" : "true", + "test.module" : "scalatest", + "test.name" : "Example.add adds two numbers", + "test.source.file" : "dummy_source_path", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id} + }, + "name" : "scalatest.test", + "parent_id" : ${content_parent_id}, + "resource" : "org.example.TestSucceed.Example.add adds two numbers", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "span_id" : ${content_span_id}, + "start" : ${content_start_2}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id}, + "trace_id" : ${content_trace_id} + }, + "type" : "test", + "version" : 2 +}, { + "content" : { + "duration" : ${content_duration_3}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_2}, + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "scalatest", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test_session_end", + "test.command" : "scalatest", + "test.framework" : "scalatest", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id} + }, + "name" : "scalatest.test_session", + "resource" : "scalatest", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_3}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_session_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_4}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_3}, + "component" : "scalatest", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_module_end", + "test.framework" : "scalatest", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "scalatest", + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4} + }, + "name" : "scalatest.test_module", + "resource" : "scalatest", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_4}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_module_end", + "version" : 1 +} ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TestNGUtils.java b/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TestNGUtils.java index 89911376e88..3fd6df4794f 100644 --- a/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TestNGUtils.java +++ b/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TestNGUtils.java @@ -2,6 +2,7 @@ import datadog.json.JsonWriter; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; import java.io.InputStream; import java.lang.invoke.MethodHandle; @@ -44,7 +45,7 @@ public abstract class TestNGUtils { private static final MethodHandle TEST_METHOD_GET_RETRY_ANALYZER_LEGACY = METHOD_HANDLES.method(ITestNGMethod.class, "getRetryAnalyzer"); - public static Class getTestClass(final ITestResult result) { + private static Class getTestClass(final ITestResult result) { IClass testClass = result.getTestClass(); if (testClass == null) { return null; @@ -52,7 +53,7 @@ public static Class getTestClass(final ITestResult result) { return testClass.getRealClass(); } - public static Method getTestMethod(final ITestResult result) { + private static Method getTestMethod(final ITestResult result) { ITestNGMethod method = result.getMethod(); if (method == null) { return null; @@ -64,6 +65,12 @@ public static Method getTestMethod(final ITestResult result) { return constructorOrMethod.getMethod(); } + public static TestSourceData toTestSourceData(final ITestResult result) { + Class testClass = getTestClass(result); + Method testMethod = getTestMethod(result); + return new TestSourceData(testClass, testMethod); + } + public static String getParameters(final ITestResult result) { return getParameters(result.getParameters()); } diff --git a/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TracingListener.java b/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TracingListener.java index 50989d92f9f..d4bb61a3bc6 100644 --- a/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TracingListener.java +++ b/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/TracingListener.java @@ -1,9 +1,9 @@ package datadog.trace.instrumentation.testng; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; import datadog.trace.instrumentation.testng.retry.RetryAnalyzer; -import java.lang.reflect.Method; import java.util.List; import org.testng.IConfigurationListener; import org.testng.IRetryAnalyzer; @@ -75,26 +75,21 @@ public void onConfigurationSkip(ITestResult result) { public void onTestStart(final ITestResult result) { TestSuiteDescriptor suiteDescriptor = TestNGUtils.toSuiteDescriptor(result.getMethod().getTestClass()); - String testSuiteName = result.getInstanceName(); String testName = (result.getName() != null) ? result.getName() : result.getMethod().getMethodName(); String testParameters = TestNGUtils.getParameters(result); List groups = TestNGUtils.getGroups(result); - Class testClass = TestNGUtils.getTestClass(result); - Method testMethod = TestNGUtils.getTestMethod(result); - String testMethodName = testMethod != null ? testMethod.getName() : null; + TestSourceData testSourceData = TestNGUtils.toTestSourceData(result); + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.onTestStart( suiteDescriptor, result, - testSuiteName, testName, FRAMEWORK_NAME, FRAMEWORK_VERSION, testParameters, groups, - testClass, - testMethodName, - testMethod, + testSourceData, isRetry(result), null); } diff --git a/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/retry/RetryAnalyzer.java b/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/retry/RetryAnalyzer.java index eae7fd288fa..8af7aa7a7f3 100644 --- a/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/retry/RetryAnalyzer.java +++ b/dd-java-agent/instrumentation/testng/src/main/java/datadog/trace/instrumentation/testng/retry/RetryAnalyzer.java @@ -1,6 +1,7 @@ package datadog.trace.instrumentation.testng.retry; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.instrumentation.testng.TestEventsHandlerHolder; import datadog.trace.instrumentation.testng.TestNGUtils; @@ -20,7 +21,10 @@ public boolean retry(ITestResult result) { synchronized (this) { if (retryPolicy == null) { TestIdentifier testIdentifier = TestNGUtils.toTestIdentifier(result); - retryPolicy = TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy(testIdentifier); + TestSourceData testSourceData = TestNGUtils.toTestSourceData(result); + retryPolicy = + TestEventsHandlerHolder.TEST_EVENTS_HANDLER.retryPolicy( + testIdentifier, testSourceData); } } } diff --git a/dd-java-agent/instrumentation/testng/src/testFixtures/groovy/datadog/trace/instrumentation/testng/TestNGTest.groovy b/dd-java-agent/instrumentation/testng/src/testFixtures/groovy/datadog/trace/instrumentation/testng/TestNGTest.groovy index 28a892fd542..89c7b8f505f 100644 --- a/dd-java-agent/instrumentation/testng/src/testFixtures/groovy/datadog/trace/instrumentation/testng/TestNGTest.groovy +++ b/dd-java-agent/instrumentation/testng/src/testFixtures/groovy/datadog/trace/instrumentation/testng/TestNGTest.groovy @@ -2,6 +2,8 @@ package datadog.trace.instrumentation.testng import datadog.trace.api.civisibility.config.TestIdentifier import datadog.trace.civisibility.CiVisibilityInstrumentationTest +import datadog.trace.civisibility.diff.FileDiff +import datadog.trace.civisibility.diff.LineDiff import org.example.TestError import org.example.TestFailed import org.example.TestFailedAndSucceed @@ -151,6 +153,23 @@ abstract class TestNGTest extends CiVisibilityInstrumentationTest { "test-efd-faulty-session-threshold" | [TestFailedAndSucceed] | 8 | [] } + def "test impacted tests detection #testcaseName"() { + givenImpactedTestsDetectionEnabled(true) + givenDiff(prDiff) + + runTests(tests) + + assertSpansData(testcaseName, expectedTracesCount) + + where: + testcaseName | tests | expectedTracesCount | prDiff + "test-succeed" | [TestSucceed] | 2 | LineDiff.EMPTY + "test-succeed" | [TestSucceed] | 2 | new FileDiff(new HashSet()) + "test-succeed-impacted" | [TestSucceed] | 2 | new FileDiff(new HashSet([DUMMY_SOURCE_PATH])) + "test-succeed" | [TestSucceed] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines()]) + "test-succeed-impacted" | [TestSucceed] | 2 | new LineDiff([(DUMMY_SOURCE_PATH): lines(DUMMY_TEST_METHOD_START)]) + } + private static boolean isEFDSupported() { TracingListener.FRAMEWORK_VERSION >= "7.5" } diff --git a/dd-java-agent/instrumentation/testng/testng-6/src/main/java/datadog/trace/instrumentation/testng6/TestNGClassListenerInstrumentation.java b/dd-java-agent/instrumentation/testng/testng-6/src/main/java/datadog/trace/instrumentation/testng6/TestNGClassListenerInstrumentation.java index c2bdc0cd3c0..2568b8b532f 100644 --- a/dd-java-agent/instrumentation/testng/testng-6/src/main/java/datadog/trace/instrumentation/testng6/TestNGClassListenerInstrumentation.java +++ b/dd-java-agent/instrumentation/testng/testng-6/src/main/java/datadog/trace/instrumentation/testng6/TestNGClassListenerInstrumentation.java @@ -65,6 +65,7 @@ public String[] helperClassNames() { } public static class InvokeBeforeClassAdvice { + @SuppressWarnings("bytebuddy-exception-suppression") @Advice.OnMethodEnter public static void invokeBeforeClass( @Advice.FieldValue("m_testContext") final ITestContext testContext, @@ -82,6 +83,7 @@ public static String muzzleCheck(final DataProvider dataProvider) { } public static class InvokeAfterClassAdvice { + @SuppressWarnings("bytebuddy-exception-suppression") @Advice.OnMethodExit public static void invokeAfterClass( @Advice.FieldValue("m_testContext") final ITestContext testContext, diff --git a/dd-java-agent/instrumentation/testng/testng-6/src/test/resources/test-succeed-impacted/coverages.ftl b/dd-java-agent/instrumentation/testng/testng-6/src/test/resources/test-succeed-impacted/coverages.ftl new file mode 100644 index 00000000000..8878e547a79 --- /dev/null +++ b/dd-java-agent/instrumentation/testng/testng-6/src/test/resources/test-succeed-impacted/coverages.ftl @@ -0,0 +1 @@ +[ ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/testng/testng-6/src/test/resources/test-succeed-impacted/events.ftl b/dd-java-agent/instrumentation/testng/testng-6/src/test/resources/test-succeed-impacted/events.ftl new file mode 100644 index 00000000000..262fe32c32d --- /dev/null +++ b/dd-java-agent/instrumentation/testng/testng-6/src/test/resources/test-succeed-impacted/events.ftl @@ -0,0 +1,151 @@ +[ { + "content" : { + "duration" : ${content_duration}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid}, + "component" : "testng", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_suite_end", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "testng", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "testng-6", + "test.source.file" : "dummy_source_path", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}, + "test.source.end" : 19, + "test.source.start" : 11 + }, + "name" : "testng.test_suite", + "resource" : "org.example.TestSucceed", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} + }, + "type" : "test_suite_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_2}, + "error" : 0, + "meta" : { + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "testng", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "testng", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.is_modified" : "true", + "test.module" : "testng-6", + "test.name" : "test_succeed", + "test.source.file" : "dummy_source_path", + "test.source.method" : "test_succeed()V", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id}, + "test.source.end" : 18, + "test.source.start" : 12 + }, + "name" : "testng.test", + "parent_id" : ${content_parent_id}, + "resource" : "org.example.TestSucceed.test_succeed", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "span_id" : ${content_span_id}, + "start" : ${content_start_2}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id}, + "trace_id" : ${content_trace_id} + }, + "type" : "test", + "version" : 2 +}, { + "content" : { + "duration" : ${content_duration_3}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_2}, + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "testng", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test_session_end", + "test.command" : "testng-6", + "test.framework" : "testng", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id} + }, + "name" : "testng.test_session", + "resource" : "testng-6", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_3}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_session_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_4}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_3}, + "component" : "testng", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_module_end", + "test.framework" : "testng", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "testng-6", + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4} + }, + "name" : "testng.test_module", + "resource" : "testng-6", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_4}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_module_end", + "version" : 1 +} ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/testng/testng-7/src/test/resources/test-succeed-impacted/coverages.ftl b/dd-java-agent/instrumentation/testng/testng-7/src/test/resources/test-succeed-impacted/coverages.ftl new file mode 100644 index 00000000000..8878e547a79 --- /dev/null +++ b/dd-java-agent/instrumentation/testng/testng-7/src/test/resources/test-succeed-impacted/coverages.ftl @@ -0,0 +1 @@ +[ ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/testng/testng-7/src/test/resources/test-succeed-impacted/events.ftl b/dd-java-agent/instrumentation/testng/testng-7/src/test/resources/test-succeed-impacted/events.ftl new file mode 100644 index 00000000000..e5e7aa40252 --- /dev/null +++ b/dd-java-agent/instrumentation/testng/testng-7/src/test/resources/test-succeed-impacted/events.ftl @@ -0,0 +1,151 @@ +[ { + "content" : { + "duration" : ${content_duration}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid}, + "component" : "testng", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_suite_end", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "testng", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "testng-7", + "test.source.file" : "dummy_source_path", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}, + "test.source.end" : 19, + "test.source.start" : 11 + }, + "name" : "testng.test_suite", + "resource" : "org.example.TestSucceed", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} + }, + "type" : "test_suite_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_2}, + "error" : 0, + "meta" : { + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "testng", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test", + "test.codeowners" : "[\"owner1\",\"owner2\"]", + "test.framework" : "testng", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.is_modified" : "true", + "test.module" : "testng-7", + "test.name" : "test_succeed", + "test.source.file" : "dummy_source_path", + "test.source.method" : "test_succeed()V", + "test.status" : "pass", + "test.suite" : "org.example.TestSucceed", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id}, + "test.source.end" : 18, + "test.source.start" : 12 + }, + "name" : "testng.test", + "parent_id" : ${content_parent_id}, + "resource" : "org.example.TestSucceed.test_succeed", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "span_id" : ${content_span_id}, + "start" : ${content_start_2}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id}, + "trace_id" : ${content_trace_id} + }, + "type" : "test", + "version" : 2 +}, { + "content" : { + "duration" : ${content_duration_3}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_2}, + "_dd.profiling.ctx" : "test", + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "component" : "testng", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "runtime-id" : ${content_meta_runtime_id}, + "span.kind" : "test_session_end", + "test.command" : "testng-7", + "test.framework" : "testng", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id} + }, + "name" : "testng.test_session", + "resource" : "testng-7", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_3}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_session_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_4}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_3}, + "component" : "testng", + "dummy_ci_tag" : "dummy_ci_tag_value", + "env" : "none", + "library_version" : ${content_meta_library_version}, + "span.kind" : "test_module_end", + "test.framework" : "testng", + "test.framework_version" : ${content_meta_test_framework_version}, + "test.module" : "testng-7", + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "session-name" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4} + }, + "name" : "testng.test_module", + "resource" : "testng-7", + "service" : "worker.org.gradle.process.internal.worker.gradleworkermain", + "start" : ${content_start_4}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_module_end", + "version" : 1 +} ] \ No newline at end of file diff --git a/dd-java-agent/instrumentation/weaver/src/main/java/datadog/trace/instrumentation/weaver/DatadogWeaverReporter.java b/dd-java-agent/instrumentation/weaver/src/main/java/datadog/trace/instrumentation/weaver/DatadogWeaverReporter.java index 78180cb65ad..fed3a83eefe 100644 --- a/dd-java-agent/instrumentation/weaver/src/main/java/datadog/trace/instrumentation/weaver/DatadogWeaverReporter.java +++ b/dd-java-agent/instrumentation/weaver/src/main/java/datadog/trace/instrumentation/weaver/DatadogWeaverReporter.java @@ -1,6 +1,7 @@ package datadog.trace.instrumentation.weaver; import datadog.trace.api.civisibility.InstrumentationBridge; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.events.TestDescriptor; import datadog.trace.api.civisibility.events.TestEventsHandler; import datadog.trace.api.civisibility.events.TestSuiteDescriptor; @@ -98,15 +99,12 @@ public static void onTestFinished(TestFinished event, TaskDef taskDef) { TEST_EVENTS_HANDLER.onTestStart( testSuiteDescriptor, testDescriptor, - testSuiteName, testName, TEST_FRAMEWORK, TEST_FRAMEWORK_VERSION, testParameters, categories, - testClass, - testMethodName, - testMethod, + new TestSourceData(testClass, testMethod, testMethodName), isRetry, startMicros); diff --git a/dd-smoke-tests/backend-mock/src/main/groovy/datadog/smoketest/MockBackend.groovy b/dd-smoke-tests/backend-mock/src/main/groovy/datadog/smoketest/MockBackend.groovy index 0e1b1a90d27..6b91b597215 100644 --- a/dd-smoke-tests/backend-mock/src/main/groovy/datadog/smoketest/MockBackend.groovy +++ b/dd-smoke-tests/backend-mock/src/main/groovy/datadog/smoketest/MockBackend.groovy @@ -9,6 +9,7 @@ import spock.util.concurrent.PollingConditions import java.util.concurrent.ConcurrentLinkedQueue import java.util.concurrent.CopyOnWriteArrayList +import java.util.stream.Collectors import java.util.zip.GZIPInputStream import java.util.zip.GZIPOutputStream @@ -30,6 +31,7 @@ class MockBackend implements AutoCloseable { private final Collection> skippableTests = new CopyOnWriteArrayList<>() private final Collection> flakyTests = new CopyOnWriteArrayList<>() + private final Collection changedFiles = new CopyOnWriteArrayList<>() private boolean itrEnabled = true private boolean codeCoverageEnabled = true @@ -38,6 +40,8 @@ class MockBackend implements AutoCloseable { private boolean flakyRetriesEnabled = false + private boolean impactedTestsDetectionEnabled = false + void reset() { receivedTraces.clear() receivedCoverages.clear() @@ -47,6 +51,7 @@ class MockBackend implements AutoCloseable { skippableTests.clear() flakyTests.clear() + changedFiles.clear() } @Override @@ -70,6 +75,14 @@ class MockBackend implements AutoCloseable { skippableTests.add(["module": module, "suite": suite, "name": name, "coverage": coverage ]) } + void givenImpactedTestsDetection(boolean impactedTestsDetectionEnabled) { + this.impactedTestsDetectionEnabled = impactedTestsDetectionEnabled + } + + void givenChangedFile(String relativePath) { + changedFiles.add(relativePath) + } + String getIntakeUrl() { return intakeServer.address.toString() } @@ -113,7 +126,8 @@ class MockBackend implements AutoCloseable { "itr_enabled": $itrEnabled, "code_coverage": $codeCoverageEnabled, "tests_skipping": $testsSkippingEnabled, - "flaky_test_retries_enabled": $flakyRetriesEnabled + "flaky_test_retries_enabled": $flakyRetriesEnabled, + "impacted_tests_enabled": $impactedTestsDetectionEnabled } } }""").bytes) @@ -236,6 +250,23 @@ class MockBackend implements AutoCloseable { response.status(200).send() } + + prefix("/api/v2/ci/tests/diffs") { + response.status(200) + .addHeader("Content-Encoding", "gzip") + .send(MockBackend.compress((""" + { + "data": { + "type": "ci_app_tests_diffs_response", + "id": "", + "attributes": { + "base_sha": "ef733331f7cee9b1c89d82df87942d8606edf3f7", + "files": [ ${changedFiles.stream().map(f -> '"' + f + '"').collect(Collectors.joining(","))} ] + } + } + } + """).bytes)) + } } } diff --git a/dd-smoke-tests/maven/src/test/groovy/datadog/smoketest/MavenSmokeTest.groovy b/dd-smoke-tests/maven/src/test/groovy/datadog/smoketest/MavenSmokeTest.groovy index 02fe0cb022e..314f7f7af39 100644 --- a/dd-smoke-tests/maven/src/test/groovy/datadog/smoketest/MavenSmokeTest.groovy +++ b/dd-smoke-tests/maven/src/test/groovy/datadog/smoketest/MavenSmokeTest.groovy @@ -70,7 +70,12 @@ class MavenSmokeTest extends CiVisibilitySmokeTest { mockBackend.givenTestsSkipping(testsSkipping) mockBackend.givenSkippableTest("Maven Smoke Tests Project maven-surefire-plugin default-test", "datadog.smoke.TestSucceed", "test_to_skip_with_itr", ["src/main/java/datadog/smoke/Calculator.java": bits(9)]) - def exitCode = whenRunningMavenBuild(jacocoCoverage, commandLineParams) + mockBackend.givenImpactedTestsDetection(true) + + def agentArgs = jacocoCoverage ? [ + "${Strings.propertyNameToSystemPropertyName(CiVisibilityConfig.CIVISIBILITY_JACOCO_PLUGIN_VERSION)}=${JACOCO_PLUGIN_VERSION}" as String + ] : [] + def exitCode = whenRunningMavenBuild(agentArgs, commandLineParams) if (expectSuccess) { assert exitCode == 0 @@ -103,6 +108,26 @@ class MavenSmokeTest extends CiVisibilitySmokeTest { "test_successful_maven_run_multiple_forks" | LATEST_MAVEN_VERSION | 5 | 1 | true | true | false | true | [] | 17 } + def "test impacted tests detection"() { + givenWrapperPropertiesFile(mavenVersion) + givenMavenProjectFiles(projectName) + givenMavenDependenciesAreLoaded(projectName, mavenVersion) + + mockBackend.givenImpactedTestsDetection(true) + mockBackend.givenChangedFile("src/test/java/datadog/smoke/TestSucceed.java") + + def exitCode = whenRunningMavenBuild([ + "${Strings.propertyNameToSystemPropertyName(CiVisibilityConfig.CIVISIBILITY_GIT_CLIENT_ENABLED)}=false" as String + ], []) + assert exitCode == 0 + + verifyEventsAndCoverages(projectName, "maven", mavenVersion, mockBackend.waitForEvents(5), mockBackend.waitForCoverages(1)) + + where: + projectName | mavenVersion + "test_successful_maven_run_impacted_tests" | "3.9.9" + } + private void givenWrapperPropertiesFile(String mavenVersion) { def distributionUrl = "https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/${mavenVersion}/apache-maven-${mavenVersion}-bin.zip" @@ -166,7 +191,7 @@ class MavenSmokeTest extends CiVisibilitySmokeTest { private static final Collection LOADED_DEPENDENCIES = new HashSet<>() private void retryUntilSuccessfulOrNoAttemptsLeft(List mvnCommand) { - def processBuilder = createProcessBuilder(mvnCommand, false, false) + def processBuilder = createProcessBuilder(mvnCommand, false, []) for (int attempt = 0; attempt < DEPENDENCIES_DOWNLOAD_RETRIES; attempt++) { def exitCode = runProcess(processBuilder.start()) if (exitCode == 0) { @@ -176,8 +201,8 @@ class MavenSmokeTest extends CiVisibilitySmokeTest { throw new AssertionError((Object) "Tried $DEPENDENCIES_DOWNLOAD_RETRIES times to execute $mvnCommand and failed") } - private int whenRunningMavenBuild(boolean injectJacoco, List additionalCommandLineParams) { - def processBuilder = createProcessBuilder(["-B", "test"] + additionalCommandLineParams, true, injectJacoco) + private int whenRunningMavenBuild(List additionalAgentArgs, List additionalCommandLineParams) { + def processBuilder = createProcessBuilder(["-B", "test"] + additionalCommandLineParams, true, additionalAgentArgs) processBuilder.environment().put("DD_API_KEY", "01234567890abcdef123456789ABCDEF") @@ -198,13 +223,13 @@ class MavenSmokeTest extends CiVisibilitySmokeTest { return p.exitValue() } - ProcessBuilder createProcessBuilder(List mvnCommand, boolean runWithAgent, boolean injectJacoco) { + ProcessBuilder createProcessBuilder(List mvnCommand, boolean runWithAgent, List additionalAgentArgs) { String mavenRunnerShadowJar = System.getProperty("datadog.smoketest.maven.jar.path") assert new File(mavenRunnerShadowJar).isFile() List command = new ArrayList<>() command.add(javaPath()) - command.addAll(jvmArguments(runWithAgent, injectJacoco)) + command.addAll(jvmArguments(runWithAgent, additionalAgentArgs)) command.addAll((String[]) ["-jar", mavenRunnerShadowJar]) command.addAll(programArguments()) command.addAll(mvnCommand) @@ -222,7 +247,7 @@ class MavenSmokeTest extends CiVisibilitySmokeTest { return System.getProperty("java.home") + separator + "bin" + separator + "java" } - List jvmArguments(boolean runWithAgent, boolean injectJacoco) { + List jvmArguments(boolean runWithAgent, List additionalAgentArgs) { def arguments = [ "-D${MavenWrapperMain.MVNW_VERBOSE}=true".toString(), "-Duser.dir=${projectHome.toAbsolutePath()}".toString(), @@ -249,9 +274,7 @@ class MavenSmokeTest extends CiVisibilitySmokeTest { "${Strings.propertyNameToSystemPropertyName(CiVisibilityConfig.CIVISIBILITY_AGENTLESS_URL)}=${mockBackend.intakeUrl}," + "${Strings.propertyNameToSystemPropertyName(CiVisibilityConfig.CIVISIBILITY_FLAKY_RETRY_ONLY_KNOWN_FLAKES)}=true," - if (injectJacoco) { - agentArgument += "${Strings.propertyNameToSystemPropertyName(CiVisibilityConfig.CIVISIBILITY_JACOCO_PLUGIN_VERSION)}=${JACOCO_PLUGIN_VERSION}," - } + agentArgument += additionalAgentArgs.join(",") arguments += agentArgument.toString() } diff --git a/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/coverages.ftl b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/coverages.ftl new file mode 100644 index 00000000000..cb8d26b550c --- /dev/null +++ b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/coverages.ftl @@ -0,0 +1,19 @@ +[ { + "files" : [ { + "filename" : "src/test/java/datadog/smoke/TestSucceed.java" + }, { + "filename" : "src/main/java/datadog/smoke/Calculator.java" + } ], + "span_id" : ${content_span_id_6}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} +}, { + "files" : [ { + "filename" : "src/test/java/datadog/smoke/TestSucceed.java" + }, { + "filename" : "src/main/java/datadog/smoke/Calculator.java" + } ], + "span_id" : ${content_span_id_5}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} +} ] \ No newline at end of file diff --git a/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/events.ftl b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/events.ftl new file mode 100644 index 00000000000..894716aab40 --- /dev/null +++ b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/events.ftl @@ -0,0 +1,368 @@ +[ { + "content" : { + "duration" : ${content_duration}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid}, + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "ci.workspace_path" : ${content_meta_ci_workspace_path}, + "component" : "maven", + "env" : "integration-test", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "os.architecture" : ${content_meta_os_architecture}, + "os.platform" : ${content_meta_os_platform}, + "os.version" : ${content_meta_os_version}, + "runtime-id" : ${content_meta_runtime_id}, + "runtime.name" : ${content_meta_runtime_name}, + "runtime.vendor" : ${content_meta_runtime_vendor}, + "runtime.version" : ${content_meta_runtime_version}, + "span.kind" : "test_session_end", + "test.code_coverage.enabled" : "true", + "test.command" : "mvn -B test", + "test.framework" : "junit4", + "test.framework_version" : "4.13.2", + "test.itr.tests_skipping.enabled" : "true", + "test.itr.tests_skipping.type" : "test", + "test.status" : "pass", + "test.toolchain" : ${content_meta_test_toolchain}, + "test.type" : "test", + "test_session.name" : "mvn -B test" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id}, + "test.itr.tests_skipping.count" : 0 + }, + "name" : "maven.test_session", + "resource" : "Maven Smoke Tests Project", + "service" : "test-maven-service", + "start" : ${content_start}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_session_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_2}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_2}, + "ci.workspace_path" : ${content_meta_ci_workspace_path}, + "component" : "maven", + "env" : "integration-test", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "os.architecture" : ${content_meta_os_architecture}, + "os.platform" : ${content_meta_os_platform}, + "os.version" : ${content_meta_os_version}, + "runtime-id" : ${content_meta_runtime_id}, + "runtime.name" : ${content_meta_runtime_name}, + "runtime.vendor" : ${content_meta_runtime_vendor}, + "runtime.version" : ${content_meta_runtime_version}, + "span.kind" : "test_module_end", + "test.code_coverage.enabled" : "true", + "test.command" : "mvn -B test", + "test.execution" : "maven-surefire-plugin:test:default-test", + "test.framework" : "junit4", + "test.framework_version" : "4.13.2", + "test.itr.tests_skipping.enabled" : "true", + "test.itr.tests_skipping.type" : "test", + "test.module" : "Maven Smoke Tests Project maven-surefire-plugin default-test", + "test.status" : "pass", + "test.type" : "test", + "test_session.name" : "mvn -B test" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_2}, + "test.itr.tests_skipping.count" : 0 + }, + "name" : "maven.test_module", + "resource" : "Maven Smoke Tests Project maven-surefire-plugin default-test", + "service" : "test-maven-service", + "start" : ${content_start_2}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id} + }, + "type" : "test_module_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_3}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_3}, + "env" : "integration-test", + "execution" : "default-compile", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "os.architecture" : ${content_meta_os_architecture}, + "os.platform" : ${content_meta_os_platform}, + "os.version" : ${content_meta_os_version}, + "plugin" : "maven-compiler-plugin", + "project" : "Maven Smoke Tests Project", + "runtime-id" : ${content_meta_runtime_id}, + "runtime.name" : ${content_meta_runtime_name}, + "runtime.vendor" : ${content_meta_runtime_vendor}, + "runtime.version" : ${content_meta_runtime_version} + }, + "metrics" : { }, + "name" : "Maven_Smoke_Tests_Project_maven_compiler_plugin_default_compile", + "parent_id" : ${content_test_session_id}, + "resource" : "Maven_Smoke_Tests_Project_maven_compiler_plugin_default_compile", + "service" : "test-maven-service", + "span_id" : ${content_span_id}, + "start" : ${content_start_3}, + "trace_id" : ${content_test_session_id} + }, + "type" : "span", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_4}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_4}, + "env" : "integration-test", + "execution" : "default-testCompile", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "os.architecture" : ${content_meta_os_architecture}, + "os.platform" : ${content_meta_os_platform}, + "os.version" : ${content_meta_os_version}, + "plugin" : "maven-compiler-plugin", + "project" : "Maven Smoke Tests Project", + "runtime-id" : ${content_meta_runtime_id}, + "runtime.name" : ${content_meta_runtime_name}, + "runtime.vendor" : ${content_meta_runtime_vendor}, + "runtime.version" : ${content_meta_runtime_version} + }, + "metrics" : { }, + "name" : "Maven_Smoke_Tests_Project_maven_compiler_plugin_default_testCompile", + "parent_id" : ${content_test_session_id}, + "resource" : "Maven_Smoke_Tests_Project_maven_compiler_plugin_default_testCompile", + "service" : "test-maven-service", + "span_id" : ${content_span_id_2}, + "start" : ${content_start_4}, + "trace_id" : ${content_test_session_id} + }, + "type" : "span", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_5}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_5}, + "env" : "integration-test", + "execution" : "default-resources", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "os.architecture" : ${content_meta_os_architecture}, + "os.platform" : ${content_meta_os_platform}, + "os.version" : ${content_meta_os_version}, + "plugin" : "maven-resources-plugin", + "project" : "Maven Smoke Tests Project", + "runtime-id" : ${content_meta_runtime_id}, + "runtime.name" : ${content_meta_runtime_name}, + "runtime.vendor" : ${content_meta_runtime_vendor}, + "runtime.version" : ${content_meta_runtime_version} + }, + "metrics" : { }, + "name" : "Maven_Smoke_Tests_Project_maven_resources_plugin_default_resources", + "parent_id" : ${content_test_session_id}, + "resource" : "Maven_Smoke_Tests_Project_maven_resources_plugin_default_resources", + "service" : "test-maven-service", + "span_id" : ${content_span_id_3}, + "start" : ${content_start_5}, + "trace_id" : ${content_test_session_id} + }, + "type" : "span", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_6}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_6}, + "env" : "integration-test", + "execution" : "default-testResources", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "os.architecture" : ${content_meta_os_architecture}, + "os.platform" : ${content_meta_os_platform}, + "os.version" : ${content_meta_os_version}, + "plugin" : "maven-resources-plugin", + "project" : "Maven Smoke Tests Project", + "runtime-id" : ${content_meta_runtime_id}, + "runtime.name" : ${content_meta_runtime_name}, + "runtime.vendor" : ${content_meta_runtime_vendor}, + "runtime.version" : ${content_meta_runtime_version} + }, + "metrics" : { }, + "name" : "Maven_Smoke_Tests_Project_maven_resources_plugin_default_testResources", + "parent_id" : ${content_test_session_id}, + "resource" : "Maven_Smoke_Tests_Project_maven_resources_plugin_default_testResources", + "service" : "test-maven-service", + "span_id" : ${content_span_id_4}, + "start" : ${content_start_6}, + "trace_id" : ${content_test_session_id} + }, + "type" : "span", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_7}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_7}, + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "ci.workspace_path" : ${content_meta_ci_workspace_path}, + "component" : "junit", + "env" : "integration-test", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "os.architecture" : ${content_meta_os_architecture}, + "os.platform" : ${content_meta_os_platform}, + "os.version" : ${content_meta_os_version}, + "runtime-id" : ${content_meta_runtime_id_2}, + "runtime.name" : ${content_meta_runtime_name}, + "runtime.vendor" : ${content_meta_runtime_vendor}, + "runtime.version" : ${content_meta_runtime_version}, + "span.kind" : "test_suite_end", + "test.framework" : "junit4", + "test.framework_version" : "4.13.2", + "test.module" : "Maven Smoke Tests Project maven-surefire-plugin default-test", + "test.source.file" : "src/test/java/datadog/smoke/TestSucceed.java", + "test.status" : "pass", + "test.suite" : "datadog.smoke.TestSucceed", + "test.type" : "test", + "test_session.name" : "mvn -B test" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_3}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id_2}, + "test.source.end" : 18, + "test.source.start" : 7 + }, + "name" : "junit.test_suite", + "resource" : "datadog.smoke.TestSucceed", + "service" : "test-maven-service", + "start" : ${content_start_7}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id} + }, + "type" : "test_suite_end", + "version" : 1 +}, { + "content" : { + "duration" : ${content_duration_8}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_8}, + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "ci.workspace_path" : ${content_meta_ci_workspace_path}, + "component" : "junit", + "env" : "integration-test", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "os.architecture" : ${content_meta_os_architecture}, + "os.platform" : ${content_meta_os_platform}, + "os.version" : ${content_meta_os_version}, + "runtime-id" : ${content_meta_runtime_id_2}, + "runtime.name" : ${content_meta_runtime_name}, + "runtime.vendor" : ${content_meta_runtime_vendor}, + "runtime.version" : ${content_meta_runtime_version}, + "span.kind" : "test", + "test.framework" : "junit4", + "test.framework_version" : "4.13.2", + "test.is_modified" : "true", + "test.module" : "Maven Smoke Tests Project maven-surefire-plugin default-test", + "test.name" : "test_succeed", + "test.source.file" : "src/test/java/datadog/smoke/TestSucceed.java", + "test.source.method" : "test_succeed()V", + "test.status" : "pass", + "test.suite" : "datadog.smoke.TestSucceed", + "test.type" : "test", + "test_session.name" : "mvn -B test" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_4}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id_2}, + "test.source.end" : 12, + "test.source.start" : 9 + }, + "name" : "junit.test", + "parent_id" : ${content_parent_id}, + "resource" : "datadog.smoke.TestSucceed.test_succeed", + "service" : "test-maven-service", + "span_id" : ${content_span_id_5}, + "start" : ${content_start_8}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id}, + "trace_id" : ${content_trace_id} + }, + "type" : "test", + "version" : 2 +}, { + "content" : { + "duration" : ${content_duration_9}, + "error" : 0, + "meta" : { + "_dd.p.tid" : ${content_meta__dd_p_tid_9}, + "_dd.tracer_host" : ${content_meta__dd_tracer_host}, + "ci.workspace_path" : ${content_meta_ci_workspace_path}, + "component" : "junit", + "env" : "integration-test", + "language" : "jvm", + "library_version" : ${content_meta_library_version}, + "os.architecture" : ${content_meta_os_architecture}, + "os.platform" : ${content_meta_os_platform}, + "os.version" : ${content_meta_os_version}, + "runtime-id" : ${content_meta_runtime_id_2}, + "runtime.name" : ${content_meta_runtime_name}, + "runtime.vendor" : ${content_meta_runtime_vendor}, + "runtime.version" : ${content_meta_runtime_version}, + "span.kind" : "test", + "test.framework" : "junit4", + "test.framework_version" : "4.13.2", + "test.is_modified" : "true", + "test.module" : "Maven Smoke Tests Project maven-surefire-plugin default-test", + "test.name" : "test_to_skip_with_itr", + "test.source.file" : "src/test/java/datadog/smoke/TestSucceed.java", + "test.source.method" : "test_to_skip_with_itr()V", + "test.status" : "pass", + "test.suite" : "datadog.smoke.TestSucceed", + "test.type" : "test", + "test_session.name" : "mvn -B test" + }, + "metrics" : { + "_dd.host.vcpu_count" : ${content_metrics__dd_host_vcpu_count_5}, + "_dd.profiling.enabled" : 0, + "_dd.trace_span_attribute_schema" : 0, + "process_id" : ${content_metrics_process_id_2}, + "test.source.end" : 17, + "test.source.start" : 14 + }, + "name" : "junit.test", + "parent_id" : ${content_parent_id}, + "resource" : "datadog.smoke.TestSucceed.test_to_skip_with_itr", + "service" : "test-maven-service", + "span_id" : ${content_span_id_6}, + "start" : ${content_start_9}, + "test_module_id" : ${content_test_module_id}, + "test_session_id" : ${content_test_session_id}, + "test_suite_id" : ${content_test_suite_id}, + "trace_id" : ${content_trace_id_2} + }, + "type" : "test", + "version" : 2 +} ] \ No newline at end of file diff --git a/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/pom.xml b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/pom.xml new file mode 100644 index 00000000000..48f92df3632 --- /dev/null +++ b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + com.datadog.ci.test + maven-smoke-test + 1.0-SNAPSHOT + Maven Smoke Tests Project + + + 8 + 8 + UTF-8 + + + + + + + false + + central + Central Repository + https://repo.maven.apache.org/maven2 + + + + + + never + + + false + + central + Central Repository + https://repo.maven.apache.org/maven2 + + + + + + junit + junit + 4.13.2 + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0 + + + + + diff --git a/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/src/main/java/datadog/smoke/Calculator.java b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/src/main/java/datadog/smoke/Calculator.java new file mode 100644 index 00000000000..2f4461a279d --- /dev/null +++ b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/src/main/java/datadog/smoke/Calculator.java @@ -0,0 +1,11 @@ +package datadog.smoke; + +public class Calculator { + public static int add(int a, int b) { + return a + b; + } + + public static int subtract(int a, int b) { + return a - b; + } +} diff --git a/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/src/test/java/datadog/smoke/TestSucceed.java b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/src/test/java/datadog/smoke/TestSucceed.java new file mode 100644 index 00000000000..c9baff75d9e --- /dev/null +++ b/dd-smoke-tests/maven/src/test/resources/test_successful_maven_run_impacted_tests/src/test/java/datadog/smoke/TestSucceed.java @@ -0,0 +1,18 @@ +package datadog.smoke; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class TestSucceed { + + @Test + public void test_succeed() { + assertTrue(Calculator.add(2, 2) == 4); + } + + @Test + public void test_to_skip_with_itr() { + assertTrue(Calculator.subtract(3, 2) == 1); + } +} diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/CiVisibilityConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/CiVisibilityConfig.java index 2e8324e4224..c7a2215ae90 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/CiVisibilityConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/CiVisibilityConfig.java @@ -21,6 +21,7 @@ public final class CiVisibilityConfig { public static final String CIVISIBILITY_COMPILER_PLUGIN_VERSION = "civisibility.compiler.plugin.version"; public static final String CIVISIBILITY_DEBUG_PORT = "civisibility.debug.port"; + public static final String CIVISIBILITY_GIT_CLIENT_ENABLED = "civisibility.git.client.enabled"; public static final String CIVISIBILITY_GIT_UPLOAD_ENABLED = "civisibility.git.upload.enabled"; public static final String CIVISIBILITY_GIT_UNSHALLOW_ENABLED = "civisibility.git.unshallow.enabled"; @@ -51,6 +52,8 @@ public final class CiVisibilityConfig { public static final String CIVISIBILITY_RESOURCE_FOLDER_NAMES = "civisibility.resource.folder.names"; public static final String CIVISIBILITY_FLAKY_RETRY_ENABLED = "civisibility.flaky.retry.enabled"; + public static final String CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED = + "civisibility.impacted.tests.detection.enabled"; public static final String CIVISIBILITY_FLAKY_RETRY_ONLY_KNOWN_FLAKES = "civisibility.flaky.retry.only.known.flakes"; public static final String CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED = diff --git a/internal-api/build.gradle b/internal-api/build.gradle index fab211ca323..dff5bb34505 100644 --- a/internal-api/build.gradle +++ b/internal-api/build.gradle @@ -96,6 +96,7 @@ excludedClassesCoverage += [ "datadog.trace.api.civisibility.config.EarlyFlakeDetectionSettings.ExecutionsByDuration", "datadog.trace.api.civisibility.config.TestIdentifier", "datadog.trace.api.civisibility.config.TestMetadata", + "datadog.trace.api.civisibility.config.TestSourceData", "datadog.trace.api.civisibility.coverage.CoveragePercentageBridge", "datadog.trace.api.civisibility.coverage.CoveragePerTestBridge", "datadog.trace.api.civisibility.coverage.NoOpCoverageStore", diff --git a/internal-api/src/main/java/datadog/trace/api/Config.java b/internal-api/src/main/java/datadog/trace/api/Config.java index c3d4b9e0071..99b732df679 100644 --- a/internal-api/src/main/java/datadog/trace/api/Config.java +++ b/internal-api/src/main/java/datadog/trace/api/Config.java @@ -336,6 +336,7 @@ public static String getHostName() { private final String[] ciVisibilityCodeCoverageExcludedPackages; private final List ciVisibilityJacocoGradleSourceSets; private final Integer ciVisibilityDebugPort; + private final boolean ciVisibilityGitClientEnabled; private final boolean ciVisibilityGitUploadEnabled; private final boolean ciVisibilityGitUnshallowEnabled; private final boolean ciVisibilityGitUnshallowDefer; @@ -356,6 +357,7 @@ public static String getHostName() { private final String ciVisibilityInjectedTracerVersion; private final List ciVisibilityResourceFolderNames; private final boolean ciVisibilityFlakyRetryEnabled; + private final boolean ciVisibilityImpactedTestsDetectionEnabled; private final boolean ciVisibilityFlakyRetryOnlyKnownFlakes; private final int ciVisibilityFlakyRetryCount; private final int ciVisibilityTotalFlakyRetryCount; @@ -1443,6 +1445,7 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) ciVisibilityJacocoGradleSourceSets = configProvider.getList(CIVISIBILITY_GRADLE_SOURCE_SETS, Arrays.asList("main", "test")); ciVisibilityDebugPort = configProvider.getInteger(CIVISIBILITY_DEBUG_PORT); + ciVisibilityGitClientEnabled = configProvider.getBoolean(CIVISIBILITY_GIT_CLIENT_ENABLED, true); ciVisibilityGitUploadEnabled = configProvider.getBoolean( CIVISIBILITY_GIT_UPLOAD_ENABLED, DEFAULT_CIVISIBILITY_GIT_UPLOAD_ENABLED); @@ -1492,6 +1495,8 @@ PROFILING_DATADOG_PROFILER_ENABLED, isDatadogProfilerSafeInCurrentEnvironment()) CIVISIBILITY_RESOURCE_FOLDER_NAMES, DEFAULT_CIVISIBILITY_RESOURCE_FOLDER_NAMES); ciVisibilityFlakyRetryEnabled = configProvider.getBoolean(CIVISIBILITY_FLAKY_RETRY_ENABLED, true); + ciVisibilityImpactedTestsDetectionEnabled = + configProvider.getBoolean(CIVISIBILITY_IMPACTED_TESTS_DETECTION_ENABLED, true); ciVisibilityFlakyRetryOnlyKnownFlakes = configProvider.getBoolean(CIVISIBILITY_FLAKY_RETRY_ONLY_KNOWN_FLAKES, false); ciVisibilityEarlyFlakeDetectionEnabled = @@ -2819,6 +2824,10 @@ public Integer getCiVisibilityDebugPort() { return ciVisibilityDebugPort; } + public boolean isCiVisibilityGitClientEnabled() { + return ciVisibilityGitClientEnabled; + } + public boolean isCiVisibilityGitUploadEnabled() { return ciVisibilityGitUploadEnabled; } @@ -2899,6 +2908,10 @@ public boolean isCiVisibilityFlakyRetryEnabled() { return ciVisibilityFlakyRetryEnabled; } + public boolean isCiVisibilityImpactedTestsDetectionEnabled() { + return ciVisibilityImpactedTestsDetectionEnabled; + } + public boolean isCiVisibilityFlakyRetryOnlyKnownFlakes() { return ciVisibilityFlakyRetryOnlyKnownFlakes; } diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/config/Configurations.java b/internal-api/src/main/java/datadog/trace/api/civisibility/config/Configurations.java index 529598236de..c0082e8fd22 100644 --- a/internal-api/src/main/java/datadog/trace/api/civisibility/config/Configurations.java +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/config/Configurations.java @@ -104,4 +104,36 @@ public int hashCode() { testBundle, custom); } + + @Override + public String toString() { + return "Configurations{" + + "osPlatform='" + + osPlatform + + '\'' + + ", osArchitecture='" + + osArchitecture + + '\'' + + ", osVersion='" + + osVersion + + '\'' + + ", runtimeName='" + + runtimeName + + '\'' + + ", runtimeVersion='" + + runtimeVersion + + '\'' + + ", runtimeVendor='" + + runtimeVendor + + '\'' + + ", runtimeArchitecture='" + + runtimeArchitecture + + '\'' + + ", testBundle='" + + testBundle + + '\'' + + ", custom=" + + custom + + '}'; + } } diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/config/TestMetadata.java b/internal-api/src/main/java/datadog/trace/api/civisibility/config/TestMetadata.java index 58d573dd29d..28c7d7799f4 100644 --- a/internal-api/src/main/java/datadog/trace/api/civisibility/config/TestMetadata.java +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/config/TestMetadata.java @@ -2,6 +2,7 @@ import java.util.Objects; +/** Additional test metadata returned by the backend. */ public class TestMetadata { private final boolean missingLineCodeCoverage; diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/config/TestSourceData.java b/internal-api/src/main/java/datadog/trace/api/civisibility/config/TestSourceData.java new file mode 100644 index 00000000000..96fb08545f2 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/config/TestSourceData.java @@ -0,0 +1,84 @@ +package datadog.trace.api.civisibility.config; + +import java.lang.reflect.Method; +import java.util.Objects; +import javax.annotation.Nullable; + +/** Data needed to identify test definition source. */ +public class TestSourceData { + + public static final TestSourceData UNKNOWN = new TestSourceData(null, null); + + @Nullable private final Class testClass; + + @Nullable private final Method testMethod; + + /** + * The name of the test method. May not correspond to {@code testMethod.getName()} (for instance, + * in Spock the testMethod is generated at compile time and has a name that is different from the + * source code method name) + */ + @Nullable private final String testMethodName; + + public TestSourceData(@Nullable Class testClass, @Nullable Method testMethod) { + this(testClass, testMethod, testMethod != null ? testMethod.getName() : null); + } + + public TestSourceData( + @Nullable Class testClass, @Nullable Method testMethod, @Nullable String testMethodName) { + this.testClass = testClass; + this.testMethod = testMethod; + this.testMethodName = testMethodName; + } + + @Nullable + public Class getTestClass() { + return testClass; + } + + @Nullable + public Method getTestMethod() { + return testMethod; + } + + /** + * Returns the name of the test method. May not correspond to {@code testMethod.getName()} (for + * instance, in Spock the testMethod is generated at compile time and has a name that is different + * from the source code method name) + */ + @Nullable + public String getTestMethodName() { + return testMethodName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestSourceData that = (TestSourceData) o; + return Objects.equals(testClass, that.testClass) + && Objects.equals(testMethod, that.testMethod) + && Objects.equals(testMethodName, that.testMethodName); + } + + @Override + public int hashCode() { + return Objects.hash(testClass, testMethod, testMethodName); + } + + @Override + public String toString() { + return "TestSourceData{" + + "testClass=" + + testClass + + ", testMethod=" + + testMethod + + ", testMethodName=" + + testMethodName + + '}'; + } +} diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/events/TestEventsHandler.java b/internal-api/src/main/java/datadog/trace/api/civisibility/events/TestEventsHandler.java index ddfc9292293..49f7d8fda48 100644 --- a/internal-api/src/main/java/datadog/trace/api/civisibility/events/TestEventsHandler.java +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/events/TestEventsHandler.java @@ -3,11 +3,11 @@ import datadog.trace.api.civisibility.DDTest; import datadog.trace.api.civisibility.DDTestSuite; import datadog.trace.api.civisibility.config.TestIdentifier; +import datadog.trace.api.civisibility.config.TestSourceData; import datadog.trace.api.civisibility.retry.TestRetryPolicy; import datadog.trace.api.civisibility.telemetry.tag.TestFrameworkInstrumentation; import datadog.trace.bootstrap.ContextStore; import java.io.Closeable; -import java.lang.reflect.Method; import java.util.Collection; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -41,15 +41,12 @@ void onTestSuiteStart( void onTestStart( SuiteKey suiteDescriptor, TestKey descriptor, - String testSuiteName, String testName, @Nullable String testFramework, @Nullable String testFrameworkVersion, @Nullable String testParameters, @Nullable Collection categories, - @Nullable Class testClass, - @Nullable String testMethodName, - @Nullable Method testMethod, + @Nonnull TestSourceData testSourceData, boolean isRetry, @Nullable Long startTime); @@ -62,15 +59,12 @@ void onTestStart( void onTestIgnore( SuiteKey suiteDescriptor, TestKey testDescriptor, - String testSuiteName, String testName, @Nullable String testFramework, @Nullable String testFrameworkVersion, @Nullable String testParameters, @Nullable Collection categories, - @Nullable Class testClass, - @Nullable String testMethodName, - @Nullable Method testMethod, + @Nonnull TestSourceData testSourceData, @Nullable String reason); boolean skip(TestIdentifier test); @@ -78,7 +72,7 @@ void onTestIgnore( boolean shouldBeSkipped(TestIdentifier test); @Nonnull - TestRetryPolicy retryPolicy(TestIdentifier test); + TestRetryPolicy retryPolicy(TestIdentifier test, TestSourceData source); boolean isNew(TestIdentifier test); diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityCountMetric.java b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityCountMetric.java index 1a3c87085d3..7dcd91d1164 100644 --- a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityCountMetric.java +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityCountMetric.java @@ -17,6 +17,7 @@ import datadog.trace.api.civisibility.telemetry.tag.HasCodeowner; import datadog.trace.api.civisibility.telemetry.tag.IsBenchmark; import datadog.trace.api.civisibility.telemetry.tag.IsHeadless; +import datadog.trace.api.civisibility.telemetry.tag.IsModified; import datadog.trace.api.civisibility.telemetry.tag.IsNew; import datadog.trace.api.civisibility.telemetry.tag.IsRetry; import datadog.trace.api.civisibility.telemetry.tag.IsRum; @@ -62,6 +63,7 @@ public enum CiVisibilityCountMetric { IsBenchmark.class, EarlyFlakeDetectionAbortReason.class, IsNew.class, + IsModified.class, IsRetry.class, IsRum.class, BrowserDriver.class), @@ -129,8 +131,13 @@ public enum CiVisibilityCountMetric { EFD_REQUEST_ERRORS("early_flake_detection.request_errors", ErrorType.class, StatusCode.class), /** The number of requests sent to the flaky tests endpoint */ FLAKY_TESTS_REQUEST("flaky_tests.request", RequestCompressed.class), - /** The number of known tests requests sent to the flaky tests that errored */ - FLAKY_TESTS_REQUEST_ERRORS("flaky_tests.request_errors", ErrorType.class, StatusCode.class); + /** The number of tests requests sent to the flaky tests endpoint that errored */ + FLAKY_TESTS_REQUEST_ERRORS("flaky_tests.request_errors", ErrorType.class, StatusCode.class), + /** The number of requests sent to the changed files endpoint */ + IMPACTED_TESTS_DETECTION_REQUEST("impacted_tests_detection.request", RequestCompressed.class), + /** The number of tests requests sent to the changed files endpoint that errored */ + IMPACTED_TESTS_DETECTION_REQUEST_ERRORS( + "impacted_tests_detection.request_errors", ErrorType.class, StatusCode.class); // need a "holder" class, as accessing static fields from enum constructors is illegal static class IndexHolder { diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityDistributionMetric.java b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityDistributionMetric.java index 7b8ef4a9d15..a9b3e2c0206 100644 --- a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityDistributionMetric.java +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/CiVisibilityDistributionMetric.java @@ -45,12 +45,19 @@ public enum CiVisibilityDistributionMetric { EFD_RESPONSE_BYTES("early_flake_detection.response_bytes", ResponseCompressed.class), /** The number of tests received by the known tests endpoint */ EFD_RESPONSE_TESTS("early_flake_detection.response_tests"), - /* The time it takes to get the response of the flaky tests endpoint request in ms */ + /** The time it takes to get the response of the flaky tests endpoint request in ms */ FLAKY_TESTS_REQUEST_MS("flaky_tests.request_ms"), /** The number of bytes received by the flaky tests endpoint */ FLAKY_TESTS_RESPONSE_BYTES("flaky_tests.response_bytes", ResponseCompressed.class), /** The number of tests received by the flaky tests endpoint */ - FLAKY_TESTS_RESPONSE_TESTS("flaky_tests.response_tests"); + FLAKY_TESTS_RESPONSE_TESTS("flaky_tests.response_tests"), + /** The time it takes to get the response of the changed files endpoint request in ms */ + IMPACTED_TESTS_DETECTION_REQUEST_MS("impacted_tests_detection.request_ms"), + /** The number of bytes received by the changed files endpoint */ + IMPACTED_TESTS_DETECTION_RESPONSE_BYTES( + "impacted_tests_detection.response_bytes", ResponseCompressed.class), + /** The number of files received by the changed files endpoint */ + IMPACTED_TESTS_DETECTION_RESPONSE_FILES("impacted_tests_detection.response_files"); private static final String NAMESPACE = "civisibility"; diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/Command.java b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/Command.java index 9a4660a635e..7b0fa25efd3 100644 --- a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/Command.java +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/Command.java @@ -11,6 +11,7 @@ public enum Command implements TagValue { GET_LOCAL_COMMITS, GET_OBJECTS, PACK_OBJECTS, + DIFF, OTHER; private final String s; diff --git a/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/IsModified.java b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/IsModified.java new file mode 100644 index 00000000000..a128bb28a3c --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/api/civisibility/telemetry/tag/IsModified.java @@ -0,0 +1,13 @@ +package datadog.trace.api.civisibility.telemetry.tag; + +import datadog.trace.api.civisibility.telemetry.TagValue; + +/** Whether the definition of a test was modified. */ +public enum IsModified implements TagValue { + TRUE; + + @Override + public String asString() { + return "is_modified:true"; + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java index d6efb9acda1..0adf63b7742 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/Tags.java @@ -94,6 +94,7 @@ public class Tags { public static final String TEST_EARLY_FLAKE_ABORT_REASON = "test.early_flake.abort_reason"; public static final String TEST_IS_NEW = "test.is_new"; public static final String TEST_IS_RETRY = "test.is_retry"; + public static final String TEST_IS_MODIFIED = "test.is_modified"; public static final String CI_PROVIDER_NAME = "ci.provider.name"; public static final String CI_PIPELINE_ID = "ci.pipeline.id"; @@ -118,6 +119,9 @@ public class Tags { public static final String GIT_COMMIT_MESSAGE = "git.commit.message"; public static final String GIT_BRANCH = "git.branch"; public static final String GIT_TAG = "git.tag"; + public static final String GIT_PULL_REQUEST_BASE_BRANCH = "git.pull_request.base_branch"; + public static final String GIT_PULL_REQUEST_BASE_BRANCH_SHA = "git.pull_request.base_branch_sha"; + public static final String GIT_COMMIT_HEAD_SHA = "git.commit.head_sha"; public static final String RUNTIME_NAME = "runtime.name"; public static final String RUNTIME_VENDOR = "runtime.vendor";