diff --git a/tracer/build/_build/Build.Steps.cs b/tracer/build/_build/Build.Steps.cs index 0aab82bcf408..24e3a120a7b1 100644 --- a/tracer/build/_build/Build.Steps.cs +++ b/tracer/build/_build/Build.Steps.cs @@ -2249,12 +2249,13 @@ string NormalizedPath(AbsolutePath ap) new(@".*System.Threading.ThreadAbortException: Thread was being aborted\.", RegexOptions.Compiled), new(@".*System.InvalidOperationException: Module Samples.Trimming.dll has no HINSTANCE.*", RegexOptions.Compiled), // CI Visibility known errors - new (@".*The Git repository couldn't be automatically extracted.*", RegexOptions.Compiled), - new (@".*DD_GIT_REPOSITORY_URL is set with.*", RegexOptions.Compiled), - new (@".*The Git commit sha couldn't be automatically extracted.*", RegexOptions.Compiled), - new (@".*DD_GIT_COMMIT_SHA must be a full-length git SHA.*", RegexOptions.Compiled), - new (@".*Timeout occurred when flushing spans.*", RegexOptions.Compiled), - new (@".*ITR: .*", RegexOptions.Compiled), + new(@".*The Git repository couldn't be automatically extracted.*", RegexOptions.Compiled), + new(@".*DD_GIT_REPOSITORY_URL is set with.*", RegexOptions.Compiled), + new(@".*The Git commit sha couldn't be automatically extracted.*", RegexOptions.Compiled), + new(@".*DD_GIT_COMMIT_SHA must be a full-length git SHA.*", RegexOptions.Compiled), + new(@".*Timeout occurred when flushing spans.*", RegexOptions.Compiled), + new(@".*TestOptimization: .*", RegexOptions.Compiled), + new(@".*TestOptimizationClient: .*", RegexOptions.Compiled), // This one is annoying but we _think_ due to a dodgy named pipes implementation, so ignoring for now new(@".*An error occurred while sending data to the agent at \\\\\.\\pipe\\trace-.*The operation has timed out.*", RegexOptions.Compiled), new(@".*An error occurred while sending data to the agent at \\\\\.\\pipe\\metrics-.*The operation has timed out.*", RegexOptions.Compiled), @@ -2312,7 +2313,8 @@ string NormalizedPath(AbsolutePath ap) knownPatterns.Add(new(@".*The Git commit sha couldn't be automatically extracted.*", RegexOptions.Compiled)); knownPatterns.Add(new(@".*DD_GIT_COMMIT_SHA must be a full-length git SHA.*", RegexOptions.Compiled)); knownPatterns.Add(new(@".*Timeout occurred when flushing spans.*", RegexOptions.Compiled)); - knownPatterns.Add(new(@".*ITR: .*", RegexOptions.Compiled)); + knownPatterns.Add(new(@".*TestOptimization: .*", RegexOptions.Compiled)); + knownPatterns.Add(new(@".*TestOptimizationClient: .*", RegexOptions.Compiled)); CheckLogsForErrors(knownPatterns, allFilesMustExist: true, minLogLevel: LogLevel.Warning); }); diff --git a/tracer/src/Datadog.Trace.BenchmarkDotNet/BenchmarkMetadata.cs b/tracer/src/Datadog.Trace.BenchmarkDotNet/BenchmarkMetadata.cs index 77b8625daa4c..d8389b1f5803 100644 --- a/tracer/src/Datadog.Trace.BenchmarkDotNet/BenchmarkMetadata.cs +++ b/tracer/src/Datadog.Trace.BenchmarkDotNet/BenchmarkMetadata.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -18,7 +19,7 @@ internal static class BenchmarkMetadata static BenchmarkMetadata() { MetadataByBenchmark = new(); - CIVisibility.InitializeFromManualInstrumentation(); + TestOptimization.Instance.InitializeFromManualInstrumentation(); } public static void GetIds(object key, out TraceId traceId, out ulong spanId) @@ -26,7 +27,7 @@ public static void GetIds(object key, out TraceId traceId, out ulong spanId) var value = MetadataByBenchmark.GetOrAdd(key, @case => new()); if (value.TraceId is null) { - var useAllBits = CIVisibility.Settings.TracerSettings?.TraceId128BitGenerationEnabled ?? true; + var useAllBits = TestOptimization.Instance.Settings.TracerSettings?.TraceId128BitGenerationEnabled ?? true; value.TraceId = RandomIdGenerator.Shared.NextTraceId(useAllBits); value.SpanId = RandomIdGenerator.Shared.NextSpanId(useAllBits); } diff --git a/tracer/src/Datadog.Trace.BenchmarkDotNet/DatadogDiagnoser.cs b/tracer/src/Datadog.Trace.BenchmarkDotNet/DatadogDiagnoser.cs index aec1a82bf3ce..4aee3c5e2f83 100644 --- a/tracer/src/Datadog.Trace.BenchmarkDotNet/DatadogDiagnoser.cs +++ b/tracer/src/Datadog.Trace.BenchmarkDotNet/DatadogDiagnoser.cs @@ -301,7 +301,7 @@ private static void SetEnvironmentVariables(DiagnoserActionParameters parameters environment[profilerHeapEnabled] = "1"; } - environment["DD_PROFILING_AGENTLESS"] = CIVisibility.Settings.Agentless ? "1" : "0"; + environment["DD_PROFILING_AGENTLESS"] = TestOptimization.Instance.Settings.Agentless ? "1" : "0"; environment["DD_PROFILING_UPLOAD_PERIOD"] = "90"; environment["DD_INTERNAL_PROFILING_SAMPLING_RATE"] = "1"; environment["DD_INTERNAL_PROFILING_WALLTIME_THREADS_THRESHOLD"] = "64"; diff --git a/tracer/src/Datadog.Trace.Coverage.collector/AssemblyProcessor.cs b/tracer/src/Datadog.Trace.Coverage.collector/AssemblyProcessor.cs index dd292fd40286..50b8c0de03e6 100644 --- a/tracer/src/Datadog.Trace.Coverage.collector/AssemblyProcessor.cs +++ b/tracer/src/Datadog.Trace.Coverage.collector/AssemblyProcessor.cs @@ -64,9 +64,9 @@ public AssemblyProcessor(string filePath, CoverageSettings settings, ICollectorL _settings = settings; _logger = logger ?? new ConsoleCollectorLogger(); _assemblyFilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); - _enableJitOptimizations = settings.CIVisibility.CodeCoverageEnableJitOptimizations; + _enableJitOptimizations = settings.TestOptimization.CodeCoverageEnableJitOptimizations; _coverageMode = CoverageMode.LineExecution; - if (settings.CIVisibility.CodeCoverageMode is { Length: > 0 } strCodeCoverageMode) + if (settings.TestOptimization.CodeCoverageMode is { Length: > 0 } strCodeCoverageMode) { if (Enum.TryParse(strCodeCoverageMode, ignoreCase: true, out var coverageMode)) { @@ -169,7 +169,7 @@ public unsafe void Process() { _logger.Debug($"Assembly: {FilePath} is signed."); - var snkFilePath = _settings.CIVisibility.CodeCoverageSnkFilePath; + var snkFilePath = _settings.TestOptimization.CodeCoverageSnkFilePath; _logger.Debug($"Assembly: {FilePath} loading .snk file: {snkFilePath}."); if (!string.IsNullOrWhiteSpace(snkFilePath) && File.Exists(snkFilePath)) { diff --git a/tracer/src/Datadog.Trace.Coverage.collector/CoverageSettings.cs b/tracer/src/Datadog.Trace.Coverage.collector/CoverageSettings.cs index 84116662ae44..d68ce18cce8f 100644 --- a/tracer/src/Datadog.Trace.Coverage.collector/CoverageSettings.cs +++ b/tracer/src/Datadog.Trace.Coverage.collector/CoverageSettings.cs @@ -15,10 +15,10 @@ namespace Datadog.Trace.Coverage.Collector; /// internal class CoverageSettings { - public CoverageSettings(XmlElement? configurationElement, string? tracerHome, CIVisibilitySettings? ciVisibilitySettings = null) + public CoverageSettings(XmlElement? configurationElement, string? tracerHome, TestOptimizationSettings? testOptimizationSettings = null) { TracerHome = tracerHome; - CIVisibility = ciVisibilitySettings ?? CIVisibilitySettings.FromDefaultSources(); + TestOptimization = testOptimizationSettings ?? TestOptimizationSettings.FromDefaultSources(); if (configurationElement is not null) { @@ -43,7 +43,7 @@ public CoverageSettings(XmlElement? configurationElement, string? tracerHome, CI } public CoverageSettings(XmlElement? configurationElement) - : this(configurationElement, Util.EnvironmentHelpers.GetEnvironmentVariable("DD_DOTNET_TRACER_HOME"), CIVisibilitySettings.FromDefaultSources()) + : this(configurationElement, Util.EnvironmentHelpers.GetEnvironmentVariable("DD_DOTNET_TRACER_HOME"), TestOptimizationSettings.FromDefaultSources()) { } @@ -70,7 +70,7 @@ public CoverageSettings(XmlElement? configurationElement) /// /// Gets the CI Visibility settings /// - public CIVisibilitySettings CIVisibility { get; } + public TestOptimizationSettings TestOptimization { get; } private static void GetStringArrayFromXmlElement(XmlElement? xmlElement, ref IReadOnlyList elements, Func? validator = null) { diff --git a/tracer/src/Datadog.Trace.MSBuild/DatadogLogger.cs b/tracer/src/Datadog.Trace.MSBuild/DatadogLogger.cs index ef7ff737b07e..379132e75def 100644 --- a/tracer/src/Datadog.Trace.MSBuild/DatadogLogger.cs +++ b/tracer/src/Datadog.Trace.MSBuild/DatadogLogger.cs @@ -47,7 +47,7 @@ static DatadogLogger() // . } - CIVisibility.Initialize(); + TestOptimization.Instance.Initialize(); } /// diff --git a/tracer/src/Datadog.Trace.Tools.Runner/CiUtils.cs b/tracer/src/Datadog.Trace.Tools.Runner/CiUtils.cs index 841eaf4ff386..b41841242520 100644 --- a/tracer/src/Datadog.Trace.Tools.Runner/CiUtils.cs +++ b/tracer/src/Datadog.Trace.Tools.Runner/CiUtils.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -15,6 +16,7 @@ using Datadog.Trace.Ci; using Datadog.Trace.Ci.CiEnvironment; using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Ci.Net; using Datadog.Trace.Logging; using Datadog.Trace.Util; using Spectre.Console; @@ -37,30 +39,33 @@ public static async Task InitializeCiCommandsAsync( // Define the arguments var lstArguments = new List(args.Length > 1 ? args.Skip(1) : []); - // CI Visibility mode is enabled. - var ciVisibilitySettings = CIVisibility.Settings; + // Test optimization instance. + var testOptimization = TestOptimization.Instance; + + // Test optimization mode is enabled. + var testOptimizationSettings = testOptimization.Settings; // Get profiler environment variables if (!RunHelper.TryGetEnvironmentVariables( applicationContext, context, commonTracerSettings, - new Utils.CIVisibilityOptions(ciVisibilitySettings.InstallDatadogTraceInGac, true, reducePathLength), + new Utils.CIVisibilityOptions(testOptimizationSettings.InstallDatadogTraceInGac, true, reducePathLength), out var profilerEnvironmentVariables)) { context.ExitCode = 1; return new InitResults(false, lstArguments, null, false, false, Task.CompletedTask); } - // Reload the CI Visibility settings (in case they were changed by the environment variables using the `--set-env` option) - ciVisibilitySettings = CIVisibilitySettings.FromDefaultSources(); + // Reload the Test optimization settings (in case they were changed by the environment variables using the `--set-env` option) + testOptimizationSettings = TestOptimizationSettings.FromDefaultSources(); - // We force CIVisibility mode on child process + // We force Test optimization mode on child process profilerEnvironmentVariables[Configuration.ConfigurationKeys.CIVisibility.Enabled] = "1"; // We check the settings and merge with the command settings options - var agentless = ciVisibilitySettings.Agentless; - var apiKey = ciVisibilitySettings.ApiKey; + var agentless = testOptimizationSettings.Agentless; + var apiKey = testOptimizationSettings.ApiKey; var customApiKey = apiKeyOption?.GetValue(context); if (!string.IsNullOrEmpty(customApiKey)) @@ -103,7 +108,7 @@ public static async Task InitializeCiCommandsAsync( var uploadRepositoryChangesTask = Task.CompletedTask; // Set Agentless configuration from the command line options - ciVisibilitySettings.SetAgentlessConfiguration(agentless, apiKey, ciVisibilitySettings.AgentlessUrl); + testOptimizationSettings.SetAgentlessConfiguration(agentless, apiKey, testOptimizationSettings.AgentlessUrl); if (!string.IsNullOrEmpty(agentUrl)) { @@ -111,17 +116,17 @@ public static async Task InitializeCiCommandsAsync( } // Initialize flags to enable code coverage and test skipping - var codeCoverageEnabled = ciVisibilitySettings.CodeCoverageEnabled == true || ciVisibilitySettings.TestsSkippingEnabled == true; - var testSkippingEnabled = ciVisibilitySettings.TestsSkippingEnabled == true; - var earlyFlakeDetectionEnabled = ciVisibilitySettings.EarlyFlakeDetectionEnabled == true; - var flakyRetryEnabled = ciVisibilitySettings.FlakyRetryEnabled == true; + var codeCoverageEnabled = testOptimizationSettings.CodeCoverageEnabled == true || testOptimizationSettings.TestsSkippingEnabled == true; + var testSkippingEnabled = testOptimizationSettings.TestsSkippingEnabled == true; + var earlyFlakeDetectionEnabled = testOptimizationSettings.EarlyFlakeDetectionEnabled == true; + var flakyRetryEnabled = testOptimizationSettings.FlakyRetryEnabled == true; var hasEvpProxy = !string.IsNullOrEmpty(agentConfiguration?.EventPlatformProxyEndpoint); if (agentless || hasEvpProxy) { // Initialize CI Visibility with the current settings Log.Debug("RunCiCommand: Initialize CI Visibility for the runner."); - CIVisibility.InitializeFromRunner(ciVisibilitySettings, discoveryService, hasEvpProxy); + testOptimization.InitializeFromRunner(testOptimizationSettings, discoveryService, hasEvpProxy); // Upload git metadata by default (unless is disabled explicitly) or if ITR is enabled (required). Log.Debug("RunCiCommand: Uploading repository changes."); @@ -135,15 +140,15 @@ public static async Task InitializeCiCommandsAsync( ciValues.GitSearchFolder = null; } - var lazyItrClient = new Lazy(() => new(ciValues.WorkspacePath, ciVisibilitySettings)); - if (ciVisibilitySettings.GitUploadEnabled != false || ciVisibilitySettings.IntelligentTestRunnerEnabled) + var client = TestOptimizationClient.Create(ciValues.WorkspacePath, testOptimizationSettings); + if (testOptimizationSettings.GitUploadEnabled != false || testOptimizationSettings.IntelligentTestRunnerEnabled) { // If we are in git upload only then we can defer the await until the child command exits. async Task UploadRepositoryChangesAsync() { try { - await lazyItrClient.Value.UploadRepositoryChangesAsync().ConfigureAwait(false); + await client.UploadRepositoryChangesAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -164,7 +169,7 @@ async Task UploadRepositoryChangesAsync() // EVP proxy is enabled. // By setting the environment variables we avoid the usage of the DiscoveryService in each child process // to ask for EVP proxy support. - var evpProxyMode = CIVisibility.EventPlatformProxySupportFromEndpointUrl(agentConfiguration?.EventPlatformProxyEndpoint).ToString(); + var evpProxyMode = testOptimization.TracerManagement?.EventPlatformProxySupportFromEndpointUrl(agentConfiguration?.EventPlatformProxyEndpoint).ToString(); profilerEnvironmentVariables[Configuration.ConfigurationKeys.CIVisibility.ForceAgentsEvpProxy] = evpProxyMode; EnvironmentHelpers.SetEnvironmentVariable(Configuration.ConfigurationKeys.CIVisibility.ForceAgentsEvpProxy, evpProxyMode); Log.Debug("RunCiCommand: EVP proxy was detected: {Mode}", evpProxyMode); @@ -172,24 +177,24 @@ async Task UploadRepositoryChangesAsync() // If we have an api key, and the code coverage or the tests skippable environment variables // are not set when the intelligent test runner is enabled, we query the settings api to check if it should enable coverage or not. - if (!ciVisibilitySettings.IntelligentTestRunnerEnabled) + if (!testOptimizationSettings.IntelligentTestRunnerEnabled) { Log.Debug("RunCiCommand: Intelligent test runner is disabled, call to configuration api skipped."); } // If we still don't know if we have to enable code coverage or test skipping, then let's request the configuration API - if (ciVisibilitySettings.IntelligentTestRunnerEnabled - && (ciVisibilitySettings.CodeCoverageEnabled == null || - ciVisibilitySettings.TestsSkippingEnabled == null || - ciVisibilitySettings.EarlyFlakeDetectionEnabled == null || - ciVisibilitySettings.FlakyRetryEnabled == null)) + if (testOptimizationSettings.IntelligentTestRunnerEnabled + && (testOptimizationSettings.CodeCoverageEnabled == null || + testOptimizationSettings.TestsSkippingEnabled == null || + testOptimizationSettings.EarlyFlakeDetectionEnabled == null || + testOptimizationSettings.FlakyRetryEnabled == null)) { try { - CIVisibility.Log.Debug("RunCiCommand: Calling configuration api..."); + testOptimization.Log.Debug("RunCiCommand: Calling configuration api..."); // we skip the framework info because we are interested in the target projects info not the runner one. - var itrSettings = await lazyItrClient.Value.GetSettingsAsync(skipFrameworkInfo: true).ConfigureAwait(false); + var itrSettings = await client.GetSettingsAsync(skipFrameworkInfo: true).ConfigureAwait(false); // we check if the backend require the git metadata first if (itrSettings.RequireGit == true) @@ -198,7 +203,7 @@ async Task UploadRepositoryChangesAsync() await uploadRepositoryChangesTask.ConfigureAwait(false); Log.Debug("RunCiCommand: calling the configuration api again."); - itrSettings = await lazyItrClient.Value.GetSettingsAsync(skipFrameworkInfo: true).ConfigureAwait(false); + itrSettings = await client.GetSettingsAsync(skipFrameworkInfo: true).ConfigureAwait(false); } codeCoverageEnabled = codeCoverageEnabled || itrSettings.CodeCoverage == true || itrSettings.TestsSkipping == true; @@ -208,7 +213,7 @@ async Task UploadRepositoryChangesAsync() } catch (Exception ex) { - CIVisibility.Log.Warning(ex, "Error getting ITR settings from configuration api"); + testOptimization.Log.Warning(ex, "Error getting ITR settings from configuration api"); } } } @@ -217,9 +222,9 @@ async Task UploadRepositoryChangesAsync() Log.Debug("RunCiCommand: TestSkippingEnabled = {Value}", testSkippingEnabled); Log.Debug("RunCiCommand: EarlyFlakeDetectionEnabled = {Value}", earlyFlakeDetectionEnabled); Log.Debug("RunCiCommand: FlakyRetryEnabled = {Value}", flakyRetryEnabled); - ciVisibilitySettings.SetCodeCoverageEnabled(codeCoverageEnabled); - ciVisibilitySettings.SetEarlyFlakeDetectionEnabled(earlyFlakeDetectionEnabled); - ciVisibilitySettings.SetFlakyRetryEnabled(flakyRetryEnabled); + testOptimizationSettings.SetCodeCoverageEnabled(codeCoverageEnabled); + testOptimizationSettings.SetEarlyFlakeDetectionEnabled(earlyFlakeDetectionEnabled); + testOptimizationSettings.SetFlakyRetryEnabled(flakyRetryEnabled); profilerEnvironmentVariables[Configuration.ConfigurationKeys.CIVisibility.CodeCoverage] = codeCoverageEnabled ? "1" : "0"; profilerEnvironmentVariables[Configuration.ConfigurationKeys.CIVisibility.EarlyFlakeDetectionEnabled] = earlyFlakeDetectionEnabled ? "1" : "0"; profilerEnvironmentVariables[Configuration.ConfigurationKeys.CIVisibility.FlakyRetryEnabled] = flakyRetryEnabled ? "1" : "0"; @@ -228,7 +233,7 @@ async Task UploadRepositoryChangesAsync() { // If test skipping is disabled we set this to the child process so we avoid to query the settings api again. // If is not disabled we need to query the backend again in the child process with more runtime info. - ciVisibilitySettings.SetTestsSkippingEnabled(testSkippingEnabled); + testOptimizationSettings.SetTestsSkippingEnabled(testSkippingEnabled); profilerEnvironmentVariables[Configuration.ConfigurationKeys.CIVisibility.TestsSkippingEnabled] = "0"; } diff --git a/tracer/src/Datadog.Trace.Tools.Runner/Crank/Importer.cs b/tracer/src/Datadog.Trace.Tools.Runner/Crank/Importer.cs index 4bec4cd3b39c..5d8f16e8e84b 100644 --- a/tracer/src/Datadog.Trace.Tools.Runner/Crank/Importer.cs +++ b/tracer/src/Datadog.Trace.Tools.Runner/Crank/Importer.cs @@ -77,11 +77,12 @@ public static int Process(string jsonFilePath) string jsonContent = File.ReadAllText(jsonFilePath); var result = JsonConvert.DeserializeObject(jsonContent); + var testOptimization = TestOptimization.Instance; if (result?.JobResults?.Jobs?.Count > 0) { var fileName = Path.GetFileName(jsonFilePath); - CIVisibility.Initialize(); + testOptimization.Initialize(); Tracer tracer = Tracer.Instance; foreach (var jobItem in result.JobResults.Jobs) @@ -247,7 +248,7 @@ public static int Process(string jsonFilePath) // Ensure all the spans gets flushed before we report the success. // In some cases the process finishes without sending the traces in the buffer. - CIVisibility.Flush(); + testOptimization.Flush(); } } catch (Exception ex) diff --git a/tracer/src/Datadog.Trace.Tools.Runner/LegacyCommand.cs b/tracer/src/Datadog.Trace.Tools.Runner/LegacyCommand.cs index 79acf7517fd5..32fc3e00f491 100644 --- a/tracer/src/Datadog.Trace.Tools.Runner/LegacyCommand.cs +++ b/tracer/src/Datadog.Trace.Tools.Runner/LegacyCommand.cs @@ -99,15 +99,15 @@ private void Execute(InvocationContext context) string cmdLine = string.Join(' ', args); if (!string.IsNullOrWhiteSpace(cmdLine)) { - // CI Visibility mode is enabled. + // Test optimization mode is enabled. // If the agentless feature flag is enabled, we check for ApiKey // If the agentless feature flag is disabled, we check if we have connection to the agent before running the process. if (enableCIVisibilityMode) { - var ciVisibilitySettings = Ci.Configuration.CIVisibilitySettings.FromDefaultSources(); - if (ciVisibilitySettings.Agentless) + var testOptimizationSettings = Ci.Configuration.TestOptimizationSettings.FromDefaultSources(); + if (testOptimizationSettings.Agentless) { - if (string.IsNullOrWhiteSpace(ciVisibilitySettings.ApiKey)) + if (string.IsNullOrWhiteSpace(testOptimizationSettings.ApiKey)) { Utils.WriteError("An API key is required in Agentless mode."); context.ExitCode = 1; diff --git a/tracer/src/Datadog.Trace.Tools.Runner/Utils.cs b/tracer/src/Datadog.Trace.Tools.Runner/Utils.cs index 657321d876ed..f75cfdc512fe 100644 --- a/tracer/src/Datadog.Trace.Tools.Runner/Utils.cs +++ b/tracer/src/Datadog.Trace.Tools.Runner/Utils.cs @@ -161,7 +161,7 @@ public static void SetCommonTracerSettingsToCurrentProcess(InvocationContext con { var environment = options.Environment.GetValue(context); - // Settings back DD_ENV to use it in the current process (eg for CIVisibility's TestSession) + // Settings back DD_ENV to use it in the current process (eg for TestOptimization's TestSession) if (!string.IsNullOrWhiteSpace(environment)) { EnvironmentHelpers.SetEnvironmentVariable(ConfigurationKeys.Environment, environment); @@ -169,7 +169,7 @@ public static void SetCommonTracerSettingsToCurrentProcess(InvocationContext con var service = options.Service.GetValue(context); - // Settings back DD_SERVICE to use it in the current process (eg for CIVisibility's TestSession) + // Settings back DD_SERVICE to use it in the current process (eg for TestOptimization's TestSession) if (!string.IsNullOrWhiteSpace(service)) { EnvironmentHelpers.SetEnvironmentVariable(ConfigurationKeys.ServiceName, service); @@ -177,7 +177,7 @@ public static void SetCommonTracerSettingsToCurrentProcess(InvocationContext con var version = options.Version.GetValue(context); - // Settings back DD_VERSION to use it in the current process (eg for CIVisibility's TestSession) + // Settings back DD_VERSION to use it in the current process (eg for TestOptimization's TestSession) if (!string.IsNullOrWhiteSpace(version)) { EnvironmentHelpers.SetEnvironmentVariable(ConfigurationKeys.ServiceVersion, version); @@ -185,7 +185,7 @@ public static void SetCommonTracerSettingsToCurrentProcess(InvocationContext con var agentUrl = options.AgentUrl.GetValue(context); - // Settings back DD_TRACE_AGENT_URL to use it in the current process (eg for CIVisibility's TestSession) + // Settings back DD_TRACE_AGENT_URL to use it in the current process (eg for TestOptimization's TestSession) if (!string.IsNullOrWhiteSpace(agentUrl)) { EnvironmentHelpers.SetEnvironmentVariable(ConfigurationKeys.AgentUri, agentUrl); diff --git a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs index 2844781f9fa0..7b318b31cefa 100644 --- a/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs +++ b/tracer/src/Datadog.Trace/Agent/MessagePack/SpanMessagePackFormatter.cs @@ -248,9 +248,9 @@ private int WriteSpanLink(ref byte[] bytes, int offset, in SpanModel spanModel) var traceFlags = samplingPriority switch { - null => 0u, // not set + null => 0u, // not set > 0 => 1u + (1u << 31), // keep - <= 0 => 1u << 31, // drop + <= 0 => 1u << 31, // drop }; var len = 3; @@ -387,7 +387,8 @@ private int WriteTags(ref byte[] bytes, int offset, in SpanModel model, ITagProc } // add "runtime-id" tag to service-entry (aka top-level) spans - if (span.IsTopLevel && (!Ci.CIVisibility.IsRunning || !Ci.CIVisibility.Settings.Agentless)) + var testOptimization = Ci.TestOptimization.Instance; + if (span.IsTopLevel && (!testOptimization.IsRunning || !testOptimization.Settings.Agentless)) { count++; offset += MessagePackBinary.WriteStringBytes(ref bytes, offset, _runtimeIdNameBytes); @@ -680,7 +681,8 @@ private int WriteMetrics(ref byte[] bytes, int offset, in SpanModel model, ITagP offset += MessagePackBinary.WriteDouble(ref bytes, offset, samplingPriority); } - if (span.IsTopLevel && (!Ci.CIVisibility.IsRunning || !Ci.CIVisibility.Settings.Agentless)) + var testOptimization = Ci.TestOptimization.Instance; + if (span.IsTopLevel && (!testOptimization.IsRunning || !testOptimization.Settings.Agentless)) { count++; WriteMetric(ref bytes, ref offset, Trace.Metrics.TopLevelSpan, 1.0, tagProcessors); diff --git a/tracer/src/Datadog.Trace/Ci/Agent/CIVisibilityProtocolWriter.cs b/tracer/src/Datadog.Trace/Ci/Agent/CIVisibilityProtocolWriter.cs index 160f2f91abea..e060ee7a5d65 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/CIVisibilityProtocolWriter.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/CIVisibilityProtocolWriter.cs @@ -70,7 +70,7 @@ internal sealed class CIVisibilityProtocolWriter : IEventWriter private readonly int _batchInterval; public CIVisibilityProtocolWriter( - CIVisibilitySettings settings, + TestOptimizationSettings settings, ICIVisibilityProtocolWriterSender sender, IFormatterResolver? formatterResolver = null, int? concurrency = null, diff --git a/tracer/src/Datadog.Trace/Ci/Agent/CIWriterHttpSender.cs b/tracer/src/Datadog.Trace/Ci/Agent/CIWriterHttpSender.cs index b48a2267a841..ef0297bf74dd 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/CIWriterHttpSender.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/CIWriterHttpSender.cs @@ -112,7 +112,7 @@ private async Task SendPayloadAsync(EventPlatformPayload payload, Func + #nullable enable using System; @@ -44,9 +45,10 @@ public CIEventMessagePackFormatter(TracerSettings tracerSettings) _environmentValueBytes = StringEncoding.UTF8.GetBytes(tracerSettings.Environment); } - if (!string.IsNullOrWhiteSpace(Ci.CIVisibility.Settings.TestSessionName)) + var testOptimization = TestOptimization.Instance; + if (!string.IsNullOrWhiteSpace(testOptimization.Settings.TestSessionName)) { - _testSessionNameValueBytes = StringEncoding.UTF8.GetBytes(Ci.CIVisibility.Settings.TestSessionName); + _testSessionNameValueBytes = StringEncoding.UTF8.GetBytes(testOptimization.Settings.TestSessionName); } _envelopBytes = GetEnvelopeArraySegment(); diff --git a/tracer/src/Datadog.Trace/Ci/Agent/MessagePack/CIFormatterResolver.cs b/tracer/src/Datadog.Trace/Ci/Agent/MessagePack/CIFormatterResolver.cs index e3bbf5d9b431..3a64b08c88ae 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/MessagePack/CIFormatterResolver.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/MessagePack/CIFormatterResolver.cs @@ -40,7 +40,7 @@ internal class CIFormatterResolver : IFormatterResolver private CIFormatterResolver() { _spanFormatter = SpanMessagePackFormatter.Instance; - _eventsPayloadFormatter = new CIEventMessagePackFormatter(CIVisibility.Settings.TracerSettings); + _eventsPayloadFormatter = new CIEventMessagePackFormatter(TestOptimization.Instance.Settings.TracerSettings); _eventFormatter = new IEventMessagePackFormatter(); _ciVisibilityEventFormatter = new CIVisibilityEventMessagePackFormatter(); _coveragePayloadFormatter = new CoveragePayloadMessagePackFormatter(); diff --git a/tracer/src/Datadog.Trace/Ci/Agent/MessagePack/SpanMessagePackFormatter.cs b/tracer/src/Datadog.Trace/Ci/Agent/MessagePack/SpanMessagePackFormatter.cs index 502e1474c300..8a95444a6509 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/MessagePack/SpanMessagePackFormatter.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/MessagePack/SpanMessagePackFormatter.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -119,7 +120,7 @@ public int Serialize(ref byte[] bytes, int offset, Span value, IFormatterResolve isSpan = true; } - var correlationId = value.Type is SpanTypes.Test or SpanTypes.Browser ? CIVisibility.GetSkippableTestsCorrelationId() : null; + var correlationId = value.Type is SpanTypes.Test or SpanTypes.Browser ? TestOptimization.Instance.SkippableFeature?.GetCorrelationId() : null; if (correlationId is not null) { len++; diff --git a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CICodeCoveragePayload.cs b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CICodeCoveragePayload.cs index 0f20029a0580..51df8fef3c2d 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CICodeCoveragePayload.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CICodeCoveragePayload.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -23,7 +24,7 @@ internal class CICodeCoveragePayload : MultipartPayload private readonly IFormatterResolver _formatterResolver; private readonly Stopwatch _serializationWatch; - public CICodeCoveragePayload(CIVisibilitySettings settings, int maxItemsPerPayload = DefaultMaxItemsPerPayload, int maxBytesPerPayload = DefaultMaxBytesPerPayload, IFormatterResolver? formatterResolver = null) + public CICodeCoveragePayload(TestOptimizationSettings settings, int maxItemsPerPayload = DefaultMaxItemsPerPayload, int maxBytesPerPayload = DefaultMaxBytesPerPayload, IFormatterResolver? formatterResolver = null) : base(settings, maxItemsPerPayload, maxBytesPerPayload, formatterResolver) { _formatterResolver = formatterResolver ?? CIFormatterResolver.Instance; @@ -53,7 +54,7 @@ protected override MultipartFormItem CreateMultipartFormItem(EventsBuffer("CICodeCoveragePayload: Serialized {Count} test code coverage as a single multipart item with {Size} bytes.", eventsBuffer.Count, eventInBytes.Length); + TestOptimization.Instance.Log.Debug("CICodeCoveragePayload: Serialized {Count} test code coverage as a single multipart item with {Size} bytes.", eventsBuffer.Count, eventInBytes.Length); return new MultipartFormItem($"coverage{index}", MimeTypes.MsgPack, $"filecoverage{index}.msgpack", new ArraySegment(eventInBytes)); } diff --git a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CITestCyclePayload.cs b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CITestCyclePayload.cs index 251277b1506d..a49301485bb5 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CITestCyclePayload.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CITestCyclePayload.cs @@ -13,7 +13,7 @@ namespace Datadog.Trace.Ci.Agent.Payloads; internal class CITestCyclePayload : CIVisibilityProtocolPayload { - public CITestCyclePayload(CIVisibilitySettings settings, IFormatterResolver? formatterResolver = null) + public CITestCyclePayload(TestOptimizationSettings settings, IFormatterResolver? formatterResolver = null) : base(settings, formatterResolver) { } diff --git a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CIVisibilityProtocolPayload.cs b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CIVisibilityProtocolPayload.cs index ca207f5b7d76..4fbae0234cb1 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CIVisibilityProtocolPayload.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/CIVisibilityProtocolPayload.cs @@ -19,7 +19,7 @@ internal abstract class CIVisibilityProtocolPayload : EventPlatformPayload private readonly IFormatterResolver _formatterResolver; private readonly Stopwatch _serializationWatch; - public CIVisibilityProtocolPayload(CIVisibilitySettings settings, IFormatterResolver? formatterResolver = null) + public CIVisibilityProtocolPayload(TestOptimizationSettings settings, IFormatterResolver? formatterResolver = null) : base(settings) { _formatterResolver = formatterResolver ?? CIFormatterResolver.Instance; diff --git a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/EventPlatformPayload.cs b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/EventPlatformPayload.cs index 4ecd079eafef..8cca067a17fe 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/EventPlatformPayload.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/EventPlatformPayload.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -17,11 +18,11 @@ namespace Datadog.Trace.Ci.Agent.Payloads; /// internal abstract class EventPlatformPayload { - private readonly CIVisibilitySettings _settings; + private readonly TestOptimizationSettings _settings; private bool? _useGzip; private Uri? _url; - protected EventPlatformPayload(CIVisibilitySettings settings) + protected EventPlatformPayload(TestOptimizationSettings settings) { if (settings is null) { @@ -162,12 +163,13 @@ private void EnsureUrl() break; } - if (CIVisibility.EventPlatformProxySupport == EventPlatformProxySupport.V4) + var tracerManagement = TestOptimization.Instance.TracerManagement; + if (tracerManagement?.EventPlatformProxySupport == EventPlatformProxySupport.V4) { builder.Path = $"/evp_proxy/v4/{EventPlatformPath}"; _useGzip = true; } - else if (CIVisibility.EventPlatformProxySupport == EventPlatformProxySupport.V2) + else if (tracerManagement?.EventPlatformProxySupport == EventPlatformProxySupport.V2) { builder.Path = $"/evp_proxy/v2/{EventPlatformPath}"; _useGzip = false; diff --git a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/MultipartPayload.cs b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/MultipartPayload.cs index c305d1b1876e..1594cb466783 100644 --- a/tracer/src/Datadog.Trace/Ci/Agent/Payloads/MultipartPayload.cs +++ b/tracer/src/Datadog.Trace/Ci/Agent/Payloads/MultipartPayload.cs @@ -25,7 +25,7 @@ internal abstract class MultipartPayload : EventPlatformPayload private readonly int _maxItemsPerPayload; private readonly int _maxBytesPerPayload; - public MultipartPayload(CIVisibilitySettings settings, int maxItemsPerPayload = DefaultMaxItemsPerPayload, int maxBytesPerPayload = DefaultMaxBytesPerPayload, IFormatterResolver? formatterResolver = null) + public MultipartPayload(TestOptimizationSettings settings, int maxItemsPerPayload = DefaultMaxItemsPerPayload, int maxBytesPerPayload = DefaultMaxBytesPerPayload, IFormatterResolver? formatterResolver = null) : base(settings) { if (maxBytesPerPayload < HeaderSize) diff --git a/tracer/src/Datadog.Trace/Ci/CIVisibility.cs b/tracer/src/Datadog.Trace/Ci/CIVisibility.cs deleted file mode 100644 index a590cadccf92..000000000000 --- a/tracer/src/Datadog.Trace/Ci/CIVisibility.cs +++ /dev/null @@ -1,872 +0,0 @@ -// -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. -// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. -// -#nullable enable - -using System; -using System.Collections.Generic; -using System.Net; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Datadog.Trace.Agent; -using Datadog.Trace.Agent.DiscoveryService; -using Datadog.Trace.Agent.Transports; -using Datadog.Trace.Ci.CiEnvironment; -using Datadog.Trace.Ci.Configuration; -using Datadog.Trace.Configuration; -using Datadog.Trace.HttpOverStreams; -using Datadog.Trace.Logging; -using Datadog.Trace.Pdb; -using Datadog.Trace.PlatformHelpers; -using Datadog.Trace.Util; - -namespace Datadog.Trace.Ci -{ - internal class CIVisibility - { - private static Lazy _enabledLazy = new(InternalEnabled, true); - private static CIVisibilitySettings? _settings; - private static int _firstInitialization = 1; - private static Task? _skippableTestsTask; - private static string? _skippableTestsCorrelationId; - private static Dictionary>>? _skippableTestsBySuiteAndName; - private static string? _osVersion; - - internal static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(CIVisibility)); - - public static bool Enabled => _enabledLazy.Value; - - public static bool IsRunning - { - get - { - // We try first the fast path, if the value is 0 we are running, so we can avoid the Interlocked operation. - if (_firstInitialization == 0) - { - return true; - } - - // If the value is not 0, maybe the value hasn't been updated yet, so we use the Interlocked operation to ensure the value is correct. - return Interlocked.CompareExchange(ref _firstInitialization, 0, 0) == 0; - } - } - - public static CIVisibilitySettings Settings - { - get => LazyInitializer.EnsureInitialized(ref _settings, () => CIVisibilitySettings.FromDefaultSources())!; - private set => _settings = value; - } - - public static EventPlatformProxySupport EventPlatformProxySupport { get; private set; } = EventPlatformProxySupport.None; - - public static CITracerManager? Manager - { - get - { - if (Tracer.Instance.TracerManager is CITracerManager cITracerManager) - { - return cITracerManager; - } - - return null; - } - } - - // Unlocked tracer manager is used in tests so tracer instance can be changed with a new configuration. - internal static bool UseLockedTracerManager { get; set; } = true; - - internal static IntelligentTestRunnerClient.EarlyFlakeDetectionSettingsResponse EarlyFlakeDetectionSettings { get; private set; } - - internal static IntelligentTestRunnerClient.EarlyFlakeDetectionResponse? EarlyFlakeDetectionResponse { get; private set; } - - internal static IntelligentTestRunnerClient.ImpactedTestsDetectionResponse? ImpactedTestsDetectionResponse { get; private set; } - - public static void Initialize() - { - if (Interlocked.Exchange(ref _firstInitialization, 0) != 1) - { - // Initialize() or InitializeFromRunner() or InitializeFromManualInstrumentation() was already called before - return; - } - - Log.Information("Initializing CI Visibility"); - var settings = Settings; - - // In case we are running using the agent, check if the event platform proxy is supported. - IDiscoveryService discoveryService = NullDiscoveryService.Instance; - var eventPlatformProxyEnabled = false; - if (!settings.Agentless) - { - if (!string.IsNullOrWhiteSpace(settings.ForceAgentsEvpProxy)) - { - // if we force the evp proxy (internal switch) - EventPlatformProxySupport = Enum.TryParse(settings.ForceAgentsEvpProxy, out var parsedValue) ? - parsedValue : - EventPlatformProxySupport.V2; - } - else - { - discoveryService = DiscoveryService.Create( - settings.TracerSettings.Exporter, - tcpTimeout: TimeSpan.FromSeconds(5), - initialRetryDelayMs: 10, - maxRetryDelayMs: 1000, - recheckIntervalMs: int.MaxValue); - EventPlatformProxySupport = IsEventPlatformProxySupportedByAgent(discoveryService); - } - - eventPlatformProxyEnabled = EventPlatformProxySupport != EventPlatformProxySupport.None; - if (eventPlatformProxyEnabled) - { - Log.Information("EVP Proxy was enabled with mode: {Mode}", EventPlatformProxySupport); - } - } - - LifetimeManager.Instance.AddAsyncShutdownTask(ShutdownAsync); - - var tracerSettings = settings.TracerSettings; - Log.Debug("Setting up the test session name to: {TestSessionName}", settings.TestSessionName); - Log.Debug("Setting up the service name to: {ServiceName}", tracerSettings.ServiceName); - - // Initialize Tracer - Log.Information("Initialize Test Tracer instance"); - TracerManager.ReplaceGlobalManager(tracerSettings, new CITracerManagerFactory(settings, discoveryService, eventPlatformProxyEnabled, UseLockedTracerManager)); - _ = Tracer.Instance; - - // Initialize FrameworkDescription - _ = FrameworkDescription.Instance; - - // Initialize CIEnvironment - _ = CIEnvironmentValues.Instance; - - // If we are running in agentless mode or the agent support the event platform proxy endpoint. - // We can use the intelligent test runner - if (settings.Agentless || eventPlatformProxyEnabled) - { - // Intelligent Test Runner or GitUploadEnabled - if (settings.IntelligentTestRunnerEnabled) - { - Log.Information("ITR: Update and uploading git tree metadata and getting skippable tests."); - _skippableTestsTask = GetIntelligentTestRunnerSkippableTestsAsync(); - LifetimeManager.Instance.AddAsyncShutdownTask(_ => _skippableTestsTask); - } - else if (settings.GitUploadEnabled != false) - { - // Update and upload git tree metadata. - Log.Information("ITR: Update and uploading git tree metadata."); - var tskItrUpdate = UploadGitMetadataAsync(); - LifetimeManager.Instance.AddAsyncShutdownTask(_ => tskItrUpdate); - } - } - else if (settings.IntelligentTestRunnerEnabled) - { - Log.Warning("ITR: Intelligent test runner cannot be activated. Agent doesn't support the event platform proxy endpoint."); - } - else if (settings.GitUploadEnabled != false) - { - Log.Warning("ITR: Upload git metadata cannot be activated. Agent doesn't support the event platform proxy endpoint."); - } - } - - internal static void InitializeFromRunner(CIVisibilitySettings settings, IDiscoveryService discoveryService, bool eventPlatformProxyEnabled) - { - if (Interlocked.Exchange(ref _firstInitialization, 0) != 1) - { - // Initialize() or InitializeFromRunner() was already called before - return; - } - - Log.Information("Initializing CI Visibility from dd-trace / runner"); - Settings = settings; - EventPlatformProxySupport = Enum.TryParse(settings.ForceAgentsEvpProxy, out var parsedValue) ? - parsedValue : - IsEventPlatformProxySupportedByAgent(discoveryService); - LifetimeManager.Instance.AddAsyncShutdownTask(ShutdownAsync); - - var tracerSettings = settings.TracerSettings; - Log.Debug("Setting up the test session name to: {TestSessionName}", settings.TestSessionName); - Log.Debug("Setting up the service name to: {ServiceName}", tracerSettings.ServiceName); - - // Initialize Tracer - Log.Information("Initialize Test Tracer instance"); - TracerManager.ReplaceGlobalManager(tracerSettings, new CITracerManagerFactory(settings, discoveryService, eventPlatformProxyEnabled, UseLockedTracerManager)); - _ = Tracer.Instance; - - // Initialize FrameworkDescription - _ = FrameworkDescription.Instance; - - // Initialize CIEnvironment - _ = CIEnvironmentValues.Instance; - } - - internal static void InitializeFromManualInstrumentation() - { - if (!IsRunning) - { - // If we are using only the Public API without auto-instrumentation (TestSession/TestModule/TestSuite/Test classes only) - // then we can disable both GitUpload and Intelligent Test Runner feature (only used by our integration). - Settings.SetDefaultManualInstrumentationSettings(); - Initialize(); - } - } - - internal static void Flush() - { - var sContext = SynchronizationContext.Current; - using var cts = new CancellationTokenSource(30_000); - try - { - SynchronizationContext.SetSynchronizationContext(null); - AsyncUtil.RunSync(() => FlushAsync(), cts.Token); - if (cts.IsCancellationRequested) - { - Log.Error("Timeout occurred when flushing spans.{NewLine}{StackTrace}", Environment.NewLine, Environment.StackTrace); - } - } - catch (TaskCanceledException) - { - Log.Error("Timeout occurred when flushing spans.{NewLine}{StackTrace}", Environment.NewLine, Environment.StackTrace); - } - finally - { - SynchronizationContext.SetSynchronizationContext(sContext); - } - } - - internal static async Task FlushAsync() - { - try - { - // We have to ensure the flush of the buffer after we finish the tests of an assembly. - // For some reason, sometimes when all test are finished none of the callbacks to handling the tracer disposal is triggered. - // So the last spans in buffer aren't send to the agent. - Log.Debug("Integration flushing spans."); - - if (Settings.Logs) - { - await Task.WhenAll( - Tracer.Instance.FlushAsync(), - Tracer.Instance.TracerManager.DirectLogSubmission.Sink.FlushAsync()).ConfigureAwait(false); - } - else - { - await Tracer.Instance.FlushAsync().ConfigureAwait(false); - } - - Log.Debug("Integration flushed."); - } - catch (Exception ex) - { - Log.Error(ex, "Exception occurred when flushing spans."); - } - } - - /// - /// Manually close the CI Visibility mode triggering the LifeManager to run the shutdown tasks - /// This is required due to a weird behavior on the VSTest framework were the shutdown tasks are not awaited: - /// ` if testhost doesn't shut down within 100ms(as the execution is completed, we expect it to shutdown fast). - /// vstest.console forcefully kills the process.` - /// https://github.com/microsoft/vstest/issues/1900#issuecomment-457488472 - /// https://github.com/Microsoft/vstest/blob/2d4508232b6655a4f363b8bbcc887441c7d1d334/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs#L197 - /// - internal static void Close() - { - if (IsRunning) - { - Log.Information("CI Visibility is exiting."); - LifetimeManager.Instance.RunShutdownTasks(); - - // If the continuous profiler is attached we ensure to flush the remaining profiles before closing. - try - { - if (ContinuousProfiler.Profiler.Instance.Status.IsProfilerReady) - { - ContinuousProfiler.NativeInterop.FlushProfile(); - } - } - catch (Exception ex) - { - Log.Error(ex, "Error flushing the profiler."); - } - - Interlocked.Exchange(ref _firstInitialization, 1); - } - } - - internal static void WaitForSkippableTaskToFinish() - { - if (_skippableTestsTask is { IsCompleted: false }) - { - var sContext = SynchronizationContext.Current; - try - { - SynchronizationContext.SetSynchronizationContext(null); - AsyncUtil.RunSync(() => _skippableTestsTask); - } - finally - { - SynchronizationContext.SetSynchronizationContext(sContext); - } - } - } - - internal static Task> GetSkippableTestsFromSuiteAndNameAsync(string suite, string name) - { - if (_skippableTestsTask is { } skippableTask) - { - if (skippableTask.IsCompleted) - { - return Task.FromResult(GetSkippableTestsFromSuiteAndName(suite, name)); - } - - return SlowGetSkippableTestsFromSuiteAndNameAsync(suite, name); - } - - return Task.FromResult((IList)Array.Empty()); - - static async Task> SlowGetSkippableTestsFromSuiteAndNameAsync(string suite, string name) - { - await _skippableTestsTask!.ConfigureAwait(false); - return GetSkippableTestsFromSuiteAndName(suite, name); - } - - static IList GetSkippableTestsFromSuiteAndName(string suite, string name) - { - if (_skippableTestsBySuiteAndName is { } skippeableTestBySuite) - { - if (skippeableTestBySuite.TryGetValue(suite, out var testsInSuite) && - testsInSuite.TryGetValue(name, out var tests)) - { - return tests; - } - } - - return Array.Empty(); - } - } - - internal static bool IsAnEarlyFlakeDetectionTest(string moduleName, string testSuite, string testName) - { - if (EarlyFlakeDetectionResponse is { Tests: { } efdTests } && - efdTests.TryGetValue(moduleName, out var efdResponseSuites) && - efdResponseSuites?.TryGetValue(testSuite, out var efdResponseTests) == true && - efdResponseTests is not null) - { - foreach (var test in efdResponseTests) - { - if (test == testName) - { - Log.Debug("Test is included in the early flake detection response. [ModuleName: {ModuleName}, TestSuite: {TestSuite}, TestName: {TestName}]", moduleName, testSuite, testName); - return true; - } - } - } - - Log.Debug("Test is not in the early flake detection response. [ModuleName: {ModuleName}, TestSuite: {TestSuite}, TestName: {TestName}]", moduleName, testSuite, testName); - return false; - } - - internal static bool HasSkippableTests() => _skippableTestsBySuiteAndName?.Count > 0; - - internal static string? GetSkippableTestsCorrelationId() => _skippableTestsCorrelationId; - - internal static string GetServiceNameFromRepository(string? repository) - { - if (!string.IsNullOrEmpty(repository)) - { - if (repository!.EndsWith("/") || repository.EndsWith("\\")) - { - repository = repository.Substring(0, repository.Length - 1); - } - - Regex regex = new Regex(@"[/\\]?([a-zA-Z0-9\-_.]*)$"); - Match match = regex.Match(repository); - if (match.Success && match.Groups.Count > 1) - { - const string gitSuffix = ".git"; - string repoName = match.Groups[1].Value; - if (repoName.EndsWith(gitSuffix)) - { - return repoName.Substring(0, repoName.Length - gitSuffix.Length); - } - else - { - return repoName; - } - } - } - - return string.Empty; - } - - internal static IApiRequestFactory GetRequestFactory(TracerSettings settings) - { - return GetRequestFactory(settings, TimeSpan.FromSeconds(15)); - } - - internal static IApiRequestFactory GetRequestFactory(TracerSettings tracerSettings, TimeSpan timeout) - { - IApiRequestFactory? factory = null; - var exporterSettings = tracerSettings.Exporter; - if (exporterSettings.TracesTransport != TracesTransportType.Default) - { - factory = AgentTransportStrategy.Get( - exporterSettings, - productName: "CI Visibility", - tcpTimeout: null, - AgentHttpHeaderNames.DefaultHeaders, - () => new TraceAgentHttpHeaderHelper(), - uri => uri); - } - else - { -#if NETCOREAPP - Log.Information("Using {FactoryType} for trace transport.", nameof(HttpClientRequestFactory)); - factory = new HttpClientRequestFactory( - exporterSettings.AgentUri, - AgentHttpHeaderNames.DefaultHeaders, - handler: new System.Net.Http.HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, }, - timeout: timeout); -#else - Log.Information("Using {FactoryType} for trace transport.", nameof(ApiWebRequestFactory)); - factory = new ApiWebRequestFactory(tracerSettings.Exporter.AgentUri, AgentHttpHeaderNames.DefaultHeaders, timeout: timeout); -#endif - var settings = Settings; - if (!string.IsNullOrWhiteSpace(settings.ProxyHttps)) - { - var proxyHttpsUriBuilder = new UriBuilder(settings.ProxyHttps); - - var userName = proxyHttpsUriBuilder.UserName; - var password = proxyHttpsUriBuilder.Password; - - proxyHttpsUriBuilder.UserName = string.Empty; - proxyHttpsUriBuilder.Password = string.Empty; - - if (proxyHttpsUriBuilder.Scheme == "https") - { - // HTTPS proxy is not supported by .NET BCL - Log.Error("HTTPS proxy is not supported. ({ProxyHttpsUriBuilder})", proxyHttpsUriBuilder); - return factory; - } - - NetworkCredential? credential = null; - if (!string.IsNullOrWhiteSpace(userName)) - { - credential = new NetworkCredential(userName, password); - } - - Log.Information("Setting proxy to: {ProxyHttps}", proxyHttpsUriBuilder.Uri.ToString()); - factory.SetProxy(new WebProxy(proxyHttpsUriBuilder.Uri, true, settings.ProxyNoProxy, credential), credential); - } - } - - return factory; - } - - internal static string GetOperatingSystemVersion() - { - // we cache the OS version because is called multiple times during the test execution - // and we want to avoid multiple system calls for Linux and macOS - return _osVersion ??= GetOperatingSystemVersionInternal(); - - static string GetOperatingSystemVersionInternal() - { - switch (FrameworkDescription.Instance.OSPlatform) - { - case OSPlatformName.Linux: - if (!string.IsNullOrEmpty(HostMetadata.Instance.KernelRelease)) - { - return HostMetadata.Instance.KernelRelease!; - } - - break; - case OSPlatformName.MacOS: - try - { - var osxVersionCommand = ProcessHelpers.RunCommand(new ProcessHelpers.Command("uname", "-r")); - var osxVersion = osxVersionCommand?.Output.Trim(' ', '\n'); - if (!string.IsNullOrEmpty(osxVersion)) - { - return osxVersion!; - } - } - catch (Exception ex) - { - Log.Warning(ex, "Error getting OS version on macOS"); - } - - break; - } - - return Environment.OSVersion.VersionString; - } - } - - /// - /// Resets CI Visibility to the initial values. Used for testing purposes. - /// - internal static void Reset() - { - _settings = null; - _firstInitialization = 1; - _enabledLazy = new(InternalEnabled, true); - _skippableTestsTask = null; - _skippableTestsBySuiteAndName = null; - } - - private static async Task ShutdownAsync(Exception? exception) - { - // Let's close any opened test, suite, modules and sessions before shutting down to avoid losing any data. - // But marking them as failed. - - foreach (var test in Test.ActiveTests) - { - if (exception is not null) - { - test.SetErrorInfo(exception); - } - - test.Close(TestStatus.Skip, null, "Test is being closed due to test session shutdown."); - } - - foreach (var testSuite in TestSuite.ActiveTestSuites) - { - if (exception is not null) - { - testSuite.SetErrorInfo(exception); - } - - testSuite.Close(); - } - - foreach (var testModule in TestModule.ActiveTestModules) - { - if (exception is not null) - { - testModule.SetErrorInfo(exception); - } - - await testModule.CloseAsync().ConfigureAwait(false); - } - - foreach (var testSession in TestSession.ActiveTestSessions) - { - if (exception is not null) - { - testSession.SetErrorInfo(exception); - } - - await testSession.CloseAsync(TestStatus.Skip).ConfigureAwait(false); - } - - await FlushAsync().ConfigureAwait(false); - MethodSymbolResolver.Instance.Clear(); - } - - private static bool InternalEnabled() - { - string? processName = null; - - // By configuration - if (Settings.Enabled is { } enabled) - { - if (enabled) - { - Log.Information("CI Visibility Enabled by Configuration"); - return true; - } - - // explicitly disabled - Log.Information("CI Visibility Disabled by Configuration"); - return false; - } - - // Try to autodetect based in the domain name. - var domainName = AppDomain.CurrentDomain.FriendlyName ?? string.Empty; - if (domainName.StartsWith("testhost", StringComparison.Ordinal) || - domainName.StartsWith("xunit", StringComparison.Ordinal) || - domainName.StartsWith("nunit", StringComparison.Ordinal) || - domainName.StartsWith("MSBuild", StringComparison.Ordinal)) - { - Log.Information("CI Visibility Enabled by Domain name whitelist"); - PropagateCiVisibilityEnvironmentVariable(); - return true; - } - - // Try to autodetect based in the process name. - processName ??= GetProcessName(); - if (processName.StartsWith("testhost.", StringComparison.Ordinal)) - { - Log.Information("CI Visibility Enabled by Process name whitelist"); - PropagateCiVisibilityEnvironmentVariable(); - return true; - } - - return false; - - static void PropagateCiVisibilityEnvironmentVariable() - { - try - { - // Set the configuration key to propagate the configuration to child processes. - Environment.SetEnvironmentVariable(ConfigurationKeys.CIVisibility.Enabled, "1", EnvironmentVariableTarget.Process); - } - catch - { - // . - } - } - - static string GetProcessName() - { - try - { - return ProcessHelpers.GetCurrentProcessName(); - } - catch (Exception exception) - { - Log.Warning(exception, "Error getting current process name when checking CI Visibility status"); - } - - return string.Empty; - } - } - - private static async Task UploadGitMetadataAsync() - { - try - { - var itrClient = new IntelligentTestRunnerClient(CIEnvironmentValues.Instance.WorkspacePath, Settings); - await itrClient.UploadRepositoryChangesAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Error(ex, "ITR: Error uploading repository git metadata."); - } - } - - private static async Task GetIntelligentTestRunnerSkippableTestsAsync() - { - try - { - var settings = Settings; - var lazyItrClient = new Lazy(() => new(CIEnvironmentValues.Instance.WorkspacePath, settings)); - - Task? uploadRepositoryChangesTask = null; - if (settings.GitUploadEnabled != false) - { - // Upload the git metadata - async Task UploadRepositoryChangesAsync() - { - try - { - await lazyItrClient.Value.UploadRepositoryChangesAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - Log.Error(ex, "CIVisibility: Error uploading repository git metadata."); - } - } - - uploadRepositoryChangesTask = Task.Run(UploadRepositoryChangesAsync); - } - - // If any DD_CIVISIBILITY_CODE_COVERAGE_ENABLED or DD_CIVISIBILITY_TESTSSKIPPING_ENABLED has not been set - // We query the settings api for those - if (settings.CodeCoverageEnabled == null - || settings.TestsSkippingEnabled == null - || settings.EarlyFlakeDetectionEnabled != false - || settings.ImpactedTestsDetectionEnabled == null) - { - var itrSettings = await lazyItrClient.Value.GetSettingsAsync().ConfigureAwait(false); - - // we check if the backend require the git metadata first - if (itrSettings.RequireGit == true && uploadRepositoryChangesTask is not null) - { - Log.Debug("ITR: require git received, awaiting for the git repository upload."); - await uploadRepositoryChangesTask.ConfigureAwait(false); - - Log.Debug("ITR: calling the configuration api again."); - itrSettings = await lazyItrClient.Value.GetSettingsAsync(skipFrameworkInfo: true).ConfigureAwait(false); - } - - if (settings.CodeCoverageEnabled == null && itrSettings.CodeCoverage.HasValue) - { - Log.Information("ITR: Code Coverage has been changed to {Value} by settings api.", itrSettings.CodeCoverage.Value); - settings.SetCodeCoverageEnabled(itrSettings.CodeCoverage.Value); - } - - if (settings.TestsSkippingEnabled == null && itrSettings.TestsSkipping.HasValue) - { - Log.Information("ITR: Tests Skipping has been changed to {Value} by settings api.", itrSettings.TestsSkipping.Value); - settings.SetTestsSkippingEnabled(itrSettings.TestsSkipping.Value); - } - - if (settings.EarlyFlakeDetectionEnabled == true || itrSettings.EarlyFlakeDetection.Enabled == true) - { - Log.Information("CIVisibility: Early flake detection settings has been enabled by the settings api."); - EarlyFlakeDetectionSettings = itrSettings.EarlyFlakeDetection; - settings.SetEarlyFlakeDetectionEnabled(true); - EarlyFlakeDetectionResponse = await lazyItrClient.Value.GetEarlyFlakeDetectionTestsAsync().ConfigureAwait(false); - } - else - { - settings.SetEarlyFlakeDetectionEnabled(false); - } - - if (settings.FlakyRetryEnabled == null && itrSettings.FlakyTestRetries.HasValue) - { - Log.Information("CIVisibility: Flaky Retries has been changed to {Value} by the settings api.", itrSettings.FlakyTestRetries.Value); - settings.SetFlakyRetryEnabled(itrSettings.FlakyTestRetries.Value); - } - - if (settings.ImpactedTestsDetectionEnabled == null && itrSettings.ImpactedTestsEnabled.HasValue) - { - Log.Information("CIVisibility: Impacted Tests Detection has been changed to {Value} by the settings api.", itrSettings.ImpactedTestsEnabled.Value); - settings.SetImpactedTestsEnabled(itrSettings.ImpactedTestsEnabled.Value); - } - - if (settings.ImpactedTestsDetectionEnabled == true) - { - ImpactedTestsDetectionResponse = await lazyItrClient.Value.GetImpactedTestsDetectionFilesAsync().ConfigureAwait(false); - } - } - - // Log code coverage status - Log.Information("{V}", settings.CodeCoverageEnabled == true ? "ITR: Tests code coverage is enabled." : "ITR: Tests code coverage is disabled."); - - // Log early flake detection status - Log.Information("{V}", settings.EarlyFlakeDetectionEnabled == true ? "CIVisibility: Early flake detection is enabled." : "CIVisibility: Early flake detection is disabled."); - - // Log flaky retries status - Log.Information("{V}", settings.FlakyRetryEnabled == true ? "CIVisibility: Flaky retries is enabled." : "CIVisibility: Flaky retries is disabled."); - - // Log impacted tests detection status - Log.Information("{V}", settings.ImpactedTestsDetectionEnabled == true ? "CIVisibility: Impacted tests detection is enabled." : "CIVisibility: Impacted tests detection is disabled."); - - // For ITR we need the git metadata upload before consulting the skippable tests. - // If ITR is disabled we just need to make sure the git upload task has completed before leaving this method. - if (uploadRepositoryChangesTask is not null) - { - await uploadRepositoryChangesTask.ConfigureAwait(false); - } - - // If the tests skipping feature is enabled we query the api for the tests we have to skip - if (settings.TestsSkippingEnabled == true) - { - var skippeableTests = await lazyItrClient.Value.GetSkippableTestsAsync().ConfigureAwait(false); - Log.Information("ITR: CorrelationId = {CorrelationId}, SkippableTests = {Length}.", skippeableTests.CorrelationId, skippeableTests.Tests.Length); - - var skippableTestsBySuiteAndName = new Dictionary>>(); - foreach (var item in skippeableTests.Tests) - { - if (!skippableTestsBySuiteAndName.TryGetValue(item.Suite, out var suite)) - { - suite = new Dictionary>(); - skippableTestsBySuiteAndName[item.Suite] = suite; - } - - if (!suite.TryGetValue(item.Name, out var name)) - { - name = new List(); - suite[item.Name] = name; - } - - name.Add(item); - } - - _skippableTestsCorrelationId = skippeableTests.CorrelationId; - _skippableTestsBySuiteAndName = skippableTestsBySuiteAndName; - Log.Debug("ITR: SkippableTests dictionary has been built."); - } - else - { - Log.Information("ITR: Tests skipping is disabled."); - } - } - catch (Exception ex) - { - Log.Error(ex, "ITR: Error getting skippeable tests."); - } - } - - private static EventPlatformProxySupport IsEventPlatformProxySupportedByAgent(IDiscoveryService discoveryService) - { - if (discoveryService is NullDiscoveryService) - { - return EventPlatformProxySupport.None; - } - - Log.Debug("Waiting for agent configuration..."); - var agentConfiguration = new DiscoveryAgentConfigurationCallback(discoveryService).WaitAndGet(5_000); - if (agentConfiguration is null) - { - Log.Warning("Discovery service could not retrieve the agent configuration after 5 seconds."); - return EventPlatformProxySupport.None; - } - - var eventPlatformProxyEndpoint = agentConfiguration.EventPlatformProxyEndpoint; - return EventPlatformProxySupportFromEndpointUrl(eventPlatformProxyEndpoint); - } - - internal static EventPlatformProxySupport EventPlatformProxySupportFromEndpointUrl(string? eventPlatformProxyEndpoint) - { - if (!string.IsNullOrEmpty(eventPlatformProxyEndpoint)) - { - if (eventPlatformProxyEndpoint?.Contains("/v2") == true) - { - Log.Information("Event platform proxy V2 supported by agent."); - return EventPlatformProxySupport.V2; - } - - if (eventPlatformProxyEndpoint?.Contains("/v4") == true) - { - Log.Information("Event platform proxy V4 supported by agent."); - return EventPlatformProxySupport.V4; - } - - Log.Information("EventPlatformProxyEndpoint: '{EVPEndpoint}' not supported.", eventPlatformProxyEndpoint); - } - else - { - Log.Information("Event platform proxy is not supported by the agent. Falling back to the APM protocol."); - } - - return EventPlatformProxySupport.None; - } - - private class DiscoveryAgentConfigurationCallback - { - private readonly ManualResetEventSlim _manualResetEventSlim; - private readonly Action _callback; - private readonly IDiscoveryService _discoveryService; - private AgentConfiguration? _agentConfiguration; - - public DiscoveryAgentConfigurationCallback(IDiscoveryService discoveryService) - { - _manualResetEventSlim = new ManualResetEventSlim(); - LifetimeManager.Instance.AddShutdownTask(_ => _manualResetEventSlim.Set()); - _discoveryService = discoveryService; - _callback = CallBack; - _agentConfiguration = null; - _discoveryService.SubscribeToChanges(_callback); - } - - public AgentConfiguration? WaitAndGet(int timeoutInMs = 5_000) - { - _manualResetEventSlim.Wait(timeoutInMs); - return _agentConfiguration; - } - - private void CallBack(AgentConfiguration agentConfiguration) - { - _agentConfiguration = agentConfiguration; - _manualResetEventSlim.Set(); - _discoveryService.RemoveSubscription(_callback); - Log.Debug("Agent configuration received."); - } - } - } -} diff --git a/tracer/src/Datadog.Trace/Ci/CiEnvironment/GithubActionsEnvironmentValues.cs b/tracer/src/Datadog.Trace/Ci/CiEnvironment/GithubActionsEnvironmentValues.cs index a701dac2bdf0..dbea03f3278c 100644 --- a/tracer/src/Datadog.Trace/Ci/CiEnvironment/GithubActionsEnvironmentValues.cs +++ b/tracer/src/Datadog.Trace/Ci/CiEnvironment/GithubActionsEnvironmentValues.cs @@ -124,7 +124,7 @@ private void LoadGithubEventJson() } catch (Exception ex) { - CIVisibility.Log.Warning(ex, "Error loading the github-event.json"); + TestOptimization.Instance.Log.Warning(ex, "Error loading the github-event.json"); } } } diff --git a/tracer/src/Datadog.Trace/Ci/Configuration/CIVisibilitySettings.cs b/tracer/src/Datadog.Trace/Ci/Configuration/TestOptimizationSettings.cs similarity index 97% rename from tracer/src/Datadog.Trace/Ci/Configuration/CIVisibilitySettings.cs rename to tracer/src/Datadog.Trace/Ci/Configuration/TestOptimizationSettings.cs index 855b4d8824ba..434b08d32a52 100644 --- a/tracer/src/Datadog.Trace/Ci/Configuration/CIVisibilitySettings.cs +++ b/tracer/src/Datadog.Trace/Ci/Configuration/TestOptimizationSettings.cs @@ -1,4 +1,4 @@ -// +// // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Linq; using System.Threading; using Datadog.Trace.Ci.Tags; @@ -18,11 +17,11 @@ namespace Datadog.Trace.Ci.Configuration { - internal class CIVisibilitySettings + internal class TestOptimizationSettings { private TracerSettings? _tracerSettings; - public CIVisibilitySettings(IConfigurationSource source, IConfigurationTelemetry telemetry) + public TestOptimizationSettings(IConfigurationSource source, IConfigurationTelemetry telemetry) { var config = new ConfigurationBuilder(source, telemetry); Enabled = config.WithKeys(ConfigurationKeys.CIVisibility.Enabled).AsBool(); @@ -256,9 +255,9 @@ public CIVisibilitySettings(IConfigurationSource source, IConfigurationTelemetry /// public TracerSettings TracerSettings => LazyInitializer.EnsureInitialized(ref _tracerSettings, () => InitializeTracerSettings())!; - public static CIVisibilitySettings FromDefaultSources() + public static TestOptimizationSettings FromDefaultSources() { - return new CIVisibilitySettings(GlobalConfigurationSource.Instance, TelemetryFactory.Config); + return new TestOptimizationSettings(GlobalConfigurationSource.Instance, TelemetryFactory.Config); } internal void SetCodeCoverageEnabled(bool value) diff --git a/tracer/src/Datadog.Trace/Ci/Coverage/CoverageReporter.cs b/tracer/src/Datadog.Trace/Ci/Coverage/CoverageReporter.cs index 3b8afff7ea3b..4de7e228ed26 100644 --- a/tracer/src/Datadog.Trace/Ci/Coverage/CoverageReporter.cs +++ b/tracer/src/Datadog.Trace/Ci/Coverage/CoverageReporter.cs @@ -18,7 +18,7 @@ namespace Datadog.Trace.Ci.Coverage; [EditorBrowsable(EditorBrowsableState.Never)] public static class CoverageReporter { - private static CoverageEventHandler _handler = CIVisibility.Settings.TestsSkippingEnabled == true ? new DefaultCoverageEventHandler() : new DefaultWithGlobalCoverageEventHandler(); + private static CoverageEventHandler _handler = TestOptimization.Instance.Settings.TestsSkippingEnabled == true ? new DefaultCoverageEventHandler() : new DefaultWithGlobalCoverageEventHandler(); /// /// Gets or sets coverage handler diff --git a/tracer/src/Datadog.Trace/Ci/Coverage/Util/CoverageUtils.cs b/tracer/src/Datadog.Trace/Ci/Coverage/Util/CoverageUtils.cs index e4c5599fa771..4aec22432761 100644 --- a/tracer/src/Datadog.Trace/Ci/Coverage/Util/CoverageUtils.cs +++ b/tracer/src/Datadog.Trace/Ci/Coverage/Util/CoverageUtils.cs @@ -2,19 +2,21 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; using System.Collections.Generic; using System.IO; using System.Text; +using Datadog.Trace.Ci; using Datadog.Trace.Ci.Coverage.Models.Global; using Datadog.Trace.Logging; using Datadog.Trace.Vendors.Newtonsoft.Json; internal static class CoverageUtils { - internal static readonly IDatadogLogger Log = Datadog.Trace.Ci.CIVisibility.Log; + internal static readonly IDatadogLogger Log = TestOptimization.Instance.Log; public static bool TryCombineAndGetTotalCoverage(string inputFolder, string outputFile) { diff --git a/tracer/src/Datadog.Trace/Ci/GitCommandHelper.cs b/tracer/src/Datadog.Trace/Ci/GitCommandHelper.cs index f64fff6cee98..4e02724e4fcd 100644 --- a/tracer/src/Datadog.Trace/Ci/GitCommandHelper.cs +++ b/tracer/src/Datadog.Trace/Ci/GitCommandHelper.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -37,21 +38,21 @@ internal static class GitCommandHelper { var sw = System.Diagnostics.Stopwatch.StartNew(); var gitOutput = ProcessHelpers.RunCommand( - new ProcessHelpers.Command( - "git", - arguments, - workingDirectory, - outputEncoding: Encoding.Default, - errorEncoding: Encoding.Default, - inputEncoding: Encoding.Default, - useWhereIsIfFileNotFound: true, - timeout: TimeSpan.FromMinutes(5)), - input); + new ProcessHelpers.Command( + "git", + arguments, + workingDirectory, + outputEncoding: Encoding.Default, + errorEncoding: Encoding.Default, + inputEncoding: Encoding.Default, + useWhereIsIfFileNotFound: true, + timeout: TimeSpan.FromMinutes(5)), + input); TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitCommandMs(ciVisibilityCommand, sw.Elapsed.TotalMilliseconds); if (gitOutput is null) { TelemetryFactory.Metrics.RecordCountCIVisibilityGitCommandErrors(ciVisibilityCommand, MetricTags.CIVisibilityExitCodes.Unknown); - Log.Warning("ITR: 'git {Arguments}' command is null", arguments); + Log.Warning("GitCommandHelper: 'git {Arguments}' command is null", arguments); } else if (gitOutput.ExitCode != 0) { @@ -71,14 +72,14 @@ internal static class GitCommandHelper } var txt = StringBuilderCache.GetStringAndRelease(sb); - Log.Debug("Git command {Command}", txt); + Log.Debug("GitCommandHelper: Git command {Command}", txt); } return gitOutput; } catch (System.ComponentModel.Win32Exception ex) { - Log.Warning(ex, "TestVis: 'git {Arguments}' threw Win32Exception - git is likely not available", arguments); + Log.Warning(ex, "GitCommandHelper: 'git {Arguments}' threw Win32Exception - git is likely not available", arguments); TelemetryFactory.Metrics.RecordCountCIVisibilityGitCommandErrors(ciVisibilityCommand, MetricTags.CIVisibilityExitCodes.Missing); return null; } @@ -101,7 +102,7 @@ public static FileCoverageInfo[] GetGitDiffFilesAndLines(string workingDirectory } catch (Exception ex) { - Log.Information(ex, "Error calling git diff"); + Log.Information(ex, "GitCommandHelper: Error calling git diff"); throw; } @@ -130,7 +131,7 @@ static List ParseGitDiff(string diffOutput) } currentFile = new FileCoverageInfo(headerMatch.Groups["file"].Value); - Log.Debug(" Processing {File} ...", currentFile.Path); + Log.Debug("GitCommandHelper: Processing {File} ...", currentFile.Path); continue; } @@ -148,7 +149,7 @@ static List ParseGitDiff(string diffOutput) modifiedLines.Add(new Tuple(startLine, startLine + lineCount)); var range = modifiedLines[modifiedLines.Count - 1]; - Log.Debug(" {From}..{To} ...", range.Item1, range.Item2); + Log.Debug("GitCommandHelper: {From}..{To} ...", range.Item1, range.Item2); continue; } } diff --git a/tracer/src/Datadog.Trace/Ci/ITestOptimization.cs b/tracer/src/Datadog.Trace/Ci/ITestOptimization.cs new file mode 100644 index 000000000000..057f1a45ce5e --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/ITestOptimization.cs @@ -0,0 +1,52 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System.Threading.Tasks; +using Datadog.Trace.Agent.DiscoveryService; +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Ci.Net; +using Datadog.Trace.Logging; + +namespace Datadog.Trace.Ci; + +internal interface ITestOptimization +{ + IDatadogLogger Log { get; } + + bool Enabled { get; } + + bool IsRunning { get; } + + TestOptimizationSettings Settings { get; } + + ITestOptimizationClient Client { get; } + + ITestOptimizationHostInfo HostInfo { get; } + + ITestOptimizationTracerManagement? TracerManagement { get; } + + ITestOptimizationEarlyFlakeDetectionFeature? EarlyFlakeDetectionFeature { get; } + + ITestOptimizationSkippableFeature? SkippableFeature { get; } + + ITestOptimizationImpactedTestsDetectionFeature? ImpactedTestsDetectionFeature { get; } + + ITestOptimizationFlakyRetryFeature? FlakyRetryFeature { get; } + + void Initialize(); + + void InitializeFromRunner(TestOptimizationSettings settings, IDiscoveryService discoveryService, bool eventPlatformProxyEnabled, bool? useLockedTracerManager = null); + + void InitializeFromManualInstrumentation(); + + void Flush(); + + Task FlushAsync(); + + void Close(); + + void Reset(); +} diff --git a/tracer/src/Datadog.Trace/Ci/ITestOptimizationEarlyFlakeDetectionFeature.cs b/tracer/src/Datadog.Trace/Ci/ITestOptimizationEarlyFlakeDetectionFeature.cs new file mode 100644 index 000000000000..eaf02641bafe --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/ITestOptimizationEarlyFlakeDetectionFeature.cs @@ -0,0 +1,18 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using Datadog.Trace.Ci.Net; + +namespace Datadog.Trace.Ci; + +internal interface ITestOptimizationEarlyFlakeDetectionFeature : ITestOptimizationFeature +{ + TestOptimizationClient.EarlyFlakeDetectionSettingsResponse EarlyFlakeDetectionSettings { get; } + + TestOptimizationClient.EarlyFlakeDetectionResponse? EarlyFlakeDetectionResponse { get; } + + bool IsAnEarlyFlakeDetectionTest(string moduleName, string testSuite, string testName); +} diff --git a/tracer/src/Datadog.Trace/Ci/ITestOptimizationFeature.cs b/tracer/src/Datadog.Trace/Ci/ITestOptimizationFeature.cs new file mode 100644 index 000000000000..ab9ca1a2fce5 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/ITestOptimizationFeature.cs @@ -0,0 +1,12 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +namespace Datadog.Trace.Ci; + +internal interface ITestOptimizationFeature +{ + bool Enabled { get; } +} diff --git a/tracer/src/Datadog.Trace/Ci/ITestOptimizationFlakyRetryFeature.cs b/tracer/src/Datadog.Trace/Ci/ITestOptimizationFlakyRetryFeature.cs new file mode 100644 index 000000000000..c98f022fe088 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/ITestOptimizationFlakyRetryFeature.cs @@ -0,0 +1,11 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +namespace Datadog.Trace.Ci; + +internal interface ITestOptimizationFlakyRetryFeature : ITestOptimizationFeature +{ +} diff --git a/tracer/src/Datadog.Trace/Ci/ITestOptimizationHostInfo.cs b/tracer/src/Datadog.Trace/Ci/ITestOptimizationHostInfo.cs new file mode 100644 index 000000000000..a7b470df79c4 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/ITestOptimizationHostInfo.cs @@ -0,0 +1,12 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +namespace Datadog.Trace.Ci; + +internal interface ITestOptimizationHostInfo +{ + string GetOperatingSystemVersion(); +} diff --git a/tracer/src/Datadog.Trace/Ci/ITestOptimizationImpactedTestsDetectionFeature.cs b/tracer/src/Datadog.Trace/Ci/ITestOptimizationImpactedTestsDetectionFeature.cs new file mode 100644 index 000000000000..ecbfa32afb8c --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/ITestOptimizationImpactedTestsDetectionFeature.cs @@ -0,0 +1,14 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using Datadog.Trace.Ci.Net; + +namespace Datadog.Trace.Ci; + +internal interface ITestOptimizationImpactedTestsDetectionFeature : ITestOptimizationFeature +{ + TestOptimizationClient.ImpactedTestsDetectionResponse ImpactedTestsDetectionResponse { get; } +} diff --git a/tracer/src/Datadog.Trace/Ci/ITestOptimizationSkippableFeature.cs b/tracer/src/Datadog.Trace/Ci/ITestOptimizationSkippableFeature.cs new file mode 100644 index 000000000000..a9c8f24887f1 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/ITestOptimizationSkippableFeature.cs @@ -0,0 +1,20 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System.Collections.Generic; + +namespace Datadog.Trace.Ci; + +internal interface ITestOptimizationSkippableFeature : ITestOptimizationFeature +{ + void WaitForSkippableTaskToFinish(); + + IList GetSkippableTestsFromSuiteAndName(string suite, string name); + + bool HasSkippableTests(); + + string? GetCorrelationId(); +} diff --git a/tracer/src/Datadog.Trace/Ci/ITestOptimizationTracerManagement.cs b/tracer/src/Datadog.Trace/Ci/ITestOptimizationTracerManagement.cs new file mode 100644 index 000000000000..729d55912777 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/ITestOptimizationTracerManagement.cs @@ -0,0 +1,33 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using Datadog.Trace.Agent; +using Datadog.Trace.Agent.DiscoveryService; +using Datadog.Trace.Configuration; + +namespace Datadog.Trace.Ci; + +internal interface ITestOptimizationTracerManagement +{ + EventPlatformProxySupport EventPlatformProxySupport { get; } + + TestOptimizationTracerManager? Manager { get; } + + bool UseLockedTracerManager { get; } + + IDiscoveryService DiscoveryService { get; } + + EventPlatformProxySupport IsEventPlatformProxySupportedByAgent(IDiscoveryService discoveryService); + + EventPlatformProxySupport EventPlatformProxySupportFromEndpointUrl(string? eventPlatformProxyEndpoint); + + IApiRequestFactory GetRequestFactory(TracerSettings settings); + + IApiRequestFactory GetRequestFactory(TracerSettings tracerSettings, TimeSpan timeout); + + string GetServiceNameFromRepository(string? repository); +} diff --git a/tracer/src/Datadog.Trace/Ci/ImpactedTestsModule.cs b/tracer/src/Datadog.Trace/Ci/ImpactedTestsModule.cs index 631f54b2b7e5..e3368bf188fd 100644 --- a/tracer/src/Datadog.Trace/Ci/ImpactedTestsModule.cs +++ b/tracer/src/Datadog.Trace/Ci/ImpactedTestsModule.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -32,7 +33,7 @@ internal static class ImpactedTestsModule private static FileCoverageInfo[]? modifiedFiles = null; - public static bool IsEnabled => CIVisibility.Settings.ImpactedTestsDetectionEnabled ?? false; + public static bool IsEnabled => TestOptimization.Instance.Settings.ImpactedTestsDetectionEnabled ?? false; private static string? CurrentCommit => CIEnvironmentValues.Instance.HeadCommit ?? CIEnvironmentValues.Instance.Commit; @@ -162,7 +163,7 @@ private static FileCoverageInfo[] GetDiffFilesFromBackend() { Log.Debug("Retrieving diff files from backend..."); - if (CIVisibility.ImpactedTestsDetectionResponse is { } response && response.Files is { Length: > 0 } files) + if (TestOptimization.Instance.ImpactedTestsDetectionFeature?.ImpactedTestsDetectionResponse is { Files: { Length: > 0 } files }) { var res = new FileCoverageInfo[files.Length]; for (int x = 0; x < files.Length; x++) @@ -178,7 +179,7 @@ private static FileCoverageInfo[] GetDiffFilesFromBackend() private static string? GetBaseCommitFromBackend() { - if (CIVisibility.ImpactedTestsDetectionResponse is { } response && response.BaseSha is { Length: > 0 } baseSha) + if (TestOptimization.Instance.ImpactedTestsDetectionFeature?.ImpactedTestsDetectionResponse is { BaseSha: { Length: > 0 } baseSha }) { return baseSha; } diff --git a/tracer/src/Datadog.Trace/Ci/IntelligentTestRunnerClient.cs b/tracer/src/Datadog.Trace/Ci/IntelligentTestRunnerClient.cs deleted file mode 100644 index 152d3d3e1c72..000000000000 --- a/tracer/src/Datadog.Trace/Ci/IntelligentTestRunnerClient.cs +++ /dev/null @@ -1,1610 +0,0 @@ -// -// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. -// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. -// -#nullable enable - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Runtime.CompilerServices; -using System.Runtime.ExceptionServices; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; -using Datadog.Trace.Agent; -using Datadog.Trace.Agent.Transports; -using Datadog.Trace.Ci.CiEnvironment; -using Datadog.Trace.Ci.Configuration; -using Datadog.Trace.Ci.Telemetry; -using Datadog.Trace.Configuration; -using Datadog.Trace.Logging; -using Datadog.Trace.Processors; -using Datadog.Trace.Telemetry; -using Datadog.Trace.Telemetry.Metrics; -using Datadog.Trace.Util; -using Datadog.Trace.Vendors.Newtonsoft.Json; -using Datadog.Trace.Vendors.Serilog.Events; - -namespace Datadog.Trace.Ci; - -#pragma warning disable CS0649 - -/// -/// Intelligent Test Runner Client -/// -internal class IntelligentTestRunnerClient -{ - private const string ApiKeyHeader = "DD-API-KEY"; - private const string EvpSubdomainHeader = "X-Datadog-EVP-Subdomain"; - - private const int MaxRetries = 5; - private const int MaxPackFileSizeInMb = 3; - - private const string CommitType = "commit"; - private const string TestParamsType = "test_params"; - private const string SettingsType = "ci_app_test_service_libraries_settings"; - private const string EarlyFlakeDetectionRequestType = "ci_app_libraries_tests_request"; - private const string ImpactedTestsDetectionRequestType = "ci_app_tests_diffs_request"; - - private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(IntelligentTestRunnerClient)); - private static readonly Regex ShaRegex = new("[0-9a-f]+", RegexOptions.Compiled); - private static readonly JsonSerializerSettings SerializerSettings = new() { DefaultValueHandling = DefaultValueHandling.Ignore }; - - private readonly string _id; - private readonly CIVisibilitySettings _settings; - private readonly string? _workingDirectory; - private readonly string _environment; - private readonly string _serviceName; - private readonly Dictionary? _customConfigurations; - private readonly IApiRequestFactory _apiRequestFactory; - private readonly Uri _settingsUrl; - private readonly Uri _searchCommitsUrl; - private readonly Uri _packFileUrl; - private readonly Uri _skippableTestsUrl; - private readonly Uri _earlyFlakeDetectionTestsUrl; - private readonly Uri _impactedTestsDetectionTestsUrl; - private readonly EventPlatformProxySupport _eventPlatformProxySupport; - private readonly string _repositoryUrl; - private readonly string _branchName; - private readonly string _commitSha; - - public IntelligentTestRunnerClient(string? workingDirectory, CIVisibilitySettings? settings = null) - { - _id = RandomIdGenerator.Shared.NextSpanId().ToString(CultureInfo.InvariantCulture); - _settings = settings ?? CIVisibility.Settings; - - _workingDirectory = workingDirectory; - _environment = TraceUtil.NormalizeTag(_settings.TracerSettings.Environment ?? "none") ?? "none"; - _serviceName = NormalizerTraceProcessor.NormalizeService(_settings.TracerSettings.ServiceName) ?? string.Empty; - _customConfigurations = null; - - // Extract custom tests configurations from DD_TAGS - _customConfigurations = GetCustomTestsConfigurations(_settings.TracerSettings.GlobalTags); - - _repositoryUrl = GetRepositoryUrl(); - _commitSha = GetCommitSha(); - _branchName = GetBranchName(); - - _apiRequestFactory = CIVisibility.GetRequestFactory(_settings.TracerSettings, TimeSpan.FromSeconds(45)); - - const string settingsUrlPath = "api/v2/libraries/tests/services/setting"; - const string searchCommitsUrlPath = "api/v2/git/repository/search_commits"; - const string packFileUrlPath = "api/v2/git/repository/packfile"; - const string skippableTestsUrlPath = "api/v2/ci/tests/skippable"; - const string efdTestsUrlPath = "api/v2/ci/libraries/tests"; - const string itdTestsUrlPath = "api/v2/ci/tests/diffs"; - - if (_settings.Agentless) - { - _eventPlatformProxySupport = EventPlatformProxySupport.None; - var agentlessUrl = _settings.AgentlessUrl; - if (!string.IsNullOrWhiteSpace(agentlessUrl)) - { - _settingsUrl = new UriBuilder(agentlessUrl) { Path = settingsUrlPath }.Uri; - _searchCommitsUrl = new UriBuilder(agentlessUrl) { Path = searchCommitsUrlPath }.Uri; - _packFileUrl = new UriBuilder(agentlessUrl) { Path = packFileUrlPath }.Uri; - _skippableTestsUrl = new UriBuilder(agentlessUrl) { Path = skippableTestsUrlPath }.Uri; - _earlyFlakeDetectionTestsUrl = new UriBuilder(agentlessUrl) { Path = efdTestsUrlPath }.Uri; - _impactedTestsDetectionTestsUrl = new UriBuilder(agentlessUrl) { Path = itdTestsUrlPath }.Uri; - } - else - { - _settingsUrl = new UriBuilder( - scheme: "https", - host: "api." + _settings.Site, - port: 443, - pathValue: settingsUrlPath).Uri; - - _searchCommitsUrl = new UriBuilder( - scheme: "https", - host: "api." + _settings.Site, - port: 443, - pathValue: searchCommitsUrlPath).Uri; - - _packFileUrl = new UriBuilder( - scheme: "https", - host: "api." + _settings.Site, - port: 443, - pathValue: packFileUrlPath).Uri; - - _skippableTestsUrl = new UriBuilder( - scheme: "https", - host: "api." + _settings.Site, - port: 443, - pathValue: skippableTestsUrlPath).Uri; - - _earlyFlakeDetectionTestsUrl = new UriBuilder( - scheme: "https", - host: "api." + _settings.Site, - port: 443, - pathValue: efdTestsUrlPath).Uri; - - _impactedTestsDetectionTestsUrl = new UriBuilder( - scheme: "https", - host: "api." + _settings.Site, - port: 443, - pathValue: itdTestsUrlPath).Uri; - } - } - else - { - // Use Agent EVP Proxy - _eventPlatformProxySupport = CIVisibility.EventPlatformProxySupport; - switch (_eventPlatformProxySupport) - { - case EventPlatformProxySupport.V2: - _settingsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v2/{settingsUrlPath}"); - _searchCommitsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v2/{searchCommitsUrlPath}"); - _packFileUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v2/{packFileUrlPath}"); - _skippableTestsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v2/{skippableTestsUrlPath}"); - _earlyFlakeDetectionTestsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v2/{efdTestsUrlPath}"); - _impactedTestsDetectionTestsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v2/{itdTestsUrlPath}"); - break; - case EventPlatformProxySupport.V4: - _settingsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v4/{settingsUrlPath}"); - _searchCommitsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v4/{searchCommitsUrlPath}"); - _packFileUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v4/{packFileUrlPath}"); - _skippableTestsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v4/{skippableTestsUrlPath}"); - _earlyFlakeDetectionTestsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v4/{efdTestsUrlPath}"); - _impactedTestsDetectionTestsUrl = _apiRequestFactory.GetEndpoint($"evp_proxy/v4/{itdTestsUrlPath}"); - break; - default: - throw new NotSupportedException("Event platform proxy not supported by the agent."); - } - } - } - - internal static Dictionary? GetCustomTestsConfigurations(IReadOnlyDictionary globalTags) - { - if (globalTags is null) - { - return null; - } - - Dictionary? customConfiguration = null; - foreach (var tag in globalTags) - { - const string testConfigKey = "test.configuration."; - if (tag.Key.StartsWith(testConfigKey, StringComparison.OrdinalIgnoreCase)) - { - var key = tag.Key.Substring(testConfigKey.Length); - if (string.IsNullOrEmpty(key)) - { - continue; - } - - customConfiguration ??= new Dictionary(); - customConfiguration[key] = tag.Value; - } - } - - return customConfiguration; - } - - public async Task UploadRepositoryChangesAsync() - { - Log.Debug("ITR: Uploading Repository Changes..."); - - // Let's first try get the commit data from local and remote - var initialCommitData = await GetCommitsAsync().ConfigureAwait(false); - - // Let's check if we could retrieve commit data - if (!initialCommitData.IsOk) - { - return 0; - } - - // If: - // - We have local commits - // - There are not missing commits (backend has the total number of local commits already) - // Then we are good to go with it, we don't need to check if we need to unshallow or anything and just go with that. - if (initialCommitData is { HasCommits: true, MissingCommits.Length: 0 }) - { - Log.Debug("ITR: Initial commit data has everything already, we don't need to upload anything."); - return 0; - } - - // There's some missing commits on the backend, first we need to check if we need to unshallow before sending anything... - - try - { - // We need to check if the git clone is a shallow one before uploading anything. - // In the case is a shallow clone we need to reconfigure it to upload the git tree - // without blobs so no content will be downloaded. - var gitRevParseShallowOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "rev-parse --is-shallow-repository", MetricTags.CIVisibilityCommands.CheckShallow); - if (gitRevParseShallowOutput is null) - { - Log.Warning("ITR: 'git rev-parse --is-shallow-repository' command is null"); - return 0; - } - - var isShallow = gitRevParseShallowOutput.Output.IndexOf("true", StringComparison.OrdinalIgnoreCase) > -1; - if (!isShallow) - { - // Repo is not in a shallow state, we continue with the pack files upload with the initial commit data we retrieved earlier. - Log.Debug("ITR: Repository is not in a shallow state, uploading changes..."); - return await SendObjectsPackFileAsync(initialCommitData.LocalCommits[0], initialCommitData.MissingCommits, initialCommitData.RemoteCommits).ConfigureAwait(false); - } - - Log.Debug("ITR: Unshallowing the repository..."); - - // The git repo is a shallow clone, we need to double check if there are more than just 1 commit in the logs. - var gitShallowLogOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "log --format=oneline -n 2", MetricTags.CIVisibilityCommands.CheckShallow); - if (gitShallowLogOutput is null) - { - Log.Warning("ITR: 'git log --format=oneline -n 2' command is null"); - return 0; - } - - // After asking for 2 logs lines, if the git log command returns just one commit sha, we reconfigure the repo - // to ask for git commits and trees of the last month (no blobs) - var shallowLogArray = gitShallowLogOutput.Output.Split(["\n"], StringSplitOptions.RemoveEmptyEntries); - if (shallowLogArray.Length == 1) - { - // Just one commit SHA. Fetching previous commits - - // *** - // Let's try to unshallow the repo: - // `git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse HEAD)` - // *** - - // git config --default origin --get clone.defaultRemoteName - var originNameOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "config --default origin --get clone.defaultRemoteName", MetricTags.CIVisibilityCommands.GetRemote); - var originName = originNameOutput?.Output?.Replace("\n", string.Empty).Trim() ?? "origin"; - - // git rev-parse HEAD - var headOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "rev-parse HEAD", MetricTags.CIVisibilityCommands.GetHead); - var head = headOutput?.Output?.Replace("\n", string.Empty).Trim() ?? _branchName; - - // git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse HEAD) - Log.Information("ITR: The current repo is a shallow clone, refetching data for {OriginName}|{Head}", originName, head); - var gitUnshallowOutput = GitCommandHelper.RunGitCommand(_workingDirectory, $"fetch --shallow-since=\"1 month ago\" --update-shallow --filter=\"blob:none\" --recurse-submodules=no {originName} {head}", MetricTags.CIVisibilityCommands.Unshallow); - - if (gitUnshallowOutput is null || gitUnshallowOutput.ExitCode != 0) - { - // *** - // The previous command has a drawback: if the local HEAD is a commit that has not been pushed to the remote, it will fail. - // If this is the case, we fallback to: `git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse --abbrev-ref --symbolic-full-name @{upstream})` - // This command will attempt to use the tracked branch for the current branch in order to unshallow. - // *** - - // originName = git config --default origin --get clone.defaultRemoteName - // git rev-parse --abbrev-ref --symbolic-full-name @{upstream} - headOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "rev-parse --abbrev-ref --symbolic-full-name \"@{upstream}\"", MetricTags.CIVisibilityCommands.GetHead); - head = headOutput?.Output?.Replace("\n", string.Empty).Trim() ?? _branchName; - - // git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse --abbrev-ref --symbolic-full-name @{upstream}) - Log.Information("ITR: Previous unshallow command failed, refetching data with fallback 1 for {OriginName}|{Head}", originName, head); - gitUnshallowOutput = GitCommandHelper.RunGitCommand(_workingDirectory, $"fetch --shallow-since=\"1 month ago\" --update-shallow --filter=\"blob:none\" --recurse-submodules=no {originName} {head}", MetricTags.CIVisibilityCommands.Unshallow); - } - - if (gitUnshallowOutput is null || gitUnshallowOutput.ExitCode != 0) - { - // *** - // It could be that the CI is working on a detached HEAD or maybe branch tracking hasn’t been set up. - // In that case, this command will also fail, and we will finally fallback to we just unshallow all the things: - // `git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName)` - // *** - - // originName = git config --default origin --get clone.defaultRemoteName - // git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) - Log.Information("ITR: Previous unshallow command failed, refetching data with fallback 2 for {OriginName}", originName); - GitCommandHelper.RunGitCommand(_workingDirectory, $"fetch --shallow-since=\"1 month ago\" --update-shallow --filter=\"blob:none\" --recurse-submodules=no {originName}", MetricTags.CIVisibilityCommands.Unshallow); - } - } - } - catch (Exception ex) - { - Log.Error(ex, "Error detecting and reconfiguring git repository for shallow clone."); - } - - var commitsData = await GetCommitsAsync().ConfigureAwait(false); - if (!commitsData.IsOk) - { - return 0; - } - - return await SendObjectsPackFileAsync(commitsData.LocalCommits[0], commitsData.MissingCommits, commitsData.RemoteCommits).ConfigureAwait(false); - } - - public async Task GetSettingsAsync(bool skipFrameworkInfo = false) - { - Log.Debug("ITR: Getting settings..."); - var framework = FrameworkDescription.Instance; - if (string.IsNullOrEmpty(_repositoryUrl)) - { - Log.Warning("ITR: 'git config --get remote.origin.url' command returned null or empty"); - return default; - } - - if (string.IsNullOrEmpty(_branchName)) - { - Log.Warning("ITR: 'git branch --show-current' command returned null or empty"); - return default; - } - - if (string.IsNullOrEmpty(_commitSha)) - { - Log.Warning("ITR: 'git rev-parse HEAD' command returned null or empty"); - return default; - } - - var query = new DataEnvelope>( - new Data( - _commitSha, - SettingsType, - new SettingsQuery( - _serviceName, - _environment, - _repositoryUrl, - _branchName, - _commitSha, - new TestsConfigurations( - framework.OSPlatform, - CIVisibility.GetOperatingSystemVersion(), - framework.OSArchitecture, - skipFrameworkInfo ? null : framework.Name, - skipFrameworkInfo ? null : framework.ProductVersion, - skipFrameworkInfo ? null : framework.ProcessArchitecture, - _customConfigurations))), - default); - var jsonQuery = JsonConvert.SerializeObject(query, SerializerSettings); - var jsonQueryBytes = Encoding.UTF8.GetBytes(jsonQuery); - Log.Debug("ITR: Settings.JSON RQ = {Json}", jsonQuery); - - return await WithRetries(InternalGetSettingsAsync, jsonQueryBytes, MaxRetries).ConfigureAwait(false); - - async Task InternalGetSettingsAsync(byte[] state, bool finalTry) - { - var sw = Stopwatch.StartNew(); - try - { - // We currently always send the request uncompressed - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSettings(MetricTags.CIVisibilityRequestCompressed.Uncompressed); - var request = _apiRequestFactory.Create(_settingsUrl); - SetRequestHeader(request); - - if (Log.IsEnabled(LogEventLevel.Debug)) - { - Log.Debug("ITR: Getting settings from: {Url}", _settingsUrl.ToString()); - } - - string? responseContent; - try - { - using var response = await request.PostAsync(new ArraySegment(state), MimeTypes.Json).ConfigureAwait(false); - responseContent = await response.ReadAsStringAsync().ConfigureAwait(false); - if (TelemetryHelper.GetErrorTypeFromStatusCode(response.StatusCode) is { } errorType) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSettingsErrors(errorType); - } - - CheckResponseStatusCode(response, responseContent, finalTry); - } - catch (Exception ex) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSettingsErrors(MetricTags.CIVisibilityErrorType.Network); - Log.Error(ex, "ITR: Get settings request failed."); - throw; - } - - Log.Debug("ITR: Settings.JSON RS = {Json}", responseContent); - if (string.IsNullOrEmpty(responseContent)) - { - return default; - } - - var deserializedResult = JsonConvert.DeserializeObject?>>(responseContent); - var settingsResponse = deserializedResult.Data?.Attributes ?? default; - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSettingsResponse(settingsResponse switch - { - { CodeCoverage: true, TestsSkipping: true, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipEnabled_AtrDisabled, - { CodeCoverage: true, TestsSkipping: false, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipDisabled_AtrDisabled, - { CodeCoverage: false, TestsSkipping: true, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipEnabled_AtrDisabled, - { CodeCoverage: false, TestsSkipping: false, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipDisabled_EFDEnabled_AtrDisabled, - { CodeCoverage: true, TestsSkipping: true, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipEnabled_EFDEnabled_AtrDisabled, - { CodeCoverage: true, TestsSkipping: false, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipDisabled_EFDEnabled_AtrDisabled, - { CodeCoverage: false, TestsSkipping: true, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipEnabled_EFDEnabled_AtrDisabled, - { CodeCoverage: true, TestsSkipping: true, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipEnabled_AtrEnabled, - { CodeCoverage: true, TestsSkipping: false, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipDisabled_AtrEnabled, - { CodeCoverage: false, TestsSkipping: true, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipEnabled_AtrEnabled, - { CodeCoverage: false, TestsSkipping: false, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipDisabled_EFDEnabled_AtrEnabled, - { CodeCoverage: true, TestsSkipping: true, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipEnabled_EFDEnabled_AtrEnabled, - { CodeCoverage: true, TestsSkipping: false, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipDisabled_EFDEnabled_AtrEnabled, - { CodeCoverage: false, TestsSkipping: true, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipEnabled_EFDEnabled_AtrEnabled, - _ => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipDisabled_AtrDisabled, - }); - return settingsResponse; - } - finally - { - TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsSettingsMs(sw.Elapsed.TotalMilliseconds); - } - } - } - - public async Task GetSkippableTestsAsync() - { - Log.Debug("ITR: Getting skippable tests..."); - var framework = FrameworkDescription.Instance; - if (string.IsNullOrEmpty(_repositoryUrl)) - { - Log.Warning("ITR: 'git config --get remote.origin.url' command returned null or empty"); - return new SkippableTestsResponse(); - } - - if (string.IsNullOrEmpty(_commitSha)) - { - Log.Warning("ITR: 'git rev-parse HEAD' command returned null or empty"); - return default; - } - - var query = new DataEnvelope>( - new Data( - default, - TestParamsType, - new SkippableTestsQuery( - _serviceName, - _environment, - _repositoryUrl, - _commitSha, - new TestsConfigurations( - framework.OSPlatform, - CIVisibility.GetOperatingSystemVersion(), - framework.OSArchitecture, - framework.Name, - framework.ProductVersion, - framework.ProcessArchitecture, - _customConfigurations))), - default); - var jsonQuery = JsonConvert.SerializeObject(query, SerializerSettings); - var jsonQueryBytes = Encoding.UTF8.GetBytes(jsonQuery); - Log.Debug("ITR: Skippable.JSON RQ = {Json}", jsonQuery); - - return await WithRetries(InternalGetSkippableTestsAsync, jsonQueryBytes, MaxRetries).ConfigureAwait(false); - - async Task InternalGetSkippableTestsAsync(byte[] state, bool finalTry) - { - var sw = Stopwatch.StartNew(); - try - { - // We currently always send the request uncompressed - TelemetryFactory.Metrics.RecordCountCIVisibilityITRSkippableTestsRequest(MetricTags.CIVisibilityRequestCompressed.Uncompressed); - var request = _apiRequestFactory.Create(_skippableTestsUrl); - SetRequestHeader(request); - - if (Log.IsEnabled(LogEventLevel.Debug)) - { - Log.Debug("ITR: Searching skippable tests from: {Url}", _skippableTestsUrl.ToString()); - } - - string? responseContent; - try - { - using var response = await request.PostAsync(new ArraySegment(state), MimeTypes.Json).ConfigureAwait(false); - // TODO: Check for compressed responses - if we received one, currently these are not handled and would throw when we attempt to deserialize - responseContent = await response.ReadAsStringAsync().ConfigureAwait(false); - TelemetryFactory.Metrics.RecordDistributionCIVisibilityITRSkippableTestsResponseBytes(MetricTags.CIVisibilityResponseCompressed.Uncompressed, Encoding.UTF8.GetByteCount(responseContent ?? string.Empty)); - if (TelemetryHelper.GetErrorTypeFromStatusCode(response.StatusCode) is { } errorType) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityITRSkippableTestsRequestErrors(errorType); - } - - CheckResponseStatusCode(response, responseContent, finalTry); - } - catch (Exception ex) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityITRSkippableTestsRequestErrors(MetricTags.CIVisibilityErrorType.Network); - Log.Error(ex, "ITR: Get skippable tests request failed."); - throw; - } - - Log.Debug("ITR: Skippable.JSON RS = {Json}", responseContent); - if (string.IsNullOrEmpty(responseContent)) - { - return new SkippableTestsResponse(); - } - - var deserializedResult = JsonConvert.DeserializeObject>>(responseContent!); - if (deserializedResult.Data is null || deserializedResult.Data.Length == 0) - { - return new SkippableTestsResponse(deserializedResult.Meta?.CorrelationId, []); - } - - var testAttributes = new List(deserializedResult.Data.Length); - var customConfigurations = _customConfigurations; - for (var i = 0; i < deserializedResult.Data.Length; i++) - { - var includeItem = true; - var item = deserializedResult.Data[i].Attributes; - if (item.Configurations?.Custom is { } itemCustomConfiguration) - { - if (customConfigurations is null) - { - continue; - } - - foreach (var rsCustomConfigurationItem in itemCustomConfiguration) - { - if (!customConfigurations.TryGetValue(rsCustomConfigurationItem.Key, out var customConfigValue) || - rsCustomConfigurationItem.Value != customConfigValue) - { - includeItem = false; - break; - } - } - } - - if (includeItem) - { - testAttributes.Add(item); - } - } - - if (Log.IsEnabled(LogEventLevel.Debug) && deserializedResult.Data.Length != testAttributes.Count) - { - Log.Debug("ITR: Skippable.JSON Filtered = {Json}", JsonConvert.SerializeObject(testAttributes)); - } - - var totalSkippableTests = testAttributes.ToArray(); - TelemetryFactory.Metrics.RecordCountCIVisibilityITRSkippableTestsResponseTests(totalSkippableTests.Length); - return new SkippableTestsResponse(deserializedResult.Meta?.CorrelationId, totalSkippableTests); - } - finally - { - TelemetryFactory.Metrics.RecordDistributionCIVisibilityITRSkippableTestsRequestMs(sw.Elapsed.TotalMilliseconds); - } - } - } - - public async Task SendObjectsPackFileAsync(string commitSha, string[]? commitsToInclude, string[]? commitsToExclude) - { - Log.Debug("ITR: Packing and sending delta of commits and tree objects..."); - - var packFilesObject = GetObjectsPackFileFromWorkingDirectory(commitsToInclude, commitsToExclude); - if (packFilesObject.Files.Length == 0) - { - return 0; - } - - if (string.IsNullOrEmpty(_repositoryUrl)) - { - Log.Warning("ITR: 'git config --get remote.origin.url' command returned null or empty"); - return 0; - } - - var jsonPushedSha = JsonConvert.SerializeObject(new DataEnvelope>(new Data(commitSha, CommitType, default), _repositoryUrl), SerializerSettings); - Log.Debug("ITR: ObjPack.JSON RQ = {Json}", jsonPushedSha); - var jsonPushedShaBytes = Encoding.UTF8.GetBytes(jsonPushedSha); - - TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsObjectsPackFiles(packFilesObject.Files.Length); - long totalUploadSize = 0; - foreach (var packFile in packFilesObject.Files) - { - if (!Directory.Exists(Path.GetDirectoryName(packFile)) || !File.Exists(packFile)) - { - // Pack files must be sent in order, if a pack file is missing, we stop the upload of the rest of the pack files - // Previous pack files will enrich the backend with some of the data. - Log.Error("ITR: Pack file '{PackFile}' is missing, cancelling upload.", packFile); - break; - } - - // Send PackFile content - Log.Information("ITR: Sending {PackFile}", packFile); - totalUploadSize += await WithRetries(InternalSendObjectsPackFileAsync, packFile, MaxRetries).ConfigureAwait(false); - - // Delete temporal pack file - try - { - File.Delete(packFile); - } - catch (Exception ex) - { - Log.Warning(ex, "ITR: Error deleting pack file: '{PackFile}'", packFile); - } - } - - TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsObjectsPackBytes(totalUploadSize); - - // Delete temporary folder after the upload - if (!string.IsNullOrEmpty(packFilesObject.TemporaryFolder)) - { - try - { - Directory.Delete(packFilesObject.TemporaryFolder, true); - } - catch (Exception ex) - { - Log.Warning(ex, "ITR: Error deleting temporary folder: '{TemporaryFolder}'", packFilesObject.TemporaryFolder); - } - } - - Log.Information("ITR: Total pack file upload: {TotalUploadSize} bytes", totalUploadSize); - return totalUploadSize; - - async Task InternalSendObjectsPackFileAsync(string packFile, bool finalTry) - { - var sw = Stopwatch.StartNew(); - try - { - // We currently always send the request uncompressed - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsObjectsPack(MetricTags.CIVisibilityRequestCompressed.Uncompressed); - var request = _apiRequestFactory.Create(_packFileUrl); - SetRequestHeader(request); - - using var fileStream = File.Open(packFile, FileMode.Open, FileAccess.Read, FileShare.Read); - - try - { - using var response = await request.PostAsync([ - new MultipartFormItem("pushedSha", MimeTypes.Json, null, new ArraySegment(jsonPushedShaBytes)), - new MultipartFormItem("packfile", "application/octet-stream", null, fileStream)]) - .ConfigureAwait(false); - var responseContent = await response.ReadAsStringAsync().ConfigureAwait(false); - if (TelemetryHelper.GetErrorTypeFromStatusCode(response.StatusCode) is { } errorType) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsObjectsPackErrors(errorType); - } - - CheckResponseStatusCode(response, responseContent, finalTry); - } - catch (Exception ex) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSearchCommitsErrors(MetricTags.CIVisibilityErrorType.Network); - Log.Error(ex, "ITR: Send object pack file request failed."); - throw; - } - - return new FileInfo(packFile).Length; - } - finally - { - TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsObjectsPackMs(sw.Elapsed.TotalMilliseconds); - } - } - } - - public async Task GetEarlyFlakeDetectionTestsAsync() - { - Log.Debug("ITR: Getting early flake detection tests..."); - var framework = FrameworkDescription.Instance; - if (string.IsNullOrEmpty(_repositoryUrl)) - { - Log.Warning("ITR: 'git config --get remote.origin.url' command returned null or empty"); - return default; - } - - if (string.IsNullOrEmpty(_commitSha)) - { - Log.Warning("ITR: 'git rev-parse HEAD' command returned null or empty"); - return default; - } - - var query = new DataEnvelope>( - new Data( - _commitSha, - EarlyFlakeDetectionRequestType, - new EarlyFlakeDetectionQuery( - _serviceName, - _environment, - _repositoryUrl, - new TestsConfigurations( - framework.OSPlatform, - CIVisibility.GetOperatingSystemVersion(), - framework.OSArchitecture, - framework.Name, - framework.ProductVersion, - framework.ProcessArchitecture, - _customConfigurations))), - default); - var jsonQuery = JsonConvert.SerializeObject(query, SerializerSettings); - var jsonQueryBytes = Encoding.UTF8.GetBytes(jsonQuery); - Log.Debug("ITR: Efd.JSON RQ = {Json}", jsonQuery); - - return await WithRetries(InternalGetEarlyFlakeDetectionTestsAsync, jsonQueryBytes, MaxRetries).ConfigureAwait(false); - - async Task InternalGetEarlyFlakeDetectionTestsAsync(byte[] state, bool finalTry) - { - var sw = Stopwatch.StartNew(); - try - { - // We currently always send the request uncompressed - TelemetryFactory.Metrics.RecordCountCIVisibilityEarlyFlakeDetectionRequest(MetricTags.CIVisibilityRequestCompressed.Uncompressed); - var request = _apiRequestFactory.Create(_earlyFlakeDetectionTestsUrl); - SetRequestHeader(request); - - if (Log.IsEnabled(LogEventLevel.Debug)) - { - Log.Debug("ITR: Getting early flake detection tests from: {Url}", _earlyFlakeDetectionTestsUrl.ToString()); - } - - string? responseContent; - try - { - using var response = await request.PostAsync(new ArraySegment(state), MimeTypes.Json).ConfigureAwait(false); - responseContent = await response.ReadAsStringAsync().ConfigureAwait(false); - if (TelemetryHelper.GetErrorTypeFromStatusCode(response.StatusCode) is { } errorType) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityEarlyFlakeDetectionRequestErrors(errorType); - } - - CheckResponseStatusCode(response, responseContent, finalTry); - try - { - if (response.ContentLength is { } contentLength and > 0) - { - // TODO: Check for compressed responses - currently these are not handled and will throw when we attempt to deserialize - TelemetryFactory.Metrics.RecordDistributionCIVisibilityEarlyFlakeDetectionResponseBytes(MetricTags.CIVisibilityResponseCompressed.Uncompressed, contentLength); - } - } - catch - { - // If calling ContentLength throws we just ignore it - } - } - catch (Exception ex) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityEarlyFlakeDetectionRequestErrors(MetricTags.CIVisibilityErrorType.Network); - Log.Error(ex, "ITR: Early flake detection tests request failed."); - throw; - } - - Log.Debug("ITR: Efd.JSON RS = {Json}", responseContent); - if (string.IsNullOrEmpty(responseContent)) - { - return default; - } - - var deserializedResult = JsonConvert.DeserializeObject?>>(responseContent); - var finalResponse = deserializedResult.Data?.Attributes ?? default; - - // Count the number of tests for telemetry - var testsCount = 0; - if (finalResponse.Tests is { Count: > 0 } modulesDictionary) - { - foreach (var suitesDictionary in modulesDictionary.Values) - { - if (suitesDictionary?.Count > 0) - { - foreach (var testsArray in suitesDictionary.Values) - { - testsCount += testsArray?.Length ?? 0; - } - } - } - } - - TelemetryFactory.Metrics.RecordDistributionCIVisibilityEarlyFlakeDetectionResponseTests(testsCount); - return finalResponse; - } - finally - { - TelemetryFactory.Metrics.RecordDistributionCIVisibilityEarlyFlakeDetectionRequestMs(sw.Elapsed.TotalMilliseconds); - } - } - } - - public async Task GetImpactedTestsDetectionFilesAsync() - { - Log.Debug("ITR: Getting impacted tests detection modified files..."); - var framework = FrameworkDescription.Instance; - - if (string.IsNullOrEmpty(_repositoryUrl)) - { - Log.Warning("ITR: 'git config --get remote.origin.url' command returned null or empty"); - return default; - } - - if (string.IsNullOrEmpty(_commitSha)) - { - Log.Warning("ITR: 'git rev-parse HEAD' command returned null or empty"); - return default; - } - - var query = new DataEnvelope>( - new Data( - _commitSha, - ImpactedTestsDetectionRequestType, - new ImpactedTestsDetectionQuery( - _serviceName, - _environment, - _repositoryUrl, - _branchName, - _commitSha)), - default); - var jsonQuery = JsonConvert.SerializeObject(query, SerializerSettings); - var jsonQueryBytes = Encoding.UTF8.GetBytes(jsonQuery); - Log.Debug("ITR: Efd.JSON RQ = {Json}", jsonQuery); - - return await WithRetries(InternalGetImpactedTestsDetectionFilesAsync, jsonQueryBytes, MaxRetries).ConfigureAwait(false); - - async Task InternalGetImpactedTestsDetectionFilesAsync(byte[] state, bool finalTry) - { - var sw = Stopwatch.StartNew(); - try - { - // We currently always send the request uncompressed - TelemetryFactory.Metrics.RecordCountCIVisibilityImpactedTestsDetectionRequest(MetricTags.CIVisibilityRequestCompressed.Uncompressed); - var request = _apiRequestFactory.Create(_impactedTestsDetectionTestsUrl); - SetRequestHeader(request); - - if (Log.IsEnabled(LogEventLevel.Debug)) - { - Log.Debug("ITR: Getting Impacted test file diffs: {Url}", _impactedTestsDetectionTestsUrl.ToString()); - } - - string? responseContent; - try - { - using var response = await request.PostAsync(new ArraySegment(state), MimeTypes.Json).ConfigureAwait(false); - responseContent = await response.ReadAsStringAsync().ConfigureAwait(false); - if (TelemetryHelper.GetErrorTypeFromStatusCode(response.StatusCode) is { } errorType) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityImpactedTestsDetectionRequestErrors(errorType); - } - - CheckResponseStatusCode(response, responseContent, finalTry); - try - { - if (response.ContentLength is { } contentLength and > 0) - { - // TODO: Check for compressed responses - currently these are not handled and will throw when we attempt to deserialize - TelemetryFactory.Metrics.RecordDistributionCIVisibilityImpactedTestsDetectionResponseBytes(MetricTags.CIVisibilityResponseCompressed.Uncompressed, contentLength); - } - } - catch - { - // If calling ContentLength throws we just ignore it - } - } - catch (Exception ex) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityImpactedTestsDetectionRequestErrors(MetricTags.CIVisibilityErrorType.Network); - Log.Error(ex, "ITR: Impacted tests file diffs request failed."); - throw; - } - - Log.Debug("ITR: Efd.JSON RS = {Json}", responseContent); - if (string.IsNullOrEmpty(responseContent)) - { - return default; - } - - var deserializedResult = JsonConvert.DeserializeObject?>>(responseContent); - var finalResponse = deserializedResult.Data?.Attributes ?? default; - - // Count the number of tests for telemetry - var filesCount = 0; - if (finalResponse.Files is { Length: > 0 } files) - { - filesCount = files.Length; - } - - TelemetryFactory.Metrics.RecordDistributionCIVisibilityImpactedTestsDetectionResponseFiles(filesCount); - return finalResponse; - } - finally - { - TelemetryFactory.Metrics.RecordDistributionCIVisibilityImpactedTestsDetectionRequestMs(sw.Elapsed.TotalMilliseconds); - } - } - } - - private async Task GetCommitsAsync() - { - var gitLogOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "log --format=%H -n 1000 --since=\"1 month ago\"", MetricTags.CIVisibilityCommands.GetLocalCommits); - if (gitLogOutput is null) - { - Log.Warning("ITR: 'git log...' command is null"); - return new SearchCommitResponse(null, null, false); - } - - var localCommits = gitLogOutput.Output.Split(["\n"], StringSplitOptions.RemoveEmptyEntries); - if (localCommits.Length == 0) - { - Log.Debug("ITR: Local commits not found. (since 1 month ago)"); - return new SearchCommitResponse(null, null, false); - } - - Log.Debug("ITR: Local commits = {Count}", localCommits.Length); - var remoteCommitsData = await SearchCommitAsync(localCommits).ConfigureAwait(false); - return new SearchCommitResponse(localCommits, remoteCommitsData, true); - - async Task SearchCommitAsync(string[]? commits) - { - if (commits is null) - { - return []; - } - - Log.Debug("ITR: Searching commits..."); - - Data[] commitRequests; - if (commits.Length == 0) - { - commitRequests = []; - } - else - { - commitRequests = new Data[commits.Length]; - for (var i = 0; i < commits.Length; i++) - { - commitRequests[i] = new Data(commits[i], CommitType, null); - } - } - - var jsonPushedSha = JsonConvert.SerializeObject(new DataArrayEnvelope>(commitRequests, _repositoryUrl), SerializerSettings); - Log.Debug("ITR: Commits.JSON RQ = {Json}", jsonPushedSha); - var jsonPushedShaBytes = Encoding.UTF8.GetBytes(jsonPushedSha); - - return await WithRetries(InternalSearchCommitAsync, jsonPushedShaBytes, MaxRetries).ConfigureAwait(false); - - async Task InternalSearchCommitAsync(byte[] state, bool finalTry) - { - var sw = Stopwatch.StartNew(); - try - { - // We currently always send the request uncompressed - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSearchCommits(MetricTags.CIVisibilityRequestCompressed.Uncompressed); - var request = _apiRequestFactory.Create(_searchCommitsUrl); - SetRequestHeader(request); - - if (Log.IsEnabled(LogEventLevel.Debug)) - { - Log.Debug("ITR: Searching commits from: {Url}", _searchCommitsUrl.ToString()); - } - - string? responseContent; - try - { - using var response = await request.PostAsync(new ArraySegment(state), MimeTypes.Json).ConfigureAwait(false); - responseContent = await response.ReadAsStringAsync().ConfigureAwait(false); - if (TelemetryHelper.GetErrorTypeFromStatusCode(response.StatusCode) is { } errorType) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSearchCommitsErrors(errorType); - } - - CheckResponseStatusCode(response, responseContent, finalTry); - } - catch (Exception ex) - { - TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSearchCommitsErrors(MetricTags.CIVisibilityErrorType.Network); - Log.Error(ex, "ITR: Search commit request failed."); - throw; - } - - Log.Debug("ITR: Commits.JSON RS = {Json}", responseContent); - if (string.IsNullOrEmpty(responseContent)) - { - return []; - } - - var deserializedResult = JsonConvert.DeserializeObject>>(responseContent); - if (deserializedResult.Data is null || deserializedResult.Data.Length == 0) - { - return []; - } - - var stringArray = new string[deserializedResult.Data.Length]; - for (var i = 0; i < deserializedResult.Data.Length; i++) - { - var value = deserializedResult.Data[i].Id; - if (value is not null) - { - if (ShaRegex.Matches(value).Count != 1) - { - ThrowHelper.ThrowException($"The value '{value}' is not a valid Sha."); - } - - stringArray[i] = value; - } - } - - return stringArray; - } - finally - { - TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsSearchCommitsMs(MetricTags.CIVisibilityResponseCompressed.Uncompressed, sw.Elapsed.TotalMilliseconds); - } - } - } - } - - private ObjectPackFilesResult GetObjectsPackFileFromWorkingDirectory(string[]? commitsToInclude, string[]? commitsToExclude) - { - Log.Debug("ITR: Getting objects..."); - commitsToInclude ??= []; - commitsToExclude ??= []; - var temporaryFolder = string.Empty; - var temporaryPath = Path.GetTempFileName(); - - var getObjectsArguments = "rev-list --objects --no-object-names --filter=blob:none --since=\"1 month ago\" HEAD " + string.Join(" ", commitsToExclude.Select(c => "^" + c)) + " " + string.Join(" ", commitsToInclude); - var getObjectsCommand = GitCommandHelper.RunGitCommand(_workingDirectory, getObjectsArguments, MetricTags.CIVisibilityCommands.GetObjects); - if (string.IsNullOrEmpty(getObjectsCommand?.Output)) - { - // If not objects has been returned we skip the pack + upload. - Log.Debug("ITR: No objects were returned from the git rev-list command."); - return new ObjectPackFilesResult([], temporaryFolder); - } - - // Sanitize object list (on some cases we get a "fatal: expected object ID, got garbage" error because the object list has invalid escape chars) - var objectsOutput = getObjectsCommand!.Output; - var matches = ShaRegex.Matches(objectsOutput); - var lstObjectsSha = new List(matches.Count); - foreach (Match? match in matches) - { - if (match is not null) - { - lstObjectsSha.Add(match.Value); - } - } - - if (lstObjectsSha.Count == 0) - { - // If not objects has been returned we skip the pack + upload. - Log.Debug("ITR: No valid objects were returned from the git rev-list command."); - return new ObjectPackFilesResult([], temporaryFolder); - } - - objectsOutput = string.Join("\n", lstObjectsSha) + "\n"; - - Log.Debug("ITR: Packing {NumObjects} objects...", lstObjectsSha.Count); - var getPacksArguments = $"pack-objects --compression=9 --max-pack-size={MaxPackFileSizeInMb}m \"{temporaryPath}\""; - var packObjectsResultCommand = GitCommandHelper.RunGitCommand(_workingDirectory, getPacksArguments, MetricTags.CIVisibilityCommands.PackObjects, objectsOutput); - if (packObjectsResultCommand is null) - { - Log.Warning("ITR: 'git pack-objects...' command is null"); - return new ObjectPackFilesResult([], temporaryFolder); - } - - if (packObjectsResultCommand.ExitCode != 0) - { - if (packObjectsResultCommand.Error.IndexOf("Cross-device", StringComparison.OrdinalIgnoreCase) != -1) - { - // Git can throw a cross device error if the temporal folder is in a different drive than the .git folder (eg. symbolic link) - // to handle this edge case, we create a temporal folder inside the current folder. - - Log.Warning("ITR: 'git pack-objects...' returned a cross-device error, retrying using a local temporal folder."); - temporaryFolder = Path.Combine(Environment.CurrentDirectory, ".git_tmp"); - if (!Directory.Exists(temporaryFolder)) - { - Directory.CreateDirectory(temporaryFolder); - } - - temporaryPath = Path.Combine(temporaryFolder, Path.GetFileName(temporaryPath)); - getPacksArguments = $"pack-objects --compression=9 --max-pack-size={MaxPackFileSizeInMb}m \"{temporaryPath}\""; - packObjectsResultCommand = GitCommandHelper.RunGitCommand(_workingDirectory, getPacksArguments, MetricTags.CIVisibilityCommands.PackObjects, getObjectsCommand!.Output); - if (packObjectsResultCommand is null) - { - Log.Warning("ITR: 'git pack-objects...' command is null"); - return new ObjectPackFilesResult([], temporaryFolder); - } - } - - if (packObjectsResultCommand.ExitCode != 0) - { - Log.Warning("ITR: 'git pack-objects...' command error: {Stderr}", packObjectsResultCommand.Error); - } - } - - var packObjectsSha = packObjectsResultCommand.Output.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); - - // We try to return an array with the path in the same order as has been returned by the git command. - var tempFolder = Path.GetDirectoryName(temporaryPath) ?? string.Empty; - var tempFile = Path.GetFileName(temporaryPath); - var lstFiles = new List(packObjectsSha.Length); - foreach (var pObjSha in packObjectsSha) - { - var file = Path.Combine(tempFolder, tempFile + "-" + pObjSha + ".pack"); - if (File.Exists(file)) - { - lstFiles.Add(file); - } - else - { - Log.Warning("ITR: The file '{PackFile}' doesn't exist.", file); - } - } - - return new ObjectPackFilesResult(lstFiles.ToArray(), temporaryFolder); - } - - private void SetRequestHeader(IApiRequest request) - { - request.AddHeader(HttpHeaderNames.TraceId, _id); - request.AddHeader(HttpHeaderNames.ParentId, _id); - if (_eventPlatformProxySupport is EventPlatformProxySupport.V2 or EventPlatformProxySupport.V4) - { - request.AddHeader(EvpSubdomainHeader, "api"); - } - else - { - request.AddHeader(ApiKeyHeader, _settings.ApiKey); - } - } - - private void CheckResponseStatusCode(IApiResponse response, string? responseContent, bool finalTry) - { - // Check if the rate limit header was received. - if (response.StatusCode == 429 && - response.GetHeader("x-ratelimit-reset") is { } strRateLimitDurationInSeconds && - int.TryParse(strRateLimitDurationInSeconds, out var rateLimitDurationInSeconds)) - { - if (rateLimitDurationInSeconds > 30) - { - // If 'x-ratelimit-reset' is > 30 seconds we cancel the request. - throw new RateLimitException(); - } - - throw new RateLimitException(rateLimitDurationInSeconds); - } - - if (response.StatusCode is < 200 or >= 300 && response.StatusCode != 404 && response.StatusCode != 502) - { - if (finalTry) - { - Log.Error("ITR: Request failed with status code {StatusCode} and message: {ResponseContent}", response.StatusCode, responseContent ?? string.Empty); - } - - throw new WebException($"Status: {response.StatusCode}, Content: {responseContent}"); - } - } - - private async Task WithRetries(Func> sendDelegate, TState state, int numOfRetries) - { - var retryCount = 1; - var sleepDuration = 100; // in milliseconds - - while (true) - { - T response = default!; - ExceptionDispatchInfo? exceptionDispatchInfo = null; - var isFinalTry = retryCount >= numOfRetries; - - try - { - response = await sendDelegate(state, isFinalTry).ConfigureAwait(false); - } - catch (Exception ex) - { - exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex); - } - - // Error handling block - if (exceptionDispatchInfo is not null) - { - var sourceException = exceptionDispatchInfo.SourceException; - - if (isFinalTry || - sourceException is RateLimitException { DelayTimeInSeconds: null } || - sourceException is DirectoryNotFoundException || - sourceException is FileNotFoundException) - { - // stop retrying - Log.Error(sourceException, "ITR: An error occurred while sending intelligent test runner data after {Retries} retries.", retryCount); - exceptionDispatchInfo.Throw(); - } - - // Before retry - var isSocketException = false; - var innerException = sourceException; - while (innerException != null) - { - if (innerException is SocketException) - { - isSocketException = true; - break; - } - - innerException = innerException.InnerException; - } - - if (isSocketException) - { - Log.Debug(sourceException, "ITR: Unable to communicate with the server"); - } - - if (sourceException is RateLimitException { DelayTimeInSeconds: { } delayTimeInSeconds }) - { - // Execute rate limit retry delay - await Task.Delay(TimeSpan.FromSeconds(delayTimeInSeconds)).ConfigureAwait(false); - } - else - { - // Execute retry delay - await Task.Delay(sleepDuration).ConfigureAwait(false); - sleepDuration *= 2; - } - - retryCount++; - continue; - } - - Log.Debug("ITR: Request was completed successfully."); - return response; - } - } - - private string GetRepositoryUrl() - { - if (CIEnvironmentValues.Instance.Repository is { Length: > 0 } repository) - { - return repository; - } - - var gitOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "config --get remote.origin.url", MetricTags.CIVisibilityCommands.GetRepository); - return gitOutput?.Output.Replace("\n", string.Empty) ?? string.Empty; - } - - private string GetBranchName() - { - if (CIEnvironmentValues.Instance.Branch is { Length: > 0 } branch) - { - return branch; - } - - var gitOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "branch --show-current", MetricTags.CIVisibilityCommands.GetBranch); - var res = gitOutput?.Output.Replace("\n", string.Empty) ?? string.Empty; - - if (string.IsNullOrEmpty(res)) - { - Log.Warning("ITR: empty branch indicates a detached head at commit {Commit}", _commitSha); - res = $"auto:git-detached-head"; - } - - return res; - } - - private string GetCommitSha() - { - var gitOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "rev-parse HEAD", MetricTags.CIVisibilityCommands.GetHead); - var gitSha = gitOutput?.Output.Replace("\n", string.Empty) ?? string.Empty; - if (string.IsNullOrEmpty(gitSha) && CIEnvironmentValues.Instance.Commit is { Length: > 0 } commitSha) - { - return commitSha; - } - - return gitSha; - } - - private readonly struct SearchCommitResponse - { - public readonly string[] LocalCommits; - public readonly string[] RemoteCommits; - public readonly bool IsOk; - - public SearchCommitResponse(string[]? localCommits, string[]? remoteCommits, bool isOk) - { - LocalCommits = localCommits ?? []; - RemoteCommits = remoteCommits ?? []; - IsOk = isOk; - } - - public bool HasCommits => LocalCommits.Length > 0; - - public string[] MissingCommits => LocalCommits.Except(RemoteCommits).ToArray(); - } - - private readonly struct DataEnvelope - { - [JsonProperty("data")] - public readonly T? Data; - - [JsonProperty("meta")] - public readonly Metadata? Meta; - - public DataEnvelope(T? data, string? repositoryUrl) - { - Data = data; - Meta = repositoryUrl is null ? default(Metadata?) : new Metadata(repositoryUrl); - } - } - - private readonly struct DataArrayEnvelope - { - [JsonProperty("data")] - public readonly T[] Data; - - [JsonProperty("meta")] - public readonly Metadata? Meta; - - public DataArrayEnvelope(T[] data, string? repositoryUrl) - { - Data = data; - Meta = repositoryUrl is null ? default(Metadata?) : new Metadata(repositoryUrl); - } - } - - private readonly struct Metadata - { - [JsonProperty("repository_url")] - public readonly string RepositoryUrl; - - [JsonProperty("correlation_id")] - public readonly string? CorrelationId; - - public Metadata(string repositoryUrl) - { - RepositoryUrl = repositoryUrl; - CorrelationId = null; - } - - public Metadata(string repositoryUrl, string? correlationId) - { - RepositoryUrl = repositoryUrl; - CorrelationId = correlationId; - } - } - - private readonly struct Data - { - [JsonProperty("id")] - public readonly string? Id; - - [JsonProperty("type")] - public readonly string Type; - - [JsonProperty("attributes")] - public readonly T? Attributes; - - public Data(string? id, string type, T? attributes) - { - Id = id; - Type = type; - Attributes = attributes; - } - } - - private readonly struct SkippableTestsQuery - { - [JsonProperty("service")] - public readonly string Service; - - [JsonProperty("env")] - public readonly string Environment; - - [JsonProperty("repository_url")] - public readonly string RepositoryUrl; - - [JsonProperty("sha")] - public readonly string Sha; - - [JsonProperty("configurations")] - public readonly TestsConfigurations? Configurations; - - public SkippableTestsQuery(string service, string environment, string repositoryUrl, string sha, TestsConfigurations? configurations) - { - Service = service; - Environment = environment; - RepositoryUrl = repositoryUrl; - Sha = sha; - Configurations = configurations; - } - } - - public readonly struct SkippableTestsResponse - { - public readonly string? CorrelationId; - public readonly SkippableTest[] Tests; - - public SkippableTestsResponse() - { - CorrelationId = null; - Tests = []; - } - - public SkippableTestsResponse(string? correlationId, SkippableTest[] tests) - { - CorrelationId = correlationId; - Tests = tests; - } - } - - private readonly struct SettingsQuery - { - [JsonProperty("service")] - public readonly string Service; - - [JsonProperty("env")] - public readonly string Environment; - - [JsonProperty("repository_url")] - public readonly string RepositoryUrl; - - [JsonProperty("branch")] - public readonly string Branch; - - [JsonProperty("sha")] - public readonly string Sha; - - [JsonProperty("configurations")] - public readonly TestsConfigurations Configurations; - - public SettingsQuery(string service, string environment, string repositoryUrl, string branch, string sha, TestsConfigurations configurations) - { - Service = service; - Environment = environment; - RepositoryUrl = repositoryUrl; - Branch = branch; - Sha = sha; - Configurations = configurations; - } - } - - public readonly struct SettingsResponse - { - [JsonProperty("code_coverage")] - public readonly bool? CodeCoverage; - - [JsonProperty("tests_skipping")] - public readonly bool? TestsSkipping; - - [JsonProperty("require_git")] - public readonly bool? RequireGit; - - [JsonProperty("impacted_tests_enabled")] - public readonly bool? ImpactedTestsEnabled; - - [JsonProperty("flaky_test_retries_enabled")] - public readonly bool? FlakyTestRetries; - - [JsonProperty("early_flake_detection")] - public readonly EarlyFlakeDetectionSettingsResponse EarlyFlakeDetection; - } - - public readonly struct EarlyFlakeDetectionSettingsResponse - { - [JsonProperty("enabled")] - public readonly bool? Enabled; - - [JsonProperty("slow_test_retries")] - public readonly SlowTestRetriesSettingsResponse SlowTestRetries; - - [JsonProperty("faulty_session_threshold")] - public readonly int? FaultySessionThreshold; - } - - public readonly struct SlowTestRetriesSettingsResponse - { - [JsonProperty("5s")] - public readonly int? FiveSeconds; - - [JsonProperty("10s")] - public readonly int? TenSeconds; - - [JsonProperty("30s")] - public readonly int? ThirtySeconds; - - [JsonProperty("5m")] - public readonly int? FiveMinutes; - } - - public readonly struct EarlyFlakeDetectionQuery - { - [JsonProperty("service")] - public readonly string Service; - - [JsonProperty("env")] - public readonly string Environment; - - [JsonProperty("repository_url")] - public readonly string RepositoryUrl; - - [JsonProperty("configurations")] - public readonly TestsConfigurations Configurations; - - public EarlyFlakeDetectionQuery(string service, string environment, string repositoryUrl, TestsConfigurations configurations) - { - Service = service; - Environment = environment; - RepositoryUrl = repositoryUrl; - Configurations = configurations; - } - } - - public readonly struct EarlyFlakeDetectionResponse - { - [JsonProperty("tests")] - public readonly EfdResponseModules? Tests; - - public class EfdResponseSuites : Dictionary - { - } - - public class EfdResponseModules : Dictionary - { - } - } - - public readonly struct ImpactedTestsDetectionQuery - { - [JsonProperty("service")] - public readonly string Service; - - [JsonProperty("env")] - public readonly string Environment; - - [JsonProperty("repository_url")] - public readonly string RepositoryUrl; - - [JsonProperty("branch")] - public readonly string Branch; - - [JsonProperty("sha")] - public readonly string Sha; - - public ImpactedTestsDetectionQuery(string service, string environment, string repositoryUrl, string branch, string sha) - { - Service = service; - Environment = environment; - RepositoryUrl = repositoryUrl; - Branch = branch; - Sha = sha; - } - } - - public readonly struct ImpactedTestsDetectionResponse - { - [JsonProperty("base_sha")] - public readonly string? BaseSha; - - [JsonProperty("files")] - public readonly string[]? Files; - } - - private class ObjectPackFilesResult - { - public ObjectPackFilesResult(string[] files, string temporaryFolder) - { - Files = files; - TemporaryFolder = temporaryFolder; - } - - public string[] Files { get; } - - public string TemporaryFolder { get; } - } - - private class RateLimitException : Exception - { - public RateLimitException() - : base("Server rate limiting response received. Cancelling request.") - { - DelayTimeInSeconds = null; - } - - public RateLimitException(int delayTimeInSeconds) - : base($"Server rate limiting response received. Waiting for {delayTimeInSeconds} seconds") - { - DelayTimeInSeconds = delayTimeInSeconds; - } - - public int? DelayTimeInSeconds { get; private set; } - } -} diff --git a/tracer/src/Datadog.Trace/Ci/Ipc/CircularChannel.Reader.cs b/tracer/src/Datadog.Trace/Ci/Ipc/CircularChannel.Reader.cs index 7f44e4beab1b..eb26224864a1 100644 --- a/tracer/src/Datadog.Trace/Ci/Ipc/CircularChannel.Reader.cs +++ b/tracer/src/Datadog.Trace/Ci/Ipc/CircularChannel.Reader.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.Threading; +using Datadog.Trace.Logging; using Datadog.Trace.VendoredMicrosoftCode.System.Buffers; namespace Datadog.Trace.Ci.Ipc; @@ -16,6 +17,7 @@ internal partial class CircularChannel { private class Reader : IChannelReader { + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); private readonly ManualResetEventSlim _pollingThreadFinishEvent; private readonly Thread _pollingThread; private readonly CircularChannel _channel; @@ -42,7 +44,7 @@ private void PollForMessages(object? state) } catch (Exception ex) { - CIVisibility.Log.Error(ex, "CircularChannel.Reader: Error while polling for messages (InternalPollForMessage)"); + Log.Error(ex, "CircularChannel.Reader: Error while polling for messages (InternalPollForMessage)"); } if (!_pollingThreadFinishEvent.IsSet) @@ -70,19 +72,19 @@ private void InternalPollForMessage() var hasHandle = _channel._mutex.WaitOne(_channel._settings.MutexTimeout); if (!hasHandle) { - CIVisibility.Log.Error("CircularChannel.Reader: Failed to acquire mutex within the time limit."); + Log.Error("CircularChannel.Reader: Failed to acquire mutex within the time limit."); return; } } catch (AbandonedMutexException ex) { - CIVisibility.Log.Error(ex, "CircularChannel.Reader: Mutex was abandoned."); + Log.Error(ex, "CircularChannel.Reader: Mutex was abandoned."); return; } catch (ObjectDisposedException ex) { // The mutex was disposed, nothing to do - CIVisibility.Log.Error(ex, "CircularChannel.Reader: Mutex has been disposed."); + Log.Error(ex, "CircularChannel.Reader: Mutex has been disposed."); return; } @@ -119,7 +121,7 @@ private void InternalPollForMessage() if (length + 2 > _channel.BufferBodySize) { // Handle error, reset pointers, or skip - CIVisibility.Log.Error("CircularChannel.Reader: Message size exceeds maximum allowed size."); + Log.Error("CircularChannel.Reader: Message size exceeds maximum allowed size."); break; } @@ -162,7 +164,7 @@ private void InternalPollForMessage() } catch (Exception ex) { - CIVisibility.Log.Error(ex, "CircularChannel.Reader: Error while polling for messages"); + Log.Error(ex, "CircularChannel.Reader: Error while polling for messages"); } finally { @@ -173,7 +175,7 @@ private void InternalPollForMessage() catch (ObjectDisposedException ex) { // The mutex was disposed, nothing to do - CIVisibility.Log.Error(ex, "CircularChannel.Reader: Mutex has been disposed."); + Log.Error(ex, "CircularChannel.Reader: Mutex has been disposed."); } } @@ -188,7 +190,7 @@ private void InternalPollForMessage() } catch (Exception ex) { - CIVisibility.Log.Error(ex, "CircularChannel.Reader: Error during message event handling."); + Log.Error(ex, "CircularChannel.Reader: Error during message event handling."); } finally { @@ -204,7 +206,7 @@ private void InternalPollForMessage() } catch (Exception ex) { - CIVisibility.Log.Error(ex, "CircularChannel.Reader: Error during message event handling."); + Log.Error(ex, "CircularChannel.Reader: Error during message event handling."); } finally { diff --git a/tracer/src/Datadog.Trace/Ci/Net/CachedTestOptimizationClient.cs b/tracer/src/Datadog.Trace/Ci/Net/CachedTestOptimizationClient.cs new file mode 100644 index 000000000000..13d31098c20e --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/CachedTestOptimizationClient.cs @@ -0,0 +1,62 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Threading.Tasks; + +namespace Datadog.Trace.Ci.Net; + +internal sealed class CachedTestOptimizationClient : ITestOptimizationClient +{ + private readonly ITestOptimizationClient _client; + private readonly Lazy> _settingsWithoutFrameworkInfo; + private readonly Lazy> _settingsWithFrameworkInfo; + private readonly Lazy> _earlyFlakeDetectionTests; + private readonly Lazy> _commits; + private readonly Lazy> _skippableTests; + private readonly Lazy> _impactedTestsDetectionFiles; + private readonly Lazy> _uploadRepositoryChanges; + + public CachedTestOptimizationClient(ITestOptimizationClient client) + { + _client = client; + _settingsWithoutFrameworkInfo = new(() => _client.GetSettingsAsync(true)); + _settingsWithFrameworkInfo = new(() => _client.GetSettingsAsync(false)); + _earlyFlakeDetectionTests = new(_client.GetEarlyFlakeDetectionTestsAsync); + _commits = new(_client.GetCommitsAsync); + _skippableTests = new(_client.GetSkippableTestsAsync); + _impactedTestsDetectionFiles = new(_client.GetImpactedTestsDetectionFilesAsync); + _uploadRepositoryChanges = new(_client.UploadRepositoryChangesAsync); + } + + public async Task GetSettingsAsync(bool skipFrameworkInfo = false) + { + if (skipFrameworkInfo) + { + return await _settingsWithoutFrameworkInfo.Value.ConfigureAwait(false); + } + + return await _settingsWithFrameworkInfo.Value.ConfigureAwait(false); + } + + public async Task GetEarlyFlakeDetectionTestsAsync() + => await _earlyFlakeDetectionTests.Value.ConfigureAwait(false); + + public async Task GetCommitsAsync() + => await _commits.Value.ConfigureAwait(false); + + public async Task GetSkippableTestsAsync() + => await _skippableTests.Value.ConfigureAwait(false); + + public async Task GetImpactedTestsDetectionFilesAsync() + => await _impactedTestsDetectionFiles.Value.ConfigureAwait(false); + + public async Task SendPackFilesAsync(string commitSha, string[]? commitsToInclude, string[]? commitsToExclude) + => await _client.SendPackFilesAsync(commitSha, commitsToInclude, commitsToExclude).ConfigureAwait(false); + + public async Task UploadRepositoryChangesAsync() + => await _uploadRepositoryChanges.Value.ConfigureAwait(false); +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/ITestOptimizationClient.cs b/tracer/src/Datadog.Trace/Ci/Net/ITestOptimizationClient.cs new file mode 100644 index 000000000000..b11c67bdff3c --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/ITestOptimizationClient.cs @@ -0,0 +1,26 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System.Threading.Tasks; + +namespace Datadog.Trace.Ci.Net; + +internal interface ITestOptimizationClient +{ + Task GetSettingsAsync(bool skipFrameworkInfo = false); + + Task GetEarlyFlakeDetectionTestsAsync(); + + Task GetCommitsAsync(); + + Task GetSkippableTestsAsync(); + + Task GetImpactedTestsDetectionFilesAsync(); + + Task SendPackFilesAsync(string commitSha, string[]? commitsToInclude, string[]? commitsToExclude); + + Task UploadRepositoryChangesAsync(); +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/NoopTestOptimizationClient.cs b/tracer/src/Datadog.Trace/Ci/Net/NoopTestOptimizationClient.cs new file mode 100644 index 000000000000..5cb442b96927 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/NoopTestOptimizationClient.cs @@ -0,0 +1,57 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System.Threading.Tasks; +using Datadog.Trace.Logging; + +namespace Datadog.Trace.Ci.Net; + +internal sealed class NoopTestOptimizationClient : ITestOptimizationClient +{ + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); + + public Task GetSettingsAsync(bool skipFrameworkInfo = false) + { + Log.Debug("NoopTestOptimizationClient: Getting settings..."); + return Task.FromResult(default); + } + + public Task GetEarlyFlakeDetectionTestsAsync() + { + Log.Debug("NoopTestOptimizationClient: Getting early flake detection tests..."); + return Task.FromResult(default); + } + + public Task GetCommitsAsync() + { + Log.Debug("NoopTestOptimizationClient: Getting commits..."); + return Task.FromResult(default); + } + + public Task GetSkippableTestsAsync() + { + Log.Debug("NoopTestOptimizationClient: Getting skippable tests..."); + return Task.FromResult(default); + } + + public Task GetImpactedTestsDetectionFilesAsync() + { + Log.Debug("NoopTestOptimizationClient: Getting impacted tests detection files..."); + return Task.FromResult(default); + } + + public Task SendPackFilesAsync(string commitSha, string[]? commitsToInclude, string[]? commitsToExclude) + { + Log.Debug("NoopTestOptimizationClient: Sending pack files..."); + return Task.FromResult(0); + } + + public Task UploadRepositoryChangesAsync() + { + Log.Debug("NoopTestOptimizationClient: Uploading repository changes..."); + return Task.FromResult(0); + } +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetCommitsAsync.cs b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetCommitsAsync.cs new file mode 100644 index 000000000000..cd683bc9886c --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetCommitsAsync.cs @@ -0,0 +1,140 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Linq; +using System.Threading.Tasks; +using Datadog.Trace.Ci.Telemetry; +using Datadog.Trace.Telemetry; +using Datadog.Trace.Telemetry.Metrics; +using Datadog.Trace.Vendors.Newtonsoft.Json; + +// ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable NotAccessedField.Local +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + +namespace Datadog.Trace.Ci.Net; + +internal sealed partial class TestOptimizationClient +{ + private const string SearchCommitsUrlPath = "api/v2/git/repository/search_commits"; + private const string CommitType = "commit"; + private Uri? _searchCommitsUrl; + + public async Task GetCommitsAsync() + { + var gitLogOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "log --format=%H -n 1000 --since=\"1 month ago\"", MetricTags.CIVisibilityCommands.GetLocalCommits); + if (gitLogOutput is null) + { + Log.Warning("TestOptimizationClient: 'git log...' command is null"); + return new SearchCommitResponse(null, null, false); + } + + var localCommits = gitLogOutput.Output.Split(["\n"], StringSplitOptions.RemoveEmptyEntries); + if (localCommits.Length == 0) + { + Log.Debug("TestOptimizationClient: Local commits not found. (since 1 month ago)"); + return new SearchCommitResponse(null, null, false); + } + + Log.Debug("TestOptimizationClient: Local commits = {Count}. Searching commits...", localCommits.Length); + + _searchCommitsUrl ??= GetUriFromPath(SearchCommitsUrlPath); + var commitRequests = new Data[localCommits.Length]; + for (var i = 0; i < localCommits.Length; i++) + { + commitRequests[i] = new Data(localCommits[i], CommitType, null); + } + + var jsonQuery = JsonConvert.SerializeObject(new DataArrayEnvelope>(commitRequests, _repositoryUrl), SerializerSettings); + Log.Debug("TestOptimizationClient: Commits.JSON RQ = {Json}", jsonQuery); + + string? queryResponse; + try + { + queryResponse = await SendJsonRequestAsync(_searchCommitsUrl, jsonQuery).ConfigureAwait(false); + } + catch (Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSearchCommitsErrors(MetricTags.CIVisibilityErrorType.Network); + Log.Error(ex, "TestOptimizationClient: Search commit request failed."); + throw; + } + + Log.Debug("TestOptimizationClient: Commits.JSON RS = {Json}", queryResponse); + if (string.IsNullOrEmpty(queryResponse)) + { + return new SearchCommitResponse(localCommits, null, false); + } + + var deserializedResult = JsonConvert.DeserializeObject>>(queryResponse); + if (deserializedResult.Data is null || deserializedResult.Data.Length == 0) + { + return new SearchCommitResponse(localCommits, null, true); + } + + var stringArray = new string[deserializedResult.Data.Length]; + for (var i = 0; i < deserializedResult.Data.Length; i++) + { + var value = deserializedResult.Data[i].Id; + if (value is not null) + { + if (ShaRegex.Matches(value).Count != 1) + { + ThrowHelper.ThrowException($"The value '{value}' is not a valid Sha."); + } + + stringArray[i] = value; + } + } + + return new SearchCommitResponse(localCommits, stringArray, true); + } + + private readonly struct SearchCommitsCallbacks : ICallbacks + { + public void OnBeforeSend() + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSearchCommits(MetricTags.CIVisibilityRequestCompressed.Uncompressed); + } + + public void OnStatusCodeReceived(int statusCode, int responseLength) + { + if (TelemetryHelper.GetErrorTypeFromStatusCode(statusCode) is { } errorType) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSearchCommitsErrors(errorType); + } + } + + public void OnError(Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSearchCommitsErrors(MetricTags.CIVisibilityErrorType.Network); + } + + public void OnAfterSend(double totalMs) + { + TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsSearchCommitsMs(MetricTags.CIVisibilityResponseCompressed.Uncompressed, totalMs); + } + } + + internal readonly struct SearchCommitResponse + { + public readonly string[] LocalCommits; + public readonly string[] RemoteCommits; + public readonly bool IsOk; + public readonly bool HasCommits; + public readonly string[] MissingCommits; + + public SearchCommitResponse(string[]? localCommits, string[]? remoteCommits, bool isOk) + { + LocalCommits = localCommits ?? []; + RemoteCommits = remoteCommits ?? []; + IsOk = isOk; + HasCommits = LocalCommits.Length > 0; + MissingCommits = LocalCommits.Except(RemoteCommits).ToArray(); + } + } +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetEarlyFlakeDetectionTestsAsync.cs b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetEarlyFlakeDetectionTestsAsync.cs new file mode 100644 index 000000000000..011a6e9330e6 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetEarlyFlakeDetectionTestsAsync.cs @@ -0,0 +1,150 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Datadog.Trace.Ci.Telemetry; +using Datadog.Trace.Telemetry; +using Datadog.Trace.Telemetry.Metrics; +using Datadog.Trace.Vendors.Newtonsoft.Json; + +// ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable NotAccessedField.Local +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + +namespace Datadog.Trace.Ci.Net; + +internal sealed partial class TestOptimizationClient +{ + private const string EfdTestsUrlPath = "api/v2/ci/libraries/tests"; + private const string EfdTestsType = "ci_app_libraries_tests_request"; + private Uri? _efdTestsUrl; + + public async Task GetEarlyFlakeDetectionTestsAsync() + { + Log.Debug("TestOptimizationClient: Getting early flake detection tests..."); + if (!EnsureRepositoryUrl() || !EnsureCommitSha()) + { + return default; + } + + _efdTestsUrl ??= GetUriFromPath(EfdTestsUrlPath); + var query = new DataEnvelope>( + new Data( + _commitSha, + EfdTestsType, + new EarlyFlakeDetectionQuery(_serviceName, _environment, _repositoryUrl, GetTestConfigurations())), + null); + + var jsonQuery = JsonConvert.SerializeObject(query, SerializerSettings); + Log.Debug("TestOptimizationClient: Efd.JSON RQ = {Json}", jsonQuery); + + string? queryResponse; + try + { + queryResponse = await SendJsonRequestAsync(_efdTestsUrl, jsonQuery).ConfigureAwait(false); + } + catch (Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityEarlyFlakeDetectionRequestErrors(MetricTags.CIVisibilityErrorType.Network); + Log.Error(ex, "TestOptimizationClient: Early flake detection tests request failed."); + throw; + } + + Log.Debug("TestOptimizationClient: Efd.JSON RS = {Json}", queryResponse); + if (string.IsNullOrEmpty(queryResponse)) + { + return default; + } + + var deserializedResult = JsonConvert.DeserializeObject?>>(queryResponse); + var finalResponse = deserializedResult.Data?.Attributes ?? default; + + // Count the number of tests for telemetry + var testsCount = 0; + if (finalResponse.Tests is { Count: > 0 } modulesDictionary) + { + foreach (var suitesDictionary in modulesDictionary.Values) + { + if (suitesDictionary?.Count > 0) + { + foreach (var testsArray in suitesDictionary.Values) + { + testsCount += testsArray?.Length ?? 0; + } + } + } + } + + TelemetryFactory.Metrics.RecordDistributionCIVisibilityEarlyFlakeDetectionResponseTests(testsCount); + return finalResponse; + } + + private readonly struct EfdCallbacks : ICallbacks + { + public void OnBeforeSend() + { + TelemetryFactory.Metrics.RecordCountCIVisibilityEarlyFlakeDetectionRequest(MetricTags.CIVisibilityRequestCompressed.Uncompressed); + } + + public void OnStatusCodeReceived(int statusCode, int responseLength) + { + TelemetryFactory.Metrics.RecordDistributionCIVisibilityEarlyFlakeDetectionResponseBytes(MetricTags.CIVisibilityResponseCompressed.Uncompressed, responseLength); + if (TelemetryHelper.GetErrorTypeFromStatusCode(statusCode) is { } errorType) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityEarlyFlakeDetectionRequestErrors(errorType); + } + } + + public void OnError(Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityEarlyFlakeDetectionRequestErrors(MetricTags.CIVisibilityErrorType.Network); + } + + public void OnAfterSend(double totalMs) + { + TelemetryFactory.Metrics.RecordDistributionCIVisibilityEarlyFlakeDetectionRequestMs(totalMs); + } + } + + private readonly struct EarlyFlakeDetectionQuery + { + [JsonProperty("service")] + public readonly string Service; + + [JsonProperty("env")] + public readonly string Environment; + + [JsonProperty("repository_url")] + public readonly string RepositoryUrl; + + [JsonProperty("configurations")] + public readonly TestsConfigurations Configurations; + + public EarlyFlakeDetectionQuery(string service, string environment, string repositoryUrl, TestsConfigurations configurations) + { + Service = service; + Environment = environment; + RepositoryUrl = repositoryUrl; + Configurations = configurations; + } + } + + public readonly struct EarlyFlakeDetectionResponse + { + [JsonProperty("tests")] + public readonly EfdResponseModules? Tests; + + public class EfdResponseSuites : Dictionary + { + } + + public class EfdResponseModules : Dictionary + { + } + } +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetImpactedTestsDetectionFilesAsync.cs b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetImpactedTestsDetectionFilesAsync.cs new file mode 100644 index 000000000000..ab5759721aef --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetImpactedTestsDetectionFilesAsync.cs @@ -0,0 +1,139 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Threading.Tasks; +using Datadog.Trace.Ci.Telemetry; +using Datadog.Trace.Telemetry; +using Datadog.Trace.Telemetry.Metrics; +using Datadog.Trace.Vendors.Newtonsoft.Json; + +// ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable NotAccessedField.Local +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + +namespace Datadog.Trace.Ci.Net; + +internal sealed partial class TestOptimizationClient +{ + private const string ImpactedTestsDetectionUrlPath = "api/v2/ci/tests/diffs"; + private const string ImpactedTestsDetectionType = "ci_app_tests_diffs_request"; + private Uri? _impactedTestsDetectionTestsUrl; + + public async Task GetImpactedTestsDetectionFilesAsync() + { + Log.Debug("TestOptimizationClient: Getting impacted tests detection modified files..."); + if (!EnsureRepositoryUrl() || !EnsureCommitSha()) + { + return default; + } + + _impactedTestsDetectionTestsUrl ??= GetUriFromPath(ImpactedTestsDetectionUrlPath); + var query = new DataEnvelope>( + new Data( + _commitSha, + ImpactedTestsDetectionType, + new ImpactedTestsDetectionQuery(_serviceName, _environment, _repositoryUrl, _branchName, _commitSha)), + null); + + var jsonQuery = JsonConvert.SerializeObject(query, SerializerSettings); + Log.Debug("TestOptimizationClient: ITD.JSON RQ = {Json}", jsonQuery); + + string? queryResponse; + try + { + queryResponse = await SendJsonRequestAsync(_impactedTestsDetectionTestsUrl, jsonQuery).ConfigureAwait(false); + } + catch (Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityImpactedTestsDetectionRequestErrors(MetricTags.CIVisibilityErrorType.Network); + Log.Error(ex, "TestOptimizationClient: Impacted tests file diffs request failed."); + throw; + } + + Log.Debug("TestOptimizationClient: ITD.JSON RS = {Json}", queryResponse); + if (string.IsNullOrEmpty(queryResponse)) + { + return default; + } + + var deserializedResult = JsonConvert.DeserializeObject?>>(queryResponse); + var finalResponse = deserializedResult.Data?.Attributes ?? default; + + // Count the number of tests for telemetry + var filesCount = 0; + if (finalResponse.Files is { Length: > 0 } files) + { + filesCount = files.Length; + } + + TelemetryFactory.Metrics.RecordDistributionCIVisibilityImpactedTestsDetectionResponseFiles(filesCount); + return finalResponse; + } + + private readonly struct ImpactedTestsDetectionCallbacks : ICallbacks + { + public void OnBeforeSend() + { + TelemetryFactory.Metrics.RecordCountCIVisibilityImpactedTestsDetectionRequest(MetricTags.CIVisibilityRequestCompressed.Uncompressed); + } + + public void OnStatusCodeReceived(int statusCode, int responseLength) + { + TelemetryFactory.Metrics.RecordDistributionCIVisibilityImpactedTestsDetectionResponseBytes(MetricTags.CIVisibilityResponseCompressed.Uncompressed, responseLength); + if (TelemetryHelper.GetErrorTypeFromStatusCode(statusCode) is { } errorType) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityImpactedTestsDetectionRequestErrors(errorType); + } + } + + public void OnError(Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityImpactedTestsDetectionRequestErrors(MetricTags.CIVisibilityErrorType.Network); + } + + public void OnAfterSend(double totalMs) + { + TelemetryFactory.Metrics.RecordDistributionCIVisibilityImpactedTestsDetectionRequestMs(totalMs); + } + } + + private readonly struct ImpactedTestsDetectionQuery + { + [JsonProperty("service")] + public readonly string Service; + + [JsonProperty("env")] + public readonly string Environment; + + [JsonProperty("repository_url")] + public readonly string RepositoryUrl; + + [JsonProperty("branch")] + public readonly string Branch; + + [JsonProperty("sha")] + public readonly string Sha; + + public ImpactedTestsDetectionQuery(string service, string environment, string repositoryUrl, string branch, string sha) + { + Service = service; + Environment = environment; + RepositoryUrl = repositoryUrl; + Branch = branch; + Sha = sha; + } + } + + public readonly struct ImpactedTestsDetectionResponse + { + [JsonProperty("base_sha")] + public readonly string? BaseSha; + + [JsonProperty("files")] + public readonly string[]? Files; + } +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetSettingsAsync.cs b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetSettingsAsync.cs new file mode 100644 index 000000000000..80ff589a8de1 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetSettingsAsync.cs @@ -0,0 +1,243 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Threading.Tasks; +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Ci.Telemetry; +using Datadog.Trace.Telemetry; +using Datadog.Trace.Telemetry.Metrics; +using Datadog.Trace.Vendors.Newtonsoft.Json; + +// ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable NotAccessedField.Local +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + +namespace Datadog.Trace.Ci.Net; + +internal sealed partial class TestOptimizationClient +{ + private const string SettingsUrlPath = "api/v2/libraries/tests/services/setting"; + private const string SettingsType = "ci_app_test_service_libraries_settings"; + private Uri? _settingsUrl; + + public static SettingsResponse CreateSettingsResponseFromTestOptimizationSettings(TestOptimizationSettings settings) + { + return new SettingsResponse( + codeCoverage: settings.CodeCoverageEnabled, + testsSkipping: settings.TestsSkippingEnabled, + requireGit: false, + impactedTestsEnabled: settings.ImpactedTestsDetectionEnabled, + flakyTestRetries: settings.FlakyRetryEnabled, + earlyFlakeDetection: new EarlyFlakeDetectionSettingsResponse( + enabled: settings.EarlyFlakeDetectionEnabled, + slowTestRetries: new SlowTestRetriesSettingsResponse(), + faultySessionThreshold: 0)); + } + + public async Task GetSettingsAsync(bool skipFrameworkInfo = false) + { + Log.Debug("TestOptimizationClient: Getting settings..."); + if (!EnsureRepositoryUrl() || !EnsureBranchName() || !EnsureCommitSha()) + { + return default; + } + + _settingsUrl ??= GetUriFromPath(SettingsUrlPath); + var query = new DataEnvelope>( + new Data( + _commitSha, + SettingsType, + new SettingsQuery(_serviceName, _environment, _repositoryUrl, _branchName, _commitSha, GetTestConfigurations(skipFrameworkInfo))), + null); + + var jsonQuery = JsonConvert.SerializeObject(query, SerializerSettings); + Log.Debug("TestOptimizationClient: Settings.JSON RQ = {Json}", jsonQuery); + + string? queryResponse; + try + { + queryResponse = await SendJsonRequestAsync(_settingsUrl, jsonQuery).ConfigureAwait(false); + } + catch (Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSettingsErrors(MetricTags.CIVisibilityErrorType.Network); + Log.Error(ex, "TestOptimizationClient: Get settings request failed."); + throw; + } + + Log.Debug("TestOptimizationClient: Settings.JSON RS = {Json}", queryResponse); + if (string.IsNullOrEmpty(queryResponse)) + { + return default; + } + + var deserializedResult = JsonConvert.DeserializeObject?>>(queryResponse); + var settingsResponse = deserializedResult.Data?.Attributes ?? default; + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSettingsResponse( + settingsResponse switch + { + { CodeCoverage: true, TestsSkipping: true, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipEnabled_AtrDisabled, + { CodeCoverage: true, TestsSkipping: false, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipDisabled_AtrDisabled, + { CodeCoverage: false, TestsSkipping: true, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipEnabled_AtrDisabled, + { CodeCoverage: false, TestsSkipping: false, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipDisabled_EFDEnabled_AtrDisabled, + { CodeCoverage: true, TestsSkipping: true, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipEnabled_EFDEnabled_AtrDisabled, + { CodeCoverage: true, TestsSkipping: false, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipDisabled_EFDEnabled_AtrDisabled, + { CodeCoverage: false, TestsSkipping: true, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: false } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipEnabled_EFDEnabled_AtrDisabled, + { CodeCoverage: true, TestsSkipping: true, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipEnabled_AtrEnabled, + { CodeCoverage: true, TestsSkipping: false, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipDisabled_AtrEnabled, + { CodeCoverage: false, TestsSkipping: true, EarlyFlakeDetection.Enabled: false, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipEnabled_AtrEnabled, + { CodeCoverage: false, TestsSkipping: false, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipDisabled_EFDEnabled_AtrEnabled, + { CodeCoverage: true, TestsSkipping: true, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipEnabled_EFDEnabled_AtrEnabled, + { CodeCoverage: true, TestsSkipping: false, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageEnabled_ItrSkipDisabled_EFDEnabled_AtrEnabled, + { CodeCoverage: false, TestsSkipping: true, EarlyFlakeDetection.Enabled: true, FlakyTestRetries: true } => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipEnabled_EFDEnabled_AtrEnabled, + _ => MetricTags.CIVisibilityITRSettingsResponse.CoverageDisabled_ItrSkipDisabled_AtrDisabled, + }); + return settingsResponse; + } + + private readonly struct SettingsCallbacks : ICallbacks + { + public void OnBeforeSend() + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSettings(MetricTags.CIVisibilityRequestCompressed.Uncompressed); + } + + public void OnStatusCodeReceived(int statusCode, int responseLength) + { + if (TelemetryHelper.GetErrorTypeFromStatusCode(statusCode) is { } errorType) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSettingsErrors(errorType); + } + } + + public void OnError(Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsSettingsErrors(MetricTags.CIVisibilityErrorType.Network); + } + + public void OnAfterSend(double totalMs) + { + TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsSettingsMs(totalMs); + } + } + + private readonly struct SettingsQuery + { + [JsonProperty("service")] + public readonly string Service; + + [JsonProperty("env")] + public readonly string Environment; + + [JsonProperty("repository_url")] + public readonly string RepositoryUrl; + + [JsonProperty("branch")] + public readonly string Branch; + + [JsonProperty("sha")] + public readonly string Sha; + + [JsonProperty("configurations")] + public readonly TestsConfigurations Configurations; + + public SettingsQuery(string service, string environment, string repositoryUrl, string branch, string sha, TestsConfigurations configurations) + { + Service = service; + Environment = environment; + RepositoryUrl = repositoryUrl; + Branch = branch; + Sha = sha; + Configurations = configurations; + } + } + + public readonly struct SettingsResponse + { + [JsonProperty("code_coverage")] + public readonly bool? CodeCoverage; + + [JsonProperty("tests_skipping")] + public readonly bool? TestsSkipping; + + [JsonProperty("require_git")] + public readonly bool? RequireGit; + + [JsonProperty("impacted_tests_enabled")] + public readonly bool? ImpactedTestsEnabled; + + [JsonProperty("flaky_test_retries_enabled")] + public readonly bool? FlakyTestRetries; + + [JsonProperty("early_flake_detection")] + public readonly EarlyFlakeDetectionSettingsResponse EarlyFlakeDetection; + + public SettingsResponse() + { + } + + public SettingsResponse(bool? codeCoverage, bool? testsSkipping, bool? requireGit, bool? impactedTestsEnabled, bool? flakyTestRetries, EarlyFlakeDetectionSettingsResponse earlyFlakeDetection) + { + CodeCoverage = codeCoverage; + TestsSkipping = testsSkipping; + RequireGit = requireGit; + ImpactedTestsEnabled = impactedTestsEnabled; + FlakyTestRetries = flakyTestRetries; + EarlyFlakeDetection = earlyFlakeDetection; + } + } + + public readonly struct EarlyFlakeDetectionSettingsResponse + { + [JsonProperty("enabled")] + public readonly bool? Enabled; + + [JsonProperty("slow_test_retries")] + public readonly SlowTestRetriesSettingsResponse SlowTestRetries; + + [JsonProperty("faulty_session_threshold")] + public readonly int? FaultySessionThreshold; + + public EarlyFlakeDetectionSettingsResponse() + { + } + + public EarlyFlakeDetectionSettingsResponse(bool? enabled, SlowTestRetriesSettingsResponse slowTestRetries, int? faultySessionThreshold) + { + Enabled = enabled; + SlowTestRetries = slowTestRetries; + FaultySessionThreshold = faultySessionThreshold; + } + } + + public readonly struct SlowTestRetriesSettingsResponse + { + [JsonProperty("5s")] + public readonly int? FiveSeconds; + + [JsonProperty("10s")] + public readonly int? TenSeconds; + + [JsonProperty("30s")] + public readonly int? ThirtySeconds; + + [JsonProperty("5m")] + public readonly int? FiveMinutes; + + public SlowTestRetriesSettingsResponse() + { + } + + public SlowTestRetriesSettingsResponse(int? fiveSeconds, int? tenSeconds, int? thirtySeconds, int? fiveMinutes) + { + FiveSeconds = fiveSeconds; + TenSeconds = tenSeconds; + ThirtySeconds = thirtySeconds; + FiveMinutes = fiveMinutes; + } + } +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetSkippableTestsAsync.cs b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetSkippableTestsAsync.cs new file mode 100644 index 000000000000..3137cf64aa8c --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.GetSkippableTestsAsync.cs @@ -0,0 +1,181 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Datadog.Trace.Ci.Telemetry; +using Datadog.Trace.Telemetry; +using Datadog.Trace.Telemetry.Metrics; +using Datadog.Trace.Vendors.Newtonsoft.Json; +using Datadog.Trace.Vendors.Serilog.Events; + +// ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable NotAccessedField.Local +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + +namespace Datadog.Trace.Ci.Net; + +internal sealed partial class TestOptimizationClient +{ + private const string SkippableUrlPath = "api/v2/ci/tests/skippable"; + private const string SkippableType = "test_params"; + private Uri? _skippableTestsUrl; + + public async Task GetSkippableTestsAsync() + { + Log.Debug("TestOptimizationClient: Getting skippable tests..."); + if (!EnsureRepositoryUrl() || !EnsureCommitSha()) + { + return default; + } + + _skippableTestsUrl ??= GetUriFromPath(SkippableUrlPath); + var query = new DataEnvelope>( + new Data( + null, + SkippableType, + new SkippableTestsQuery(_serviceName, _environment, _repositoryUrl, _commitSha, GetTestConfigurations())), + null); + + var jsonQuery = JsonConvert.SerializeObject(query, SerializerSettings); + Log.Debug("TestOptimizationClient: Skippable.JSON RQ = {Json}", jsonQuery); + + string? queryResponse; + try + { + queryResponse = await SendJsonRequestAsync(_skippableTestsUrl, jsonQuery).ConfigureAwait(false); + } + catch (Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityITRSkippableTestsRequestErrors(MetricTags.CIVisibilityErrorType.Network); + Log.Error(ex, "TestOptimizationClient: Get skippable tests request failed."); + throw; + } + + Log.Debug("TestOptimizationClient: Skippable.JSON RS = {Json}", queryResponse); + if (string.IsNullOrEmpty(queryResponse)) + { + return default; + } + + var deserializedResult = JsonConvert.DeserializeObject>>(queryResponse); + if (deserializedResult.Data is null || deserializedResult.Data.Length == 0) + { + return new SkippableTestsResponse(deserializedResult.Meta?.CorrelationId, []); + } + + var testAttributes = new List(deserializedResult.Data.Length); + var customConfigurations = _customConfigurations; + for (var i = 0; i < deserializedResult.Data.Length; i++) + { + var includeItem = true; + var item = deserializedResult.Data[i].Attributes; + if (item.Configurations?.Custom is { } itemCustomConfiguration) + { + if (customConfigurations is null) + { + continue; + } + + foreach (var rsCustomConfigurationItem in itemCustomConfiguration) + { + if (!customConfigurations.TryGetValue(rsCustomConfigurationItem.Key, out var customConfigValue) || + rsCustomConfigurationItem.Value != customConfigValue) + { + includeItem = false; + break; + } + } + } + + if (includeItem) + { + testAttributes.Add(item); + } + } + + if (Log.IsEnabled(LogEventLevel.Debug) && deserializedResult.Data.Length != testAttributes.Count) + { + Log.Debug("TestOptimizationClient: Skippable.JSON Filtered = {Json}", JsonConvert.SerializeObject(testAttributes)); + } + + TelemetryFactory.Metrics.RecordCountCIVisibilityITRSkippableTestsResponseTests(testAttributes.Count); + return new SkippableTestsResponse(deserializedResult.Meta?.CorrelationId, testAttributes); + } + + private readonly struct SkippableCallbacks : ICallbacks + { + public void OnBeforeSend() + { + TelemetryFactory.Metrics.RecordCountCIVisibilityITRSkippableTestsRequest(MetricTags.CIVisibilityRequestCompressed.Uncompressed); + } + + public void OnStatusCodeReceived(int statusCode, int responseLength) + { + TelemetryFactory.Metrics.RecordDistributionCIVisibilityITRSkippableTestsResponseBytes(MetricTags.CIVisibilityResponseCompressed.Uncompressed, responseLength); + if (TelemetryHelper.GetErrorTypeFromStatusCode(statusCode) is { } errorType) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityITRSkippableTestsRequestErrors(errorType); + } + } + + public void OnError(Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityITRSkippableTestsRequestErrors(MetricTags.CIVisibilityErrorType.Network); + } + + public void OnAfterSend(double totalMs) + { + TelemetryFactory.Metrics.RecordDistributionCIVisibilityITRSkippableTestsRequestMs(totalMs); + } + } + + private readonly struct SkippableTestsQuery + { + [JsonProperty("service")] + public readonly string Service; + + [JsonProperty("env")] + public readonly string Environment; + + [JsonProperty("repository_url")] + public readonly string RepositoryUrl; + + [JsonProperty("sha")] + public readonly string Sha; + + [JsonProperty("configurations")] + public readonly TestsConfigurations? Configurations; + + public SkippableTestsQuery(string service, string environment, string repositoryUrl, string sha, TestsConfigurations? configurations) + { + Service = service; + Environment = environment; + RepositoryUrl = repositoryUrl; + Sha = sha; + Configurations = configurations; + } + } + + public readonly struct SkippableTestsResponse + { + public readonly string? CorrelationId; + public readonly ICollection Tests; + + public SkippableTestsResponse() + { + CorrelationId = null; + Tests = []; + } + + public SkippableTestsResponse(string? correlationId, ICollection tests) + { + CorrelationId = correlationId; + Tests = tests; + } + } +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.SendPackFilesAsync.cs b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.SendPackFilesAsync.cs new file mode 100644 index 000000000000..e2c3a9ebb635 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.SendPackFilesAsync.cs @@ -0,0 +1,256 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Datadog.Trace.Agent; +using Datadog.Trace.Agent.Transports; +using Datadog.Trace.Ci.Telemetry; +using Datadog.Trace.Telemetry; +using Datadog.Trace.Telemetry.Metrics; +using Datadog.Trace.Vendors.Newtonsoft.Json; + +// ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable NotAccessedField.Local +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + +namespace Datadog.Trace.Ci.Net; + +internal sealed partial class TestOptimizationClient +{ + private const string PackFileUrlPath = "api/v2/git/repository/packfile"; + private Uri? _packFileUrl; + + public async Task SendPackFilesAsync(string commitSha, string[]? commitsToInclude, string[]? commitsToExclude) + { + Log.Debug("TestOptimizationClient: Packing and sending delta of commits and tree objects..."); + + var packFilesObject = GetObjectsPackFileFromWorkingDirectory(commitsToInclude, commitsToExclude); + if (packFilesObject.Files.Length == 0) + { + return 0; + } + + if (!EnsureRepositoryUrl()) + { + return 0; + } + + _packFileUrl ??= GetUriFromPath(PackFileUrlPath); + + var jsonPushedSha = JsonConvert.SerializeObject(new DataEnvelope>(new Data(commitSha, CommitType, null), _repositoryUrl), SerializerSettings); + Log.Debug("TestOptimizationClient: ObjPack.JSON RQ = {Json}", jsonPushedSha); + var jsonPushedShaBytes = Encoding.UTF8.GetBytes(jsonPushedSha); + + TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsObjectsPackFiles(packFilesObject.Files.Length); + long totalUploadSize = 0; + foreach (var packFile in packFilesObject.Files) + { + if (!Directory.Exists(Path.GetDirectoryName(packFile)) || !File.Exists(packFile)) + { + // Pack files must be sent in order, if a pack file is missing, we stop the upload of the rest of the pack files + // Previous pack files will enrich the backend with some of the data. + Log.Error("TestOptimizationClient: Pack file '{PackFile}' is missing, cancelling upload.", packFile); + break; + } + + // Send PackFile content + Log.Information("TestOptimizationClient: Sending {PackFile}", packFile); + try + { + var packFileContent = File.ReadAllBytes(packFile); + var queryResponse = await SendRequestAsync( + _packFileUrl, + [ + new MultipartFormItem("pushedSha", MimeTypes.Json, null, new ArraySegment(jsonPushedShaBytes)), + new MultipartFormItem("packfile", "application/octet-stream", null, new ArraySegment(packFileContent)) + ]) + .ConfigureAwait(false); + if (queryResponse is not null) + { + totalUploadSize += packFileContent.Length; + } + } + catch (Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsObjectsPackErrors(MetricTags.CIVisibilityErrorType.Network); + Log.Error(ex, "TestOptimizationClient: Send object pack file request failed."); + throw; + } + + // Delete temporal pack file + try + { + File.Delete(packFile); + } + catch (Exception ex) + { + Log.Warning(ex, "TestOptimizationClient: Error deleting pack file: '{PackFile}'", packFile); + } + } + + TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsObjectsPackBytes(totalUploadSize); + + // Delete temporary folder after the upload + if (!string.IsNullOrEmpty(packFilesObject.TemporaryFolder)) + { + try + { + Directory.Delete(packFilesObject.TemporaryFolder, true); + } + catch (Exception ex) + { + Log.Warning(ex, "TestOptimizationClient: Error deleting temporary folder: '{TemporaryFolder}'", packFilesObject.TemporaryFolder); + } + } + + Log.Information("TestOptimizationClient: Total pack file upload: {TotalUploadSize} bytes", totalUploadSize); + return totalUploadSize; + } + + private ObjectPackFilesResult GetObjectsPackFileFromWorkingDirectory(string[]? commitsToInclude, string[]? commitsToExclude) + { + Log.Debug("TestOptimizationClient: Getting objects..."); + commitsToInclude ??= []; + commitsToExclude ??= []; + var temporaryFolder = string.Empty; + var temporaryPath = Path.GetTempFileName(); + + var getObjectsArguments = "rev-list --objects --no-object-names --filter=blob:none --since=\"1 month ago\" HEAD " + string.Join(" ", commitsToExclude.Select(c => "^" + c)) + " " + string.Join(" ", commitsToInclude); + var getObjectsCommand = GitCommandHelper.RunGitCommand(_workingDirectory, getObjectsArguments, MetricTags.CIVisibilityCommands.GetObjects); + if (string.IsNullOrEmpty(getObjectsCommand?.Output)) + { + // If not objects has been returned we skip the pack + upload. + Log.Debug("TestOptimizationClient: No objects were returned from the git rev-list command."); + return new ObjectPackFilesResult([], temporaryFolder); + } + + // Sanitize object list (on some cases we get a "fatal: expected object ID, got garbage" error because the object list has invalid escape chars) + var objectsOutput = getObjectsCommand!.Output; + var matches = ShaRegex.Matches(objectsOutput); + var lstObjectsSha = new List(matches.Count); + foreach (Match? match in matches) + { + if (match is not null) + { + lstObjectsSha.Add(match.Value); + } + } + + if (lstObjectsSha.Count == 0) + { + // If not objects has been returned we skip the pack + upload. + Log.Debug("TestOptimizationClient: No valid objects were returned from the git rev-list command."); + return new ObjectPackFilesResult([], temporaryFolder); + } + + objectsOutput = string.Join("\n", lstObjectsSha) + "\n"; + + Log.Debug("TestOptimizationClient: Packing {NumObjects} objects...", lstObjectsSha.Count); + var getPacksArguments = $"pack-objects --compression=9 --max-pack-size={MaxPackFileSizeInMb}m \"{temporaryPath}\""; + var packObjectsResultCommand = GitCommandHelper.RunGitCommand(_workingDirectory, getPacksArguments, MetricTags.CIVisibilityCommands.PackObjects, objectsOutput); + if (packObjectsResultCommand is null) + { + Log.Warning("TestOptimizationClient: 'git pack-objects...' command is null"); + return new ObjectPackFilesResult([], temporaryFolder); + } + + if (packObjectsResultCommand.ExitCode != 0) + { + if (packObjectsResultCommand.Error.IndexOf("Cross-device", StringComparison.OrdinalIgnoreCase) != -1) + { + // Git can throw a cross device error if the temporal folder is in a different drive than the .git folder (eg. symbolic link) + // to handle this edge case, we create a temporal folder inside the current folder. + + Log.Warning("TestOptimizationClient: 'git pack-objects...' returned a cross-device error, retrying using a local temporal folder."); + temporaryFolder = Path.Combine(Environment.CurrentDirectory, ".git_tmp"); + if (!Directory.Exists(temporaryFolder)) + { + Directory.CreateDirectory(temporaryFolder); + } + + temporaryPath = Path.Combine(temporaryFolder, Path.GetFileName(temporaryPath)); + getPacksArguments = $"pack-objects --compression=9 --max-pack-size={MaxPackFileSizeInMb}m \"{temporaryPath}\""; + packObjectsResultCommand = GitCommandHelper.RunGitCommand(_workingDirectory, getPacksArguments, MetricTags.CIVisibilityCommands.PackObjects, getObjectsCommand!.Output); + if (packObjectsResultCommand is null) + { + Log.Warning("TestOptimizationClient: 'git pack-objects...' command is null"); + return new ObjectPackFilesResult([], temporaryFolder); + } + } + + if (packObjectsResultCommand.ExitCode != 0) + { + Log.Warning("TestOptimizationClient: 'git pack-objects...' command error: {Stderr}", packObjectsResultCommand.Error); + } + } + + var packObjectsSha = packObjectsResultCommand.Output.Split(new[] { "\n" }, StringSplitOptions.RemoveEmptyEntries); + + // We try to return an array with the path in the same order as has been returned by the git command. + var tempFolder = Path.GetDirectoryName(temporaryPath) ?? string.Empty; + var tempFile = Path.GetFileName(temporaryPath); + var lstFiles = new List(packObjectsSha.Length); + foreach (var pObjSha in packObjectsSha) + { + var file = Path.Combine(tempFolder, tempFile + "-" + pObjSha + ".pack"); + if (File.Exists(file)) + { + lstFiles.Add(file); + } + else + { + Log.Warning("TestOptimizationClient: The file '{PackFile}' doesn't exist.", file); + } + } + + return new ObjectPackFilesResult(lstFiles.ToArray(), temporaryFolder); + } + + private readonly struct ObjectPackFilesCallbacks : ICallbacks + { + public void OnBeforeSend() + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsObjectsPack(MetricTags.CIVisibilityRequestCompressed.Uncompressed); + } + + public void OnStatusCodeReceived(int statusCode, int responseLength) + { + if (TelemetryHelper.GetErrorTypeFromStatusCode(statusCode) is { } errorType) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsObjectsPackErrors(errorType); + } + } + + public void OnError(Exception ex) + { + TelemetryFactory.Metrics.RecordCountCIVisibilityGitRequestsObjectsPackErrors(MetricTags.CIVisibilityErrorType.Network); + } + + public void OnAfterSend(double totalMs) + { + TelemetryFactory.Metrics.RecordDistributionCIVisibilityGitRequestsObjectsPackMs(totalMs); + } + } + + private class ObjectPackFilesResult + { + public ObjectPackFilesResult(string[] files, string temporaryFolder) + { + Files = files; + TemporaryFolder = temporaryFolder; + } + + public string[] Files { get; } + + public string TemporaryFolder { get; } + } +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.UploadRepositoryChangesAsync.cs b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.UploadRepositoryChangesAsync.cs new file mode 100644 index 000000000000..5d342aee8599 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.UploadRepositoryChangesAsync.cs @@ -0,0 +1,144 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Threading.Tasks; +using Datadog.Trace.Telemetry.Metrics; + +// ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable NotAccessedField.Local +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + +namespace Datadog.Trace.Ci.Net; + +internal sealed partial class TestOptimizationClient +{ + public async Task UploadRepositoryChangesAsync() + { + Log.Debug("TestOptimizationClient: Uploading Repository Changes..."); + + // Let's first try get the commit data from local and remote + var initialCommitData = await GetCommitsAsync().ConfigureAwait(false); + + // Let's check if we could retrieve commit data + if (!initialCommitData.IsOk) + { + return 0; + } + + // If: + // - We have local commits + // - There are not missing commits (backend has the total number of local commits already) + // Then we are good to go with it, we don't need to check if we need to unshallow or anything and just go with that. + if (initialCommitData is { HasCommits: true, MissingCommits.Length: 0 }) + { + Log.Debug("TestOptimizationClient: Initial commit data has everything already, we don't need to upload anything."); + return 0; + } + + // There's some missing commits on the backend, first we need to check if we need to unshallow before sending anything... + + try + { + // We need to check if the git clone is a shallow one before uploading anything. + // In the case is a shallow clone we need to reconfigure it to upload the git tree + // without blobs so no content will be downloaded. + var gitRevParseShallowOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "rev-parse --is-shallow-repository", MetricTags.CIVisibilityCommands.CheckShallow); + if (gitRevParseShallowOutput is null) + { + Log.Warning("TestOptimizationClient: 'git rev-parse --is-shallow-repository' command is null"); + return 0; + } + + var isShallow = gitRevParseShallowOutput.Output.IndexOf("true", StringComparison.OrdinalIgnoreCase) > -1; + if (!isShallow) + { + // Repo is not in a shallow state, we continue with the pack files upload with the initial commit data we retrieved earlier. + Log.Debug("TestOptimizationClient: Repository is not in a shallow state, uploading changes..."); + return await SendPackFilesAsync(initialCommitData.LocalCommits[0], initialCommitData.MissingCommits, initialCommitData.RemoteCommits).ConfigureAwait(false); + } + + Log.Debug("TestOptimizationClient: Unshallowing the repository..."); + + // The git repo is a shallow clone, we need to double check if there are more than just 1 commit in the logs. + var gitShallowLogOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "log --format=oneline -n 2", MetricTags.CIVisibilityCommands.CheckShallow); + if (gitShallowLogOutput is null) + { + Log.Warning("TestOptimizationClient: 'git log --format=oneline -n 2' command is null"); + return 0; + } + + // After asking for 2 logs lines, if the git log command returns just one commit sha, we reconfigure the repo + // to ask for git commits and trees of the last month (no blobs) + var shallowLogArray = gitShallowLogOutput.Output.Split(["\n"], StringSplitOptions.RemoveEmptyEntries); + if (shallowLogArray.Length == 1) + { + // Just one commit SHA. Fetching previous commits + + // *** + // Let's try to unshallow the repo: + // `git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse HEAD)` + // *** + + // git config --default origin --get clone.defaultRemoteName + var originNameOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "config --default origin --get clone.defaultRemoteName", MetricTags.CIVisibilityCommands.GetRemote); + var originName = originNameOutput?.Output?.Replace("\n", string.Empty).Trim() ?? "origin"; + + // git rev-parse HEAD + var headOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "rev-parse HEAD", MetricTags.CIVisibilityCommands.GetHead); + var head = headOutput?.Output?.Replace("\n", string.Empty).Trim() ?? _branchName; + + // git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse HEAD) + Log.Information("TestOptimizationClient: The current repo is a shallow clone, refetching data for {OriginName}|{Head}", originName, head); + var gitUnshallowOutput = GitCommandHelper.RunGitCommand(_workingDirectory, $"fetch --shallow-since=\"1 month ago\" --update-shallow --filter=\"blob:none\" --recurse-submodules=no {originName} {head}", MetricTags.CIVisibilityCommands.Unshallow); + + if (gitUnshallowOutput is null || gitUnshallowOutput.ExitCode != 0) + { + // *** + // The previous command has a drawback: if the local HEAD is a commit that has not been pushed to the remote, it will fail. + // If this is the case, we fallback to: `git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse --abbrev-ref --symbolic-full-name @{upstream})` + // This command will attempt to use the tracked branch for the current branch in order to unshallow. + // *** + + // originName = git config --default origin --get clone.defaultRemoteName + // git rev-parse --abbrev-ref --symbolic-full-name @{upstream} + headOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "rev-parse --abbrev-ref --symbolic-full-name \"@{upstream}\"", MetricTags.CIVisibilityCommands.GetHead); + head = headOutput?.Output?.Replace("\n", string.Empty).Trim() ?? _branchName; + + // git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) $(git rev-parse --abbrev-ref --symbolic-full-name @{upstream}) + Log.Information("TestOptimizationClient: Previous unshallow command failed, refetching data with fallback 1 for {OriginName}|{Head}", originName, head); + gitUnshallowOutput = GitCommandHelper.RunGitCommand(_workingDirectory, $"fetch --shallow-since=\"1 month ago\" --update-shallow --filter=\"blob:none\" --recurse-submodules=no {originName} {head}", MetricTags.CIVisibilityCommands.Unshallow); + } + + if (gitUnshallowOutput is null || gitUnshallowOutput.ExitCode != 0) + { + // *** + // It could be that the CI is working on a detached HEAD or maybe branch tracking hasn’t been set up. + // In that case, this command will also fail, and we will finally fallback to we just unshallow all the things: + // `git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName)` + // *** + + // originName = git config --default origin --get clone.defaultRemoteName + // git fetch --shallow-since="1 month ago" --update-shallow --filter="blob:none" --recurse-submodules=no $(git config --default origin --get clone.defaultRemoteName) + Log.Information("TestOptimizationClient: Previous unshallow command failed, refetching data with fallback 2 for {OriginName}", originName); + GitCommandHelper.RunGitCommand(_workingDirectory, $"fetch --shallow-since=\"1 month ago\" --update-shallow --filter=\"blob:none\" --recurse-submodules=no {originName}", MetricTags.CIVisibilityCommands.Unshallow); + } + } + } + catch (Exception ex) + { + Log.Error(ex, "Error detecting and reconfiguring git repository for shallow clone."); + } + + var commitsData = await GetCommitsAsync().ConfigureAwait(false); + if (!commitsData.IsOk) + { + return 0; + } + + return await SendPackFilesAsync(commitsData.LocalCommits[0], commitsData.MissingCommits, commitsData.RemoteCommits).ConfigureAwait(false); + } +} diff --git a/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.cs b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.cs new file mode 100644 index 000000000000..c68c28948421 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/Net/TestOptimizationClient.cs @@ -0,0 +1,652 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.ExceptionServices; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Datadog.Trace.Agent; +using Datadog.Trace.Agent.Transports; +using Datadog.Trace.Ci.CiEnvironment; +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Logging; +using Datadog.Trace.Processors; +using Datadog.Trace.Telemetry.Metrics; +using Datadog.Trace.Util; +using Datadog.Trace.Vendors.Newtonsoft.Json; +using Datadog.Trace.Vendors.Serilog.Events; + +// ReSharper disable UnusedType.Local +// ReSharper disable UnusedMember.Local +// ReSharper disable ConvertToPrimaryConstructor +// ReSharper disable NotAccessedField.Local + +namespace Datadog.Trace.Ci.Net; + +internal sealed partial class TestOptimizationClient : ITestOptimizationClient +{ + private const string ApiKeyHeader = "DD-API-KEY"; + private const string EvpSubdomainHeader = "X-Datadog-EVP-Subdomain"; + + private const int MaxRetries = 5; + private const int MaxPackFileSizeInMb = 3; + + private static readonly IDatadogLogger Log; + private static readonly Regex ShaRegex; + private static readonly JsonSerializerSettings SerializerSettings; + + private readonly string _id; + private readonly TestOptimizationSettings _settings; + private readonly string? _workingDirectory; + private readonly string _environment; + private readonly string _serviceName; + private readonly Dictionary? _customConfigurations; + private readonly IApiRequestFactory _apiRequestFactory; + private readonly EventPlatformProxySupport _eventPlatformProxySupport; + private readonly string _repositoryUrl; + private readonly string _branchName; + private readonly string _commitSha; + + static TestOptimizationClient() + { + Log = DatadogLogging.GetLoggerFor(typeof(TestOptimizationClient)); + ShaRegex = new("[0-9a-f]+", RegexOptions.Compiled); + SerializerSettings = new() { DefaultValueHandling = DefaultValueHandling.Ignore }; + } + + private TestOptimizationClient(string? workingDirectory, TestOptimizationSettings? settings = null) + { + _id = RandomIdGenerator.Shared.NextSpanId().ToString(CultureInfo.InvariantCulture); + var ciVisibility = TestOptimization.Instance; + _settings = settings ?? ciVisibility.Settings; + + _workingDirectory = workingDirectory; + _environment = TraceUtil.NormalizeTag(_settings.TracerSettings.Environment ?? "none") ?? "none"; + _serviceName = NormalizerTraceProcessor.NormalizeService(_settings.TracerSettings.ServiceName) ?? string.Empty; + + // Extract custom tests configurations from DD_TAGS + _customConfigurations = GetCustomTestsConfigurations(_settings.TracerSettings.GlobalTags); + + _apiRequestFactory = ciVisibility.TracerManagement!.GetRequestFactory(_settings.TracerSettings, TimeSpan.FromSeconds(45)); + _eventPlatformProxySupport = _settings.Agentless ? EventPlatformProxySupport.None : ciVisibility.TracerManagement.EventPlatformProxySupport; + + _repositoryUrl = GetRepositoryUrl(); + _commitSha = GetCommitSha(); + _branchName = GetBranchName(); + } + + public static ITestOptimizationClient Create(string? workingDirectory, TestOptimizationSettings? settings = null) + { + return new TestOptimizationClient(workingDirectory, settings); + } + + public static ITestOptimizationClient CreateCached(string? workingDirectory, TestOptimizationSettings? settings = null) + { + return new CachedTestOptimizationClient(new TestOptimizationClient(workingDirectory, settings)); + } + + internal static Dictionary? GetCustomTestsConfigurations(IReadOnlyDictionary globalTags) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (globalTags is null) + { + return null; + } + + Dictionary? customConfiguration = null; + foreach (var tag in globalTags) + { + const string testConfigKey = "test.configuration."; + if (tag.Key.StartsWith(testConfigKey, StringComparison.OrdinalIgnoreCase)) + { + var key = tag.Key.Substring(testConfigKey.Length); + if (string.IsNullOrEmpty(key)) + { + continue; + } + + customConfiguration ??= new Dictionary(); + customConfiguration[key] = tag.Value; + } + } + + return customConfiguration; + } + + private string GetRepositoryUrl() + { + if (CIEnvironmentValues.Instance.Repository is { Length: > 0 } repository) + { + return repository; + } + + var gitOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "config --get remote.origin.url", MetricTags.CIVisibilityCommands.GetRepository); + return gitOutput?.Output.Replace("\n", string.Empty) ?? string.Empty; + } + + private string GetBranchName() + { + if (CIEnvironmentValues.Instance.Branch is { Length: > 0 } branch) + { + return branch; + } + + var gitOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "branch --show-current", MetricTags.CIVisibilityCommands.GetBranch); + var res = gitOutput?.Output.Replace("\n", string.Empty) ?? string.Empty; + + if (string.IsNullOrEmpty(res)) + { + Log.Warning("TestOptimizationClient: empty branch indicates a detached head at commit {Commit}", _commitSha); + res = $"auto:git-detached-head"; + } + + return res; + } + + private string GetCommitSha() + { + var gitOutput = GitCommandHelper.RunGitCommand(_workingDirectory, "rev-parse HEAD", MetricTags.CIVisibilityCommands.GetHead); + var gitSha = gitOutput?.Output.Replace("\n", string.Empty) ?? string.Empty; + if (string.IsNullOrEmpty(gitSha) && CIEnvironmentValues.Instance.Commit is { Length: > 0 } commitSha) + { + return commitSha; + } + + return gitSha; + } + + private bool EnsureRepositoryUrl() + { + if (string.IsNullOrEmpty(_repositoryUrl)) + { + Log.Warning("TestOptimizationClient: 'git config --get remote.origin.url' command returned null or empty"); + return false; + } + + return true; + } + + private bool EnsureBranchName() + { + if (string.IsNullOrEmpty(_branchName)) + { + Log.Warning("TestOptimizationClient: 'git branch --show-current' command returned null or empty"); + return false; + } + + return true; + } + + private bool EnsureCommitSha() + { + if (string.IsNullOrEmpty(_commitSha)) + { + Log.Warning("TestOptimizationClient: 'git rev-parse HEAD' command returned null or empty"); + return false; + } + + return true; + } + + private TestsConfigurations GetTestConfigurations(bool skipFrameworkInfo = false) + { + var framework = FrameworkDescription.Instance; + return new TestsConfigurations( + framework.OSPlatform, + TestOptimization.Instance.HostInfo.GetOperatingSystemVersion() ?? string.Empty, + framework.OSArchitecture, + skipFrameworkInfo ? null : framework.Name, + skipFrameworkInfo ? null : framework.ProductVersion, + skipFrameworkInfo ? null : framework.ProcessArchitecture, + _customConfigurations); + } + + private Uri GetUriFromPath(string uriPath) + { + if (_settings.Agentless) + { + var agentlessUrl = _settings.AgentlessUrl; + if (!string.IsNullOrWhiteSpace(agentlessUrl)) + { + return new UriBuilder(agentlessUrl) { Path = uriPath }.Uri; + } + + return new UriBuilder( + scheme: "https", + host: "api." + _settings.Site, + port: 443, + pathValue: uriPath).Uri; + } + + switch (_eventPlatformProxySupport) + { + case EventPlatformProxySupport.V2: + return _apiRequestFactory.GetEndpoint($"evp_proxy/v2/{uriPath}"); + case EventPlatformProxySupport.V4: + return _apiRequestFactory.GetEndpoint($"evp_proxy/v4/{uriPath}"); + default: + throw new NotSupportedException("Event platform proxy not supported by the agent."); + } + } + + private async Task SendJsonRequestAsync(Uri url, string jsonContent, TCallbacks callbacks = default) + where TCallbacks : struct, ICallbacks + { + var content = Encoding.UTF8.GetBytes(jsonContent); + var result = await SendRequestAsync(url, content, callbacks).ConfigureAwait(false); + return Encoding.UTF8.GetString(result); + } + + private async Task SendRequestAsync(Uri uri, byte[] body, TCallbacks callbacks = default) + where TCallbacks : struct, ICallbacks + { + return await WithRetries( + static async (state, finalTry) => + { + var callbacks = state.Callbacks; + var sw = Stopwatch.StartNew(); + try + { + callbacks.OnBeforeSend(); + var client = state.Client; + var uri = state.Uri; + var body = state.Body; + var request = client._apiRequestFactory.Create(uri); + client.SetRequestHeader(request); + + if (Log.IsEnabled(LogEventLevel.Debug)) + { + Log.Debug("TestOptimizationClient: Sending request to: {Url}", uri.ToString()); + } + + byte[]? responseContent; + try + { + using var response = await request.PostAsync(new ArraySegment(body), MimeTypes.Json).ConfigureAwait(false); + // TODO: Check for compressed responses - if we received one, currently these are not handled and would throw when we attempt to deserialize + using var stream = await response.GetStreamAsync().ConfigureAwait(false); + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + responseContent = memoryStream.ToArray(); + callbacks.OnStatusCodeReceived(response.StatusCode, responseContent.Length); + client.CheckResponseStatusCode(response, responseContent, finalTry); + Log.Debug("TestOptimizationClient: Response received from: {Url} | {StatusCode}", uri.ToString(), response.StatusCode); + } + catch (Exception ex) + { + Log.Error(ex, "TestOptimizationClient: Error getting result."); + callbacks.OnError(ex); + throw; + } + + return responseContent; + } + finally + { + callbacks.OnAfterSend(sw.Elapsed.TotalMilliseconds); + } + }, + new UriAndBodyState(this, uri, body, callbacks), + MaxRetries) + .ConfigureAwait(false); + } + + private async Task SendRequestAsync(Uri uri, MultipartFormItem[] items, TCallbacks callbacks = default) + where TCallbacks : struct, ICallbacks + { + return await WithRetries( + static async (state, finalTry) => + { + var callbacks = state.Callbacks; + var sw = Stopwatch.StartNew(); + try + { + callbacks.OnBeforeSend(); + var client = state.Client; + var uri = state.Uri; + var multipartFormItems = state.MultipartFormItems; + var request = client._apiRequestFactory.Create(uri); + client.SetRequestHeader(request); + + if (Log.IsEnabled(LogEventLevel.Debug)) + { + Log.Debug("TestOptimizationClient: Sending request to: {Url}", uri.ToString()); + } + + byte[]? responseContent; + try + { + using var response = await request.PostAsync(multipartFormItems).ConfigureAwait(false); + // TODO: Check for compressed responses - if we received one, currently these are not handled and would throw when we attempt to deserialize + using var stream = await response.GetStreamAsync().ConfigureAwait(false); + using var memoryStream = new MemoryStream(); + await stream.CopyToAsync(memoryStream).ConfigureAwait(false); + responseContent = memoryStream.ToArray(); + callbacks.OnStatusCodeReceived(response.StatusCode, responseContent.Length); + client.CheckResponseStatusCode(response, responseContent, finalTry); + Log.Debug("TestOptimizationClient: Response received from: {Url} | {StatusCode}", uri.ToString(), response.StatusCode); + } + catch (Exception ex) + { + Log.Error(ex, "TestOptimizationClient: Error getting result."); + callbacks.OnError(ex); + throw; + } + + return responseContent; + } + finally + { + callbacks.OnAfterSend(sw.Elapsed.TotalMilliseconds); + } + }, + new UriAndMultipartFormBodyState(this, uri, items, callbacks), + MaxRetries) + .ConfigureAwait(false); + } + + private void SetRequestHeader(IApiRequest request) + { + request.AddHeader(HttpHeaderNames.TraceId, _id); + request.AddHeader(HttpHeaderNames.ParentId, _id); + if (_eventPlatformProxySupport is EventPlatformProxySupport.V2 or EventPlatformProxySupport.V4) + { + request.AddHeader(EvpSubdomainHeader, "api"); + } + else + { + request.AddHeader(ApiKeyHeader, _settings.ApiKey); + } + } + + private void CheckResponseStatusCode(IApiResponse response, byte[]? responseContent, bool finalTry) + { + // Check if the rate limit header was received. + if (response.StatusCode == 429 && + response.GetHeader("x-ratelimit-reset") is { } strRateLimitDurationInSeconds && + int.TryParse(strRateLimitDurationInSeconds, out var rateLimitDurationInSeconds)) + { + if (rateLimitDurationInSeconds > 30) + { + // If 'x-ratelimit-reset' is > 30 seconds we cancel the request. + throw new RateLimitException(); + } + + throw new RateLimitException(rateLimitDurationInSeconds); + } + + if (response.StatusCode is < 200 or >= 300 && response.StatusCode != 404 && response.StatusCode != 502) + { + var stringContent = responseContent != null ? Encoding.UTF8.GetString(responseContent) : string.Empty; + if (finalTry) + { + Log.Error("TestOptimizationClient: Request failed with status code {StatusCode} and message: {ResponseContent}", response.StatusCode, stringContent); + } + + throw new WebException($"Status: {response.StatusCode}, Content: {stringContent}"); + } + } + + private async Task WithRetries(Func> sendDelegate, TState state, int numOfRetries) + { + var retryCount = 1; + var sleepDuration = 100; // in milliseconds + + while (true) + { + T response = default!; + ExceptionDispatchInfo? exceptionDispatchInfo = null; + var isFinalTry = retryCount >= numOfRetries; + + try + { + response = await sendDelegate(state, isFinalTry).ConfigureAwait(false); + } + catch (Exception ex) + { + exceptionDispatchInfo = ExceptionDispatchInfo.Capture(ex); + } + + // Error handling block + if (exceptionDispatchInfo is not null) + { + var sourceException = exceptionDispatchInfo.SourceException; + + if (isFinalTry || + sourceException is RateLimitException { DelayTimeInSeconds: null } || + sourceException is DirectoryNotFoundException || + sourceException is FileNotFoundException) + { + // stop retrying + Log.Error(sourceException, "TestOptimizationClient: An error occurred while sending data after {Retries} retries.", retryCount); + exceptionDispatchInfo.Throw(); + } + + // Before retry + var isSocketException = false; + var innerException = sourceException; + while (innerException != null) + { + if (innerException is SocketException) + { + isSocketException = true; + break; + } + + innerException = innerException.InnerException; + } + + if (isSocketException) + { + Log.Debug(sourceException, "TestOptimizationClient: Unable to communicate with the server"); + } + + if (sourceException is RateLimitException { DelayTimeInSeconds: { } delayTimeInSeconds }) + { + // Execute rate limit retry delay + await Task.Delay(TimeSpan.FromSeconds(delayTimeInSeconds)).ConfigureAwait(false); + } + else + { + // Execute retry delay + await Task.Delay(sleepDuration).ConfigureAwait(false); + sleepDuration *= 2; + } + + retryCount++; + continue; + } + + Log.Debug("TestOptimizationClient: Request was completed successfully."); + return response; + } + } + +#pragma warning disable SA1201 + internal interface ICallbacks + { + void OnBeforeSend(); + + void OnStatusCodeReceived(int statusCode, int responseLength); + + void OnError(Exception ex); + + void OnAfterSend(double totalMs); + } + + internal readonly struct ActionCallbacks : ICallbacks + { + private readonly Action? _onBeforeSend; + private readonly Action? _onStatusCodeReceived; + private readonly Action? _onError; + private readonly Action? _onAfterSend; + + public ActionCallbacks(Action? onBeforeSend = null, Action? onStatusCodeReceived = null, Action? onError = null, Action? onAfterSend = null) + { + _onBeforeSend = onBeforeSend; + _onStatusCodeReceived = onStatusCodeReceived; + _onError = onError; + _onAfterSend = onAfterSend; + } + + public void OnBeforeSend() => _onBeforeSend?.Invoke(); + + public void OnStatusCodeReceived(int statusCode, int responseLength) => _onStatusCodeReceived?.Invoke(statusCode, responseLength); + + public void OnError(Exception ex) => _onError?.Invoke(ex); + + public void OnAfterSend(double totalMs) => _onAfterSend?.Invoke(totalMs); + } + + internal readonly struct NoopCallbacks : ICallbacks + { + public void OnBeforeSend() + { + } + + public void OnStatusCodeReceived(int statusCode, int responseLength) + { + } + + public void OnError(Exception ex) + { + } + + public void OnAfterSend(double totalMs) + { + } + } + + private readonly struct UriAndBodyState + where TCallbacks : struct, ICallbacks + { + public readonly TestOptimizationClient Client; + public readonly Uri Uri; + public readonly byte[] Body; + public readonly TCallbacks Callbacks; + + public UriAndBodyState(TestOptimizationClient client, Uri uri, byte[] body, TCallbacks callbacks) + { + Client = client; + Uri = uri; + Body = body; + Callbacks = callbacks; + } + } + + private readonly struct UriAndMultipartFormBodyState + where TCallbacks : struct, ICallbacks + { + public readonly TestOptimizationClient Client; + public readonly Uri Uri; + public readonly MultipartFormItem[] MultipartFormItems; + public readonly TCallbacks Callbacks; + + public UriAndMultipartFormBodyState(TestOptimizationClient client, Uri uri, MultipartFormItem[] multipartFormItems, TCallbacks callbacks) + { + Client = client; + Uri = uri; + MultipartFormItems = multipartFormItems; + Callbacks = callbacks; + } + } + + private class RateLimitException : Exception + { + public RateLimitException() + : base("Server rate limiting response received. Cancelling request.") + { + DelayTimeInSeconds = null; + } + + public RateLimitException(int delayTimeInSeconds) + : base($"Server rate limiting response received. Waiting for {delayTimeInSeconds} seconds") + { + DelayTimeInSeconds = delayTimeInSeconds; + } + + public int? DelayTimeInSeconds { get; } + } + +#pragma warning disable SA1201 + private readonly struct DataEnvelope + { + [JsonProperty("data")] + public readonly T? Data; + + [JsonProperty("meta")] + public readonly Metadata? Meta; + + public DataEnvelope(T? data, string? repositoryUrl) + { + Data = data; + Meta = repositoryUrl is null ? null : new Metadata(repositoryUrl); + } + } + + private readonly struct DataArrayEnvelope + { + [JsonProperty("data")] + public readonly T[] Data; + + [JsonProperty("meta")] + public readonly Metadata? Meta; + + public DataArrayEnvelope(T[] data, string? repositoryUrl) + { + Data = data; + Meta = repositoryUrl is null ? null : new Metadata(repositoryUrl); + } + } + + private readonly struct Metadata + { + [JsonProperty("repository_url")] + public readonly string RepositoryUrl; + + [JsonProperty("correlation_id")] + public readonly string? CorrelationId; + + public Metadata(string repositoryUrl) + { + RepositoryUrl = repositoryUrl; + CorrelationId = null; + } + + public Metadata(string repositoryUrl, string? correlationId) + { + RepositoryUrl = repositoryUrl; + CorrelationId = correlationId; + } + } + + private readonly struct Data + { + [JsonProperty("id")] + public readonly string? Id; + + [JsonProperty("type")] + public readonly string Type; + + [JsonProperty("attributes")] + public readonly T? Attributes; + + public Data(string? id, string type, T? attributes) + { + Id = id; + Type = type; + Attributes = attributes; + } + } +} diff --git a/tracer/src/Datadog.Trace/Ci/Telemetry/TelemetryHelper.cs b/tracer/src/Datadog.Trace/Ci/Telemetry/TelemetryHelper.cs index 0d2e22947fae..ab1a2c74776e 100644 --- a/tracer/src/Datadog.Trace/Ci/Telemetry/TelemetryHelper.cs +++ b/tracer/src/Datadog.Trace/Ci/Telemetry/TelemetryHelper.cs @@ -174,7 +174,7 @@ public static MetricTags.CIVisibilityExitCodes GetTelemetryExitCodeFromExitCode( return MetricTags.CIVisibilityTestingEventTypeWithCodeOwnerAndSupportedCiAndBenchmarkAndEarlyFlakeDetectionAndRum.Module; case MetricTags.CIVisibilityTestingEventType.Session: { - var settings = CIVisibility.Settings; + var settings = TestOptimization.Instance.Settings; if (settings.Logs) { return CIEnvironmentValues.Instance switch diff --git a/tracer/src/Datadog.Trace/Ci/Test.cs b/tracer/src/Datadog.Trace/Ci/Test.cs index eee8d439d1be..d0bef8815b63 100644 --- a/tracer/src/Datadog.Trace/Ci/Test.cs +++ b/tracer/src/Datadog.Trace/Ci/Test.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -28,6 +29,7 @@ public sealed class Test private static readonly AsyncLocal CurrentTest = new(); private static readonly HashSet OpenedTests = new(); + private readonly ITestOptimization _testOptimization; private readonly Scope _scope; private int _finished; private List>? _onCloseActions; @@ -59,8 +61,9 @@ internal Test(TestSuite suite, string name, DateTimeOffset? startDate, TraceId t TelemetryFactory.Metrics.RecordCountSpanCreated(MetricTags.IntegrationName.CiAppManual); _scope = scope; + _testOptimization = TestOptimization.Instance; - if (CIVisibility.Settings.CodeCoverageEnabled == true) + if (_testOptimization.Settings.CodeCoverageEnabled == true) { Coverage.CoverageReporter.Handler.StartSession(module.Framework); } @@ -71,7 +74,7 @@ internal Test(TestSuite suite, string name, DateTimeOffset? startDate, TraceId t OpenedTests.Add(this); } - CIVisibility.Log.Debug("######### New Test Created: {Name} ({Suite} | {Module})", Name, Suite.Name, Suite.Module.Name); + _testOptimization.Log.Debug("######### New Test Created: {Name} ({Suite} | {Module})", Name, Suite.Name, Suite.Module.Name); if (startDate is null) { @@ -359,7 +362,7 @@ public void Close(TestStatus status, TimeSpan? duration, string? skipReason) { if (Interlocked.Exchange(ref _finished, 1) == 1) { - CIVisibility.Log.Warning("Test.Close() was already called before."); + _testOptimization.Log.Warning("Test.Close() was already called before."); return; } @@ -370,7 +373,7 @@ public void Close(TestStatus status, TimeSpan? duration, string? skipReason) duration ??= _scope.Span.Context.TraceContext.Clock.ElapsedSince(scope.Span.StartTime); // Set coverage - if (CIVisibility.Settings.CodeCoverageEnabled == true) + if (_testOptimization.Settings.CodeCoverageEnabled == true) { if (Coverage.CoverageReporter.Handler.EndSession() is Coverage.Models.Tests.TestCoverage testCoverage) { @@ -378,13 +381,13 @@ public void Close(TestStatus status, TimeSpan? duration, string? skipReason) testCoverage.SuiteId = tags.SuiteId; testCoverage.SpanId = _scope.Span.SpanId; - CIVisibility.Log.Debug("Coverage data for SessionId={SessionId}, SuiteId={SuiteId} and SpanId={SpanId} processed.", testCoverage.SessionId, testCoverage.SuiteId, testCoverage.SpanId); - CIVisibility.Manager?.WriteEvent(testCoverage); + _testOptimization.Log.Debug("Coverage data for SessionId={SessionId}, SuiteId={SuiteId} and SpanId={SpanId} processed.", testCoverage.SessionId, testCoverage.SuiteId, testCoverage.SpanId); + _testOptimization.TracerManagement?.Manager?.WriteEvent(testCoverage); } else if (status != TestStatus.Skip) { var testName = scope.Span.ResourceName; - CIVisibility.Log.Warning("Coverage data for test: {TestName} with Status: {Status} is empty. File: {File}", testName, status, tags.SourceFile); + _testOptimization.Log.Warning("Coverage data for test: {TestName} with Status: {Status} is empty. File: {File}", testName, status, tags.SourceFile); } } @@ -458,7 +461,7 @@ public void Close(TestStatus status, TimeSpan? duration, string? skipReason) OpenedTests.Remove(this); } - CIVisibility.Log.Debug("######### Test Closed: {Name} ({Suite} | {Module}) | {Status}", Name, Suite.Name, Suite.Module.Name, tags.Status); + _testOptimization.Log.Debug("######### Test Closed: {Name} ({Suite} | {Module}) | {Status}", Name, Suite.Name, Suite.Module.Name, tags.Status); } internal void ResetStartTime() diff --git a/tracer/src/Datadog.Trace/Ci/TestModule.cs b/tracer/src/Datadog.Trace/Ci/TestModule.cs index e6fe4637fcf3..4b2a08851774 100644 --- a/tracer/src/Datadog.Trace/Ci/TestModule.cs +++ b/tracer/src/Datadog.Trace/Ci/TestModule.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -38,6 +39,7 @@ public sealed class TestModule private static readonly AsyncLocal CurrentModule = new(); private static readonly HashSet OpenedTestModules = new(); + private readonly ITestOptimization _testOptimization; private readonly Span _span; private readonly Dictionary _suites; private readonly TestSession? _fakeSession; @@ -52,7 +54,8 @@ private TestModule(string name, string? framework, string? frameworkVersion, Dat internal TestModule(string name, string? framework, string? frameworkVersion, DateTimeOffset? startDate, TestSessionSpanTags? sessionSpanTags) { // First we make sure that CI Visibility is initialized. - CIVisibility.InitializeFromManualInstrumentation(); + _testOptimization = TestOptimization.Instance; + _testOptimization.InitializeFromManualInstrumentation(); var environment = CIEnvironmentValues.Instance; var frameworkDescription = FrameworkDescription.Instance; @@ -100,7 +103,7 @@ internal TestModule(string name, string? framework, string? frameworkVersion, Da RuntimeArchitecture = frameworkDescription.ProcessArchitecture, OSArchitecture = frameworkDescription.OSArchitecture, OSPlatform = frameworkDescription.OSPlatform, - OSVersion = CIVisibility.GetOperatingSystemVersion(), + OSVersion = _testOptimization.HostInfo.GetOperatingSystemVersion(), CiEnvVars = sessionSpanTags.CiEnvVars, SessionId = sessionSpanTags.SessionId, Command = sessionSpanTags.Command, @@ -122,7 +125,7 @@ internal TestModule(string name, string? framework, string? frameworkVersion, Da RuntimeArchitecture = frameworkDescription.ProcessArchitecture, OSArchitecture = frameworkDescription.OSArchitecture, OSPlatform = frameworkDescription.OSPlatform, - OSVersion = CIVisibility.GetOperatingSystemVersion(), + OSVersion = _testOptimization.HostInfo.GetOperatingSystemVersion(), IntelligentTestRunnerSkippingType = IntelligentTestRunnerTags.SkippingTypeTest, }; @@ -131,7 +134,8 @@ internal TestModule(string name, string? framework, string? frameworkVersion, Da // Extract session variables (from out of process sessions) var environmentVariables = EnvironmentHelpers.GetEnvironmentVariables(); var context = Tracer.Instance.TracerManager.SpanContextPropagator.Extract( - environmentVariables, new DictionaryGetterAndSetter(DictionaryGetterAndSetter.EnvironmentVariableKeyProcessor)); + environmentVariables, + new DictionaryGetterAndSetter(DictionaryGetterAndSetter.EnvironmentVariableKeyProcessor)); if (context.SpanContext is { } sessionContext) { @@ -148,7 +152,7 @@ internal TestModule(string name, string? framework, string? frameworkVersion, Da } else { - CIVisibility.Log.Information("A session cannot be found, creating a fake session as a parent of the module."); + _testOptimization.Log.Information("A session cannot be found, creating a fake session as a parent of the module."); _fakeSession = TestSession.InternalGetOrCreate(System.Environment.CommandLine, System.Environment.CurrentDirectory, null, startDate, false); if (_fakeSession.Tags is { } fakeSessionTags) { @@ -156,7 +160,7 @@ internal TestModule(string name, string? framework, string? frameworkVersion, Da tags.Command = fakeSessionTags.Command; tags.WorkingDirectory = fakeSessionTags.WorkingDirectory; - if (CIVisibility.Settings.EarlyFlakeDetectionEnabled == true) + if (_testOptimization.Settings.EarlyFlakeDetectionEnabled == true) { fakeSessionTags.EarlyFlakeDetectionTestEnabled = "true"; } @@ -165,7 +169,7 @@ internal TestModule(string name, string? framework, string? frameworkVersion, Da } // Check if Intelligent Test Runner has skippable tests and set the flag according to that - tags.TestsSkipped = CIVisibility.HasSkippableTests() ? "true" : "false"; + tags.TestsSkipped = _testOptimization.SkippableFeature?.HasSkippableTests() == true ? "true" : "false"; var span = Tracer.Instance.StartSpan( string.IsNullOrEmpty(framework) ? "test_module" : $"{framework!.ToLowerInvariant()}.test_module", @@ -187,7 +191,7 @@ internal TestModule(string name, string? framework, string? frameworkVersion, Da OpenedTestModules.Add(this); } - CIVisibility.Log.Debug("### Test Module Created: {Name}", name); + _testOptimization.Log.Debug("### Test Module Created: {Name}", name); if (startDate is null) { @@ -368,8 +372,8 @@ public void Close(TimeSpan? duration) { if (InternalClose(duration)) { - CIVisibility.Log.Debug("### Test Module Flushing after close: {Name}", Name); - CIVisibility.Flush(); + _testOptimization.Log.Debug("### Test Module Flushing after close: {Name}", Name); + _testOptimization.Flush(); } } @@ -391,8 +395,8 @@ public Task CloseAsync(TimeSpan? duration) { if (InternalClose(duration)) { - CIVisibility.Log.Debug("### Test Module Flushing after close: {Name}", Name); - return CIVisibility.FlushAsync(); + _testOptimization.Log.Debug("### Test Module Flushing after close: {Name}", Name); + return _testOptimization.FlushAsync(); } return Task.CompletedTask; @@ -431,13 +435,13 @@ private bool InternalClose(TimeSpan? duration) // Update status Tags.Status ??= TestTags.StatusPass; - if (CIVisibility.Settings.CodeCoverageEnabled == true && + if (_testOptimization.Settings.CodeCoverageEnabled == true && CoverageReporter.Handler is DefaultWithGlobalCoverageEventHandler coverageHandler && coverageHandler.GetCodeCoveragePercentage() is { } globalCoverage) { // We only report global code coverage if ITR is disabled and we are in a fake session (like the internal testlogger scenario) // For a normal customer session we never report the percentage of total lines on modules - if (!CIVisibility.Settings.IntelligentTestRunnerEnabled && _fakeSession is not null) + if (!_testOptimization.Settings.IntelligentTestRunnerEnabled && _fakeSession is not null) { // Adds the global code coverage percentage to the module var codeCoveragePercentage = globalCoverage.GetTotalPercentage(); @@ -446,9 +450,9 @@ CoverageReporter.Handler is DefaultWithGlobalCoverageEventHandler coverageHandle } // If the code coverage path environment variable is set, we store the json file - if (!string.IsNullOrWhiteSpace(CIVisibility.Settings.CodeCoveragePath)) + if (!string.IsNullOrWhiteSpace(_testOptimization.Settings.CodeCoveragePath)) { - var codeCoveragePath = Path.Combine(CIVisibility.Settings.CodeCoveragePath, $"coverage-{DateTime.Now:yyyy-MM-dd_HH_mm_ss}-{Guid.NewGuid():n}.json"); + var codeCoveragePath = Path.Combine(_testOptimization.Settings.CodeCoveragePath, $"coverage-{DateTime.Now:yyyy-MM-dd_HH_mm_ss}-{Guid.NewGuid():n}.json"); try { using var fStream = File.OpenWrite(codeCoveragePath); @@ -457,15 +461,15 @@ CoverageReporter.Handler is DefaultWithGlobalCoverageEventHandler coverageHandle } catch (Exception ex) { - CIVisibility.Log.Error(ex, "Error writing global code coverage."); + _testOptimization.Log.Error(ex, "Error writing global code coverage."); } } } - if (CIVisibility.Settings.TestsSkippingEnabled.HasValue) + if (_testOptimization.Settings.TestsSkippingEnabled.HasValue) { - span.SetTag(IntelligentTestRunnerTags.TestTestsSkippingEnabled, CIVisibility.Settings.TestsSkippingEnabled.Value ? "true" : "false"); - if (CIVisibility.Settings.TestsSkippingEnabled.Value) + span.SetTag(IntelligentTestRunnerTags.TestTestsSkippingEnabled, _testOptimization.Settings.TestsSkippingEnabled.Value ? "true" : "false"); + if (_testOptimization.Settings.TestsSkippingEnabled.Value) { // If we detect a module with tests skipping enabled, we ensure we also have the session tag set TrySetSessionTag(IntelligentTestRunnerTags.TestTestsSkippingEnabled, "true"); @@ -481,8 +485,8 @@ CoverageReporter.Handler is DefaultWithGlobalCoverageEventHandler coverageHandle } else { - span.SetTag(IntelligentTestRunnerTags.TestsSkipped, CIVisibility.HasSkippableTests() ? "true" : "false"); - if (CIVisibility.HasSkippableTests()) + span.SetTag(IntelligentTestRunnerTags.TestsSkipped, _testOptimization.SkippableFeature?.HasSkippableTests() == true ? "true" : "false"); + if (_testOptimization.SkippableFeature?.HasSkippableTests() == true) { // If we detect a module with tests being skipped, we ensure we also have the session tag set // if not we don't affect the session tag (other modules could have skipped tests) @@ -490,11 +494,11 @@ CoverageReporter.Handler is DefaultWithGlobalCoverageEventHandler coverageHandle } } - if (CIVisibility.Settings.CodeCoverageEnabled.HasValue) + if (_testOptimization.Settings.CodeCoverageEnabled.HasValue) { - var value = CIVisibility.Settings.CodeCoverageEnabled.Value ? "true" : "false"; + var value = _testOptimization.Settings.CodeCoverageEnabled.Value ? "true" : "false"; span.SetTag(CodeCoverageTags.Enabled, value); - if (CIVisibility.Settings.CodeCoverageEnabled.Value) + if (_testOptimization.Settings.CodeCoverageEnabled.Value) { // If we confirm that a module has code coverage enabled, we ensure we also have the session tag set // if not we leave the tag as is (other modules could have code coverage enabled) @@ -523,7 +527,7 @@ CoverageReporter.Handler is DefaultWithGlobalCoverageEventHandler coverageHandle OpenedTestModules.Remove(this); } - CIVisibility.Log.Debug("### Test Module Closed: {Name} | {Status}", Name, Tags.Status); + _testOptimization.Log.Debug("### Test Module Closed: {Name} | {Status}", Name, Tags.Status); if (_fakeSession is { } fakeSession) { @@ -642,13 +646,13 @@ internal bool EnableIpcClient() try { var name = $"session_{Tags.SessionId}"; - CIVisibility.Log.Debug("TestModule.Enabling IPC client: {Name}", name); + _testOptimization.Log.Debug("TestModule.Enabling IPC client: {Name}", name); _ipcClient = new IpcClient(name); return true; } catch (Exception ex) { - CIVisibility.Log.Error(ex, "Error enabling IPC client"); + _testOptimization.Log.Error(ex, "Error enabling IPC client"); return false; } } @@ -666,12 +670,12 @@ internal bool TrySetSessionTag(string name, string value) { try { - CIVisibility.Log.Debug("TestModule.Sending SetSessionTagMessage: {Name}={Value}", name, value); + _testOptimization.Log.Debug("TestModule.Sending SetSessionTagMessage: {Name}={Value}", name, value); return ipcClient.TrySendMessage(new SetSessionTagMessage(name, value)); } catch (Exception ex) { - CIVisibility.Log.Error(ex, "Error sending SetSessionTagMessage"); + _testOptimization.Log.Error(ex, "Error sending SetSessionTagMessage"); } } @@ -690,12 +694,12 @@ internal bool TrySetSessionTag(string name, double value) { try { - CIVisibility.Log.Debug("TestModule.Sending SetSessionTagMessage: {Name}={Value}", name, value); + _testOptimization.Log.Debug("TestModule.Sending SetSessionTagMessage: {Name}={Value}", name, value); return ipcClient.TrySendMessage(new SetSessionTagMessage(name, value)); } catch (Exception ex) { - CIVisibility.Log.Error(ex, "Error sending SetSessionTagMessage"); + _testOptimization.Log.Error(ex, "Error sending SetSessionTagMessage"); } } diff --git a/tracer/src/Datadog.Trace/Ci/TestOptimization.cs b/tracer/src/Datadog.Trace/Ci/TestOptimization.cs new file mode 100644 index 000000000000..7fc3429791a9 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/TestOptimization.cs @@ -0,0 +1,569 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Threading; +using System.Threading.Tasks; +using Datadog.Trace.Agent.DiscoveryService; +using Datadog.Trace.Ci.CiEnvironment; +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Ci.Net; +using Datadog.Trace.Configuration; +using Datadog.Trace.Logging; +using Datadog.Trace.Pdb; +using Datadog.Trace.Util; +using TaskExtensions = Datadog.Trace.ExtensionMethods.TaskExtensions; + +namespace Datadog.Trace.Ci; + +internal class TestOptimization : ITestOptimization +{ + private static ITestOptimization? _instance; + + private Lazy _enabledLazy; + private int _firstInitialization = 1; + private TestOptimizationSettings? _settings; + private ITestOptimizationClient? _client; + private Task? _additionalFeaturesTask; + private ITestOptimizationTracerManagement? _tracerManagement; + private ITestOptimizationHostInfo? _hostInfo; + private ITestOptimizationEarlyFlakeDetectionFeature? _earlyFlakeDetectionFeature; + private ITestOptimizationSkippableFeature? _skippableFeature; + private ITestOptimizationImpactedTestsDetectionFeature? _impactedTestsDetectionFeature; + private ITestOptimizationFlakyRetryFeature? _flakyRetryFeature; + + public TestOptimization() + { + _enabledLazy = new Lazy(InternalEnabled, true); + Log = DatadogLogging.GetLoggerFor(); + } + + public static ITestOptimization Instance + { + get => LazyInitializer.EnsureInitialized(ref _instance, () => new TestOptimization())!; + internal set => _instance = value; + } + + public static bool DefaultUseLockedTracerManager { get; set; } = true; + + public IDatadogLogger Log { get; } + + public bool IsRunning + { + get + { + // We try first the fast path, if the value is 0 we are running, so we can avoid the Interlocked operation. + if (_firstInitialization == 0) + { + return true; + } + + // If the value is not 0, maybe the value hasn't been updated yet, so we use the Interlocked operation to ensure the value is correct. + return Interlocked.CompareExchange(ref _firstInitialization, 0, 0) == 0; + } + } + + public bool Enabled => _enabledLazy.Value; + + public TestOptimizationSettings Settings + { + get => LazyInitializer.EnsureInitialized(ref _settings, () => TestOptimizationSettings.FromDefaultSources())!; + private set => _settings = value; + } + + public ITestOptimizationClient Client + { + get => LazyInitializer.EnsureInitialized(ref _client, () => TestOptimizationClient.CreateCached(Environment.CurrentDirectory, Settings))!; + private set => _client = value; + } + + public ITestOptimizationHostInfo HostInfo + { + get + { + if (_hostInfo is null) + { + _additionalFeaturesTask?.SafeWait(); + } + + return _hostInfo ??= new TestOptimizationHostInfo(); + } + private set => _hostInfo = value; + } + + public ITestOptimizationTracerManagement? TracerManagement + { + get + { + if (_tracerManagement is null) + { + _additionalFeaturesTask?.SafeWait(); + } + + return _tracerManagement; + } + private set => _tracerManagement = value; + } + + public ITestOptimizationEarlyFlakeDetectionFeature? EarlyFlakeDetectionFeature + { + get + { + if (_earlyFlakeDetectionFeature is null) + { + _additionalFeaturesTask?.SafeWait(); + } + + return _earlyFlakeDetectionFeature; + } + private set => _earlyFlakeDetectionFeature = value; + } + + public ITestOptimizationSkippableFeature? SkippableFeature + { + get + { + if (_skippableFeature is null) + { + _additionalFeaturesTask?.SafeWait(); + } + + return _skippableFeature; + } + private set => _skippableFeature = value; + } + + public ITestOptimizationImpactedTestsDetectionFeature? ImpactedTestsDetectionFeature + { + get + { + if (_impactedTestsDetectionFeature is null) + { + _additionalFeaturesTask?.SafeWait(); + } + + return _impactedTestsDetectionFeature; + } + private set => _impactedTestsDetectionFeature = value; + } + + public ITestOptimizationFlakyRetryFeature? FlakyRetryFeature + { + get + { + if (_flakyRetryFeature is null) + { + _additionalFeaturesTask?.SafeWait(); + } + + return _flakyRetryFeature; + } + private set => _flakyRetryFeature = value; + } + + public void Initialize() + { + if (Interlocked.Exchange(ref _firstInitialization, 0) != 1) + { + // Initialize() or InitializeFromRunner() or InitializeFromManualInstrumentation() was already called before + return; + } + + Log.Information("TestOptimization: Initializing CI Visibility"); + var settings = Settings; + + // In case we are running using the agent, check if the event platform proxy is supported. + TracerManagement = new TestOptimizationTracerManagement( + settings: Settings, + getDiscoveryServiceFunc: static s => DiscoveryService.Create( + s.TracerSettings.Exporter, + tcpTimeout: TimeSpan.FromSeconds(5), + initialRetryDelayMs: 10, + maxRetryDelayMs: 1000, + recheckIntervalMs: int.MaxValue), + useLockedTracerManager: DefaultUseLockedTracerManager); + + var eventPlatformProxyEnabled = TracerManagement.EventPlatformProxySupport != EventPlatformProxySupport.None; + if (eventPlatformProxyEnabled) + { + Log.Information("TestOptimization: EVP Proxy was enabled with mode: {Mode}", TracerManagement.EventPlatformProxySupport); + } + + LifetimeManager.Instance.AddAsyncShutdownTask(ShutdownAsync); + + var tracerSettings = settings.TracerSettings; + Log.Debug("TestOptimization: Setting up the test session name to: {TestSessionName}", settings.TestSessionName); + Log.Debug("TestOptimization: Setting up the service name to: {ServiceName}", tracerSettings.ServiceName); + + // Initialize Tracer + Log.Information("TestOptimization: Initialize Test Tracer instance"); + TracerManager.ReplaceGlobalManager( + tracerSettings, + new TestOptimizationTracerManagerFactory( + settings: settings, + testOptimizationTracerManagement: TracerManagement, + enabledEventPlatformProxy: eventPlatformProxyEnabled)); + _ = Tracer.Instance; + + // Initialize FrameworkDescription + _ = FrameworkDescription.Instance; + + // Initialize CIEnvironment + _ = CIEnvironmentValues.Instance; + + // If we are running in agentless mode or the agent support the event platform proxy endpoint. + // We can use the intelligent test runner + if (settings.Agentless || eventPlatformProxyEnabled) + { + var additionalFeaturesTask = InitializeAdditionalFeaturesAsync(); + _additionalFeaturesTask = additionalFeaturesTask; + LifetimeManager.Instance.AddAsyncShutdownTask(_ => additionalFeaturesTask); + } + else if (settings.IntelligentTestRunnerEnabled) + { + Log.Warning("TestOptimization: Intelligent test runner cannot be activated. Agent doesn't support the event platform proxy endpoint."); + } + else if (settings.GitUploadEnabled != false) + { + Log.Warning("TestOptimization: Upload git metadata cannot be activated. Agent doesn't support the event platform proxy endpoint."); + } + } + + public void InitializeFromRunner(TestOptimizationSettings settings, IDiscoveryService discoveryService, bool eventPlatformProxyEnabled, bool? useLockedTracerManager = null) + { + if (Interlocked.Exchange(ref _firstInitialization, 0) != 1) + { + // Initialize() or InitializeFromRunner() was already called before + return; + } + + Log.Information("TestOptimization: Initializing CI Visibility from dd-trace / runner"); + Settings = settings; + LifetimeManager.Instance.AddAsyncShutdownTask(ShutdownAsync); + + var tracerSettings = settings.TracerSettings; + Log.Debug("TestOptimization: Setting up the test session name to: {TestSessionName}", settings.TestSessionName); + Log.Debug("TestOptimization: Setting up the service name to: {ServiceName}", tracerSettings.ServiceName); + + // Initialize Tracer + Log.Information("TestOptimization: Initialize Test Tracer instance"); + TracerManagement = new TestOptimizationTracerManagement( + settings: Settings, + getDiscoveryServiceFunc: _ => discoveryService, + useLockedTracerManager: useLockedTracerManager ?? DefaultUseLockedTracerManager); + TracerManager.ReplaceGlobalManager( + tracerSettings, + new TestOptimizationTracerManagerFactory( + settings: settings, + testOptimizationTracerManagement: TracerManagement, + enabledEventPlatformProxy: eventPlatformProxyEnabled)); + _ = Tracer.Instance; + + // Initialize FrameworkDescription + _ = FrameworkDescription.Instance; + + // Initialize CIEnvironment + _ = CIEnvironmentValues.Instance; + + // Initialize features + var remoteSettings = TestOptimizationClient.CreateSettingsResponseFromTestOptimizationSettings(settings); + var client = new NoopTestOptimizationClient(); + FlakyRetryFeature = TestOptimizationFlakyRetryFeature.Create(settings, remoteSettings, client); + EarlyFlakeDetectionFeature = TestOptimizationEarlyFlakeDetectionFeature.Create(settings, remoteSettings, client); + ImpactedTestsDetectionFeature = TestOptimizationImpactedTestsDetectionFeature.Create(settings, remoteSettings, client); + SkippableFeature = TestOptimizationSkippableFeature.Create(settings, remoteSettings, client); + } + + public void InitializeFromManualInstrumentation() + { + if (!IsRunning) + { + // If we are using only the Public API without auto-instrumentation (TestSession/TestModule/TestSuite/Test classes only) + // then we can disable both GitUpload and Intelligent Test Runner feature (only used by our integration). + Settings.SetDefaultManualInstrumentationSettings(); + Initialize(); + } + } + + public void Flush() + { + TaskExtensions.SafeWait(funcTask: FlushAsync, millisecondsTimeout: 30_000); + } + + public async Task FlushAsync() + { + try + { + // We have to ensure the flush of the buffer after we finish the tests of an assembly. + // For some reason, sometimes when all test are finished none of the callbacks to handling the tracer disposal is triggered. + // So the last spans in buffer aren't send to the agent. + Log.Debug("TestOptimization: Integration flushing spans."); + + if (Settings.Logs) + { + await Task.WhenAll( + Tracer.Instance.FlushAsync(), + Tracer.Instance.TracerManager.DirectLogSubmission.Sink.FlushAsync()) + .ConfigureAwait(false); + } + else + { + await Tracer.Instance.FlushAsync().ConfigureAwait(false); + } + + Log.Debug("TestOptimization: Integration flushed."); + } + catch (Exception ex) + { + Log.Error(ex, "TestOptimization: Exception occurred when flushing spans."); + } + } + + /// + /// Manually close the CI Visibility mode triggering the LifeManager to run the shutdown tasks + /// This is required due to a weird behavior on the VSTest framework were the shutdown tasks are not awaited: + /// ` if testhost doesn't shut down within 100ms(as the execution is completed, we expect it to shutdown fast). + /// vstest.console forcefully kills the process.` + /// https://github.com/microsoft/vstest/issues/1900#issuecomment-457488472 + /// https://github.com/Microsoft/vstest/blob/2d4508232b6655a4f363b8bbcc887441c7d1d334/src/Microsoft.TestPlatform.CrossPlatEngine/Client/ProxyOperationManager.cs#L197 + /// + public void Close() + { + if (!IsRunning) + { + return; + } + + Log.Information("TestOptimization: CI Visibility is exiting."); + LifetimeManager.Instance.RunShutdownTasks(); + + // If the continuous profiler is attached we ensure to flush the remaining profiles before closing. + try + { + if (ContinuousProfiler.Profiler.Instance.Status.IsProfilerReady) + { + ContinuousProfiler.NativeInterop.FlushProfile(); + } + } + catch (Exception ex) + { + Log.Error(ex, "TestOptimization: Error flushing the profiler."); + } + + Interlocked.Exchange(ref _firstInitialization, 1); + } + + public void Reset() + { + _settings = null; + _client = null; + _firstInitialization = 1; + _enabledLazy = new(InternalEnabled, true); + _additionalFeaturesTask = null; + _tracerManagement = null; + _hostInfo = null; + _earlyFlakeDetectionFeature = null; + _skippableFeature = null; + _impactedTestsDetectionFeature = null; + _flakyRetryFeature = null; + } + + private bool InternalEnabled() + { + // By configuration + if (Settings.Enabled is { } enabled) + { + if (enabled) + { + Log.Information("TestOptimization: CI Visibility Enabled by Configuration"); + return true; + } + + // explicitly disabled + Log.Information("TestOptimization: CI Visibility Disabled by Configuration"); + return false; + } + + // Try to autodetect based in the domain name. + var domainName = AppDomain.CurrentDomain.FriendlyName ?? string.Empty; + if (domainName.StartsWith("testhost", StringComparison.Ordinal) || + domainName.StartsWith("xunit", StringComparison.Ordinal) || + domainName.StartsWith("nunit", StringComparison.Ordinal) || + domainName.StartsWith("MSBuild", StringComparison.Ordinal)) + { + Log.Information("TestOptimization: CI Visibility Enabled by Domain name whitelist"); + PropagateCiVisibilityEnvironmentVariable(); + return true; + } + + // Try to autodetect based in the process name. + var processName = GetProcessName(); + if (processName.StartsWith("testhost.", StringComparison.Ordinal)) + { + Log.Information("TestOptimization: CI Visibility Enabled by Process name whitelist"); + PropagateCiVisibilityEnvironmentVariable(); + return true; + } + + return false; + + void PropagateCiVisibilityEnvironmentVariable() + { + try + { + // Set the configuration key to propagate the configuration to child processes. + Environment.SetEnvironmentVariable(ConfigurationKeys.CIVisibility.Enabled, "1", EnvironmentVariableTarget.Process); + } + catch + { + // . + } + } + + string GetProcessName() + { + try + { + return ProcessHelpers.GetCurrentProcessName(); + } + catch (Exception exception) + { + Log.Warning(exception, "TestOptimization: Error getting current process name when checking CI Visibility status"); + } + + return string.Empty; + } + } + + private async Task ShutdownAsync(Exception? exception) + { + // Let's close any opened test, suite, modules and sessions before shutting down to avoid losing any data. + // But marking them as failed. + + foreach (var test in Test.ActiveTests) + { + if (exception is not null) + { + test.SetErrorInfo(exception); + } + + test.Close(TestStatus.Skip, null, "Test is being closed due to test session shutdown."); + } + + foreach (var testSuite in TestSuite.ActiveTestSuites) + { + if (exception is not null) + { + testSuite.SetErrorInfo(exception); + } + + testSuite.Close(); + } + + foreach (var testModule in TestModule.ActiveTestModules) + { + if (exception is not null) + { + testModule.SetErrorInfo(exception); + } + + await testModule.CloseAsync().ConfigureAwait(false); + } + + foreach (var testSession in TestSession.ActiveTestSessions) + { + if (exception is not null) + { + testSession.SetErrorInfo(exception); + } + + await testSession.CloseAsync(TestStatus.Skip).ConfigureAwait(false); + } + + await FlushAsync().ConfigureAwait(false); + MethodSymbolResolver.Instance.Clear(); + } + + private async Task InitializeAdditionalFeaturesAsync() + { + try + { + Log.Information("TestOptimization: Initializing additional features."); + var settings = Settings; + var client = Client; + + Task? uploadRepositoryChangesTask = null; + if (settings.GitUploadEnabled != false) + { + uploadRepositoryChangesTask = Task.Run(TryUploadRepositoryChangesAsync); + } + + // If any DD_CIVISIBILITY_CODE_COVERAGE_ENABLED or DD_CIVISIBILITY_TESTSSKIPPING_ENABLED has not been set + // We query the settings api for those + if (settings.CodeCoverageEnabled == null + || settings.TestsSkippingEnabled == null + || settings.EarlyFlakeDetectionEnabled != false + || settings.ImpactedTestsDetectionEnabled == null) + { + var remoteSettings = await client.GetSettingsAsync().ConfigureAwait(false); + + // we check if the backend require the git metadata first + if (remoteSettings.RequireGit == true && uploadRepositoryChangesTask is not null) + { + Log.Debug("TestOptimization: Require git received, awaiting for the git repository upload."); + await uploadRepositoryChangesTask.ConfigureAwait(false); + + Log.Debug("TestOptimization: Calling the configuration api again."); + remoteSettings = await client.GetSettingsAsync(skipFrameworkInfo: true).ConfigureAwait(false); + } + + FlakyRetryFeature = TestOptimizationFlakyRetryFeature.Create(settings, remoteSettings, client); + EarlyFlakeDetectionFeature = TestOptimizationEarlyFlakeDetectionFeature.Create(settings, remoteSettings, client); + ImpactedTestsDetectionFeature = TestOptimizationImpactedTestsDetectionFeature.Create(settings, remoteSettings, client); + SkippableFeature = TestOptimizationSkippableFeature.Create(settings, remoteSettings, client); + + if (settings.CodeCoverageEnabled == null && remoteSettings.CodeCoverage.HasValue) + { + Log.Information("TestOptimization: Code Coverage has been changed to {Value} by settings api.", remoteSettings.CodeCoverage.Value); + settings.SetCodeCoverageEnabled(remoteSettings.CodeCoverage.Value); + } + } + else + { + // For ITR we need the git metadata upload before consulting the skippable tests. + // If ITR is disabled we just need to make sure the git upload task has completed before leaving this method. + if (uploadRepositoryChangesTask is not null) + { + await uploadRepositoryChangesTask.ConfigureAwait(false); + } + + var remoteSettings = TestOptimizationClient.CreateSettingsResponseFromTestOptimizationSettings(settings); + FlakyRetryFeature = TestOptimizationFlakyRetryFeature.Create(settings, remoteSettings, client); + EarlyFlakeDetectionFeature = TestOptimizationEarlyFlakeDetectionFeature.Create(settings, remoteSettings, client); + ImpactedTestsDetectionFeature = TestOptimizationImpactedTestsDetectionFeature.Create(settings, remoteSettings, client); + SkippableFeature = TestOptimizationSkippableFeature.Create(settings, remoteSettings, client); + } + + Log.Information("TestOptimization: Additional features intialized."); + } + catch (Exception ex) + { + Log.Error(ex, "TestOptimization: Error initializing additional features."); + } + } + + private async Task TryUploadRepositoryChangesAsync() + { + try + { + await Client.UploadRepositoryChangesAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + Log.Error(ex, "TestOptimization: Error uploading repository git metadata."); + } + } +} diff --git a/tracer/src/Datadog.Trace/Ci/TestOptimizationEarlyFlakeDetectionFeature.cs b/tracer/src/Datadog.Trace/Ci/TestOptimizationEarlyFlakeDetectionFeature.cs new file mode 100644 index 000000000000..caf2b766015e --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/TestOptimizationEarlyFlakeDetectionFeature.cs @@ -0,0 +1,89 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System.Threading.Tasks; +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Ci.Net; +using Datadog.Trace.Logging; + +namespace Datadog.Trace.Ci; + +internal class TestOptimizationEarlyFlakeDetectionFeature : ITestOptimizationEarlyFlakeDetectionFeature +{ + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(TestOptimizationEarlyFlakeDetectionFeature)); + private readonly Task _earlyFlakeDetectionSettingsTask; + + private TestOptimizationEarlyFlakeDetectionFeature(TestOptimizationSettings settings, TestOptimizationClient.SettingsResponse clientSettingsResponse, ITestOptimizationClient testOptimizationClient) + { + if (settings is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(settings)); + } + + if (testOptimizationClient is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(testOptimizationClient)); + } + + EarlyFlakeDetectionSettings = clientSettingsResponse.EarlyFlakeDetection; + + if (settings.EarlyFlakeDetectionEnabled == true || clientSettingsResponse.EarlyFlakeDetection.Enabled == true) + { + Log.Information("TestOptimizationEarlyFlakeDetectionFeature: Early flake detection is enabled."); + settings.SetEarlyFlakeDetectionEnabled(true); + _earlyFlakeDetectionSettingsTask = Task.Run(() => InternalGetEarlyFlakeDetectionSettingsAsync(testOptimizationClient)); + Enabled = true; + } + else + { + Log.Information("TestOptimizationEarlyFlakeDetectionFeature: Early flake detection is disabled."); + settings.SetEarlyFlakeDetectionEnabled(false); + _earlyFlakeDetectionSettingsTask = Task.FromResult(new TestOptimizationClient.EarlyFlakeDetectionResponse()); + Enabled = false; + } + + return; + + static async Task InternalGetEarlyFlakeDetectionSettingsAsync(ITestOptimizationClient testOptimizationClient) + { + Log.Debug("TestOptimizationEarlyFlakeDetectionFeature: Getting early flake detection data..."); + var response = await testOptimizationClient.GetEarlyFlakeDetectionTestsAsync().ConfigureAwait(false); + Log.Debug("TestOptimizationEarlyFlakeDetectionFeature: Early flake detection data received."); + return response; + } + } + + public bool Enabled { get; } + + public TestOptimizationClient.EarlyFlakeDetectionSettingsResponse EarlyFlakeDetectionSettings { get; } + + public TestOptimizationClient.EarlyFlakeDetectionResponse? EarlyFlakeDetectionResponse + => _earlyFlakeDetectionSettingsTask.SafeGetResult(); + + public static ITestOptimizationEarlyFlakeDetectionFeature Create(TestOptimizationSettings settings, TestOptimizationClient.SettingsResponse clientSettingsResponse, ITestOptimizationClient testOptimizationClient) + => new TestOptimizationEarlyFlakeDetectionFeature(settings, clientSettingsResponse, testOptimizationClient); + + public bool IsAnEarlyFlakeDetectionTest(string moduleName, string testSuite, string testName) + { + if (EarlyFlakeDetectionResponse is { Tests: { } efdTests } && + efdTests.TryGetValue(moduleName, out var efdResponseSuites) && + efdResponseSuites?.TryGetValue(testSuite, out var efdResponseTests) == true && + efdResponseTests is not null) + { + foreach (var test in efdResponseTests) + { + if (test == testName) + { + Log.Debug("TestOptimizationEarlyFlakeDetectionFeature: Test is included in the early flake detection response. [ModuleName: {ModuleName}, TestSuite: {TestSuite}, TestName: {TestName}]", moduleName, testSuite, testName); + return true; + } + } + } + + Log.Debug("TestOptimizationEarlyFlakeDetectionFeature: Test is not in the early flake detection response. [ModuleName: {ModuleName}, TestSuite: {TestSuite}, TestName: {TestName}]", moduleName, testSuite, testName); + return false; + } +} diff --git a/tracer/src/Datadog.Trace/Ci/TestOptimizationFlakyRetryFeature.cs b/tracer/src/Datadog.Trace/Ci/TestOptimizationFlakyRetryFeature.cs new file mode 100644 index 000000000000..2e667171102c --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/TestOptimizationFlakyRetryFeature.cs @@ -0,0 +1,51 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Ci.Net; +using Datadog.Trace.Logging; + +namespace Datadog.Trace.Ci; + +internal class TestOptimizationFlakyRetryFeature : ITestOptimizationFlakyRetryFeature +{ + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(TestOptimizationFlakyRetryFeature)); + + private TestOptimizationFlakyRetryFeature(TestOptimizationSettings settings, TestOptimizationClient.SettingsResponse clientSettingsResponse, ITestOptimizationClient testOptimizationClient) + { + if (settings is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(settings)); + } + + if (testOptimizationClient is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(testOptimizationClient)); + } + + if (settings.FlakyRetryEnabled == null && clientSettingsResponse.FlakyTestRetries.HasValue) + { + Log.Information("TestOptimizationFlakyRetryFeature: Flaky retries has been changed to {Value} by the settings api.", clientSettingsResponse.FlakyTestRetries.Value); + settings.SetFlakyRetryEnabled(clientSettingsResponse.FlakyTestRetries.Value); + } + + if (settings.FlakyRetryEnabled == true) + { + Log.Information("TestOptimizationFlakyRetryFeature: Flaky retries is enabled."); + Enabled = true; + } + else + { + Log.Information("TestOptimizationFlakyRetryFeature: Flaky retries is disabled."); + Enabled = false; + } + } + + public bool Enabled { get; } + + public static ITestOptimizationFlakyRetryFeature Create(TestOptimizationSettings settings, TestOptimizationClient.SettingsResponse clientSettingsResponse, ITestOptimizationClient testOptimizationClient) + => new TestOptimizationFlakyRetryFeature(settings, clientSettingsResponse, testOptimizationClient); +} diff --git a/tracer/src/Datadog.Trace/Ci/TestOptimizationHostInfo.cs b/tracer/src/Datadog.Trace/Ci/TestOptimizationHostInfo.cs new file mode 100644 index 000000000000..1105c524d791 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/TestOptimizationHostInfo.cs @@ -0,0 +1,56 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using Datadog.Trace.Logging; +using Datadog.Trace.PlatformHelpers; +using Datadog.Trace.Util; + +namespace Datadog.Trace.Ci; + +internal class TestOptimizationHostInfo : ITestOptimizationHostInfo +{ + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(TestOptimizationHostInfo)); + private string? _osVersion; + + public string GetOperatingSystemVersion() + => _osVersion ??= GetOperatingSystemVersionInternal(); + + private string GetOperatingSystemVersionInternal() + { + switch (FrameworkDescription.Instance.OSPlatform) + { + case OSPlatformName.Linux: + if (!string.IsNullOrEmpty(HostMetadata.Instance.KernelRelease)) + { + return HostMetadata.Instance.KernelRelease!; + } + + break; + case OSPlatformName.MacOS: + try + { + // Executes the command "uname -r" to fetch the macOS version. + var osxVersionCommand = ProcessHelpers.RunCommand(new ProcessHelpers.Command("uname", "-r")); + var osxVersion = osxVersionCommand?.Output.Trim(' ', '\n'); + if (!string.IsNullOrEmpty(osxVersion)) + { + return osxVersion!; + } + } + catch (Exception ex) + { + // Log the exception if retrieving the macOS version fails. + Log.Warning(ex, "TestOptimizationHostInfo: Error getting OS version on macOS"); + } + + break; + } + + // Fallback to the default OS version string. + return Environment.OSVersion.VersionString; + } +} diff --git a/tracer/src/Datadog.Trace/Ci/TestOptimizationImpactedTestsDetectionFeature.cs b/tracer/src/Datadog.Trace/Ci/TestOptimizationImpactedTestsDetectionFeature.cs new file mode 100644 index 000000000000..44c9828edfa4 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/TestOptimizationImpactedTestsDetectionFeature.cs @@ -0,0 +1,68 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System.Threading.Tasks; +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Ci.Net; +using Datadog.Trace.Logging; + +namespace Datadog.Trace.Ci; + +internal class TestOptimizationImpactedTestsDetectionFeature : ITestOptimizationImpactedTestsDetectionFeature +{ + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(TestOptimizationImpactedTestsDetectionFeature)); + private readonly Task _impactedTestsDetectionFilesTask; + + private TestOptimizationImpactedTestsDetectionFeature(TestOptimizationSettings settings, TestOptimizationClient.SettingsResponse clientSettingsResponse, ITestOptimizationClient testOptimizationClient) + { + if (settings is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(settings)); + } + + if (testOptimizationClient is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(testOptimizationClient)); + } + + if (settings.ImpactedTestsDetectionEnabled == null && clientSettingsResponse.ImpactedTestsEnabled.HasValue) + { + Log.Information("TestOptimizationImpactedTestsDetectionFeature: Impacted tests detection has been changed to {Value} by the settings api.", clientSettingsResponse.ImpactedTestsEnabled.Value); + settings.SetImpactedTestsEnabled(clientSettingsResponse.ImpactedTestsEnabled.Value); + } + + if (settings.ImpactedTestsDetectionEnabled == true) + { + Log.Information("TestOptimizationImpactedTestsDetectionFeature: Impacted tests detection is enabled."); + _impactedTestsDetectionFilesTask = Task.Run(() => InternalGetImpactedTestsDetectionFilesAsync(testOptimizationClient)); + Enabled = true; + } + else + { + Log.Information("TestOptimizationImpactedTestsDetectionFeature: Impacted tests detection is disabled."); + _impactedTestsDetectionFilesTask = Task.FromResult(default(TestOptimizationClient.ImpactedTestsDetectionResponse)); + Enabled = false; + } + + return; + + static async Task InternalGetImpactedTestsDetectionFilesAsync(ITestOptimizationClient testOptimizationClient) + { + Log.Debug("TestOptimizationImpactedTestsDetectionFeature: Getting impacted tests detection modified files..."); + var response = await testOptimizationClient.GetImpactedTestsDetectionFilesAsync().ConfigureAwait(false); + Log.Debug("TestOptimizationImpactedTestsDetectionFeature: Impacted tests detection modified files received."); + return response; + } + } + + public bool Enabled { get; } + + public TestOptimizationClient.ImpactedTestsDetectionResponse ImpactedTestsDetectionResponse + => _impactedTestsDetectionFilesTask.SafeGetResult(); + + public static ITestOptimizationImpactedTestsDetectionFeature Create(TestOptimizationSettings settings, TestOptimizationClient.SettingsResponse clientSettingsResponse, ITestOptimizationClient testOptimizationClient) + => new TestOptimizationImpactedTestsDetectionFeature(settings, clientSettingsResponse, testOptimizationClient); +} diff --git a/tracer/src/Datadog.Trace/Ci/TestOptimizationSkippableFeature.cs b/tracer/src/Datadog.Trace/Ci/TestOptimizationSkippableFeature.cs new file mode 100644 index 000000000000..a3f3103f8258 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/TestOptimizationSkippableFeature.cs @@ -0,0 +1,140 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Ci.Net; +using Datadog.Trace.Logging; + +namespace Datadog.Trace.Ci; + +internal class TestOptimizationSkippableFeature : ITestOptimizationSkippableFeature +{ + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(TestOptimizationSkippableFeature)); + private readonly Task _skippableTestsTask; + + private TestOptimizationSkippableFeature(TestOptimizationSettings settings, TestOptimizationClient.SettingsResponse clientSettingsResponse, ITestOptimizationClient testOptimizationClient) + { + if (settings is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(settings)); + } + + if (testOptimizationClient is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(testOptimizationClient)); + } + + if (settings.TestsSkippingEnabled == null && clientSettingsResponse.TestsSkipping.HasValue) + { + Log.Information("TestOptimizationSkippableFeature: Tests Skipping has been changed to {Value} by settings api.", clientSettingsResponse.TestsSkipping.Value); + settings.SetTestsSkippingEnabled(clientSettingsResponse.TestsSkipping.Value); + } + + if (settings.TestsSkippingEnabled == true) + { + Log.Information("TestOptimizationSkippableFeature: Test skipping is enabled."); + _skippableTestsTask = Task.Run(() => InternalGetSkippableTestsAsync(testOptimizationClient)); + Enabled = true; + } + else + { + Log.Information("TestOptimizationSkippableFeature: Test skipping is disabled."); + _skippableTestsTask = Task.FromResult(new SkippableTestsDictionary()); + Enabled = false; + } + + return; + + static async Task InternalGetSkippableTestsAsync(ITestOptimizationClient testOptimizationClient) + { + // For ITR we need the git metadata upload before consulting the skippable tests. + // If ITR is disabled we just need to make sure the git upload task has completed before leaving this method. + await testOptimizationClient.UploadRepositoryChangesAsync().ConfigureAwait(false); + + Log.Debug("TestOptimizationSkippableFeature: Getting skippable tests..."); + var skippeableTests = await testOptimizationClient.GetSkippableTestsAsync().ConfigureAwait(false); + Log.Information("TestOptimizationSkippableFeature: CorrelationId = {CorrelationId}, SkippableTests = {Length}.", skippeableTests.CorrelationId, skippeableTests.Tests.Count); + + var skippableTestsBySuiteAndName = new SkippableTestsDictionary(); + foreach (var item in skippeableTests.Tests) + { + if (!skippableTestsBySuiteAndName.TryGetValue(item.Suite, out var suite)) + { + suite = new Dictionary>(); + skippableTestsBySuiteAndName[item.Suite] = suite; + } + + if (!suite.TryGetValue(item.Name, out var name)) + { + name = new List(); + suite[item.Name] = name; + } + + name.Add(item); + } + + skippableTestsBySuiteAndName.CorrelationId = skippeableTests.CorrelationId; + Log.Debug("TestOptimizationSkippableFeature: SkippableTests dictionary has been built."); + return skippableTestsBySuiteAndName; + } + } + + public bool Enabled { get; } + + public static ITestOptimizationSkippableFeature Create(TestOptimizationSettings settings, TestOptimizationClient.SettingsResponse clientSettingsResponse, ITestOptimizationClient testOptimizationClient) + => new TestOptimizationSkippableFeature(settings, clientSettingsResponse, testOptimizationClient); + + public void WaitForSkippableTaskToFinish() + { + try + { + _skippableTestsTask.SafeWait(); + } + catch (Exception ex) + { + Log.Error(ex, "TestOptimizationSkippableFeature: Error waiting for skippable tests task to finish."); + } + } + + public IList GetSkippableTestsFromSuiteAndName(string suite, string name) + { + WaitForSkippableTaskToFinish(); + return InternalGetSkippableTestsFromSuiteAndName(suite, name); + } + + private IList InternalGetSkippableTestsFromSuiteAndName(string suite, string name) + { + var skippableTestsBySuiteAndName = _skippableTestsTask.SafeGetResult(); + if (skippableTestsBySuiteAndName.TryGetValue(suite, out var testsInSuite) && + testsInSuite.TryGetValue(name, out var tests)) + { + return tests; + } + + return []; + } + + public bool HasSkippableTests() + { + var skippableTestsBySuiteAndName = _skippableTestsTask.SafeGetResult(); + return skippableTestsBySuiteAndName.Count > 0; + } + + public string? GetCorrelationId() + { + var skippableTestsBySuiteAndName = _skippableTestsTask.SafeGetResult(); + return skippableTestsBySuiteAndName.CorrelationId; + } + + internal class SkippableTestsDictionary : Dictionary>> + { + public string? CorrelationId { get; set; } + } +} diff --git a/tracer/src/Datadog.Trace/Ci/TestOptimizationTracerManagement.cs b/tracer/src/Datadog.Trace/Ci/TestOptimizationTracerManagement.cs new file mode 100644 index 000000000000..b120a5a226f0 --- /dev/null +++ b/tracer/src/Datadog.Trace/Ci/TestOptimizationTracerManagement.cs @@ -0,0 +1,275 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Net; +using System.Text.RegularExpressions; +using System.Threading; +using Datadog.Trace.Agent; +using Datadog.Trace.Agent.DiscoveryService; +using Datadog.Trace.Agent.Transports; +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Configuration; +using Datadog.Trace.HttpOverStreams; +using Datadog.Trace.Logging; + +namespace Datadog.Trace.Ci; + +internal class TestOptimizationTracerManagement : ITestOptimizationTracerManagement +{ + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(typeof(TestOptimizationTracerManagement)); + private readonly TestOptimizationSettings _settings; + + public TestOptimizationTracerManagement( + TestOptimizationSettings settings, + Func? getDiscoveryServiceFunc, + bool? useLockedTracerManager) + { + if (settings is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(settings)); + } + + _settings = settings; + DiscoveryService = NullDiscoveryService.Instance; + if (!settings.Agentless) + { + if (!string.IsNullOrWhiteSpace(settings.ForceAgentsEvpProxy)) + { + // if we force the evp proxy (internal switch) + if (Enum.TryParse(settings.ForceAgentsEvpProxy, out var parsedValue)) + { + EventPlatformProxySupport = parsedValue; + } + else if (getDiscoveryServiceFunc != null) + { + DiscoveryService = getDiscoveryServiceFunc(settings); + EventPlatformProxySupport = IsEventPlatformProxySupportedByAgent(DiscoveryService); + } + else + { + EventPlatformProxySupport = EventPlatformProxySupport.V2; + } + } + else + { + DiscoveryService = getDiscoveryServiceFunc?.Invoke(settings) ?? NullDiscoveryService.Instance; + EventPlatformProxySupport = IsEventPlatformProxySupportedByAgent(DiscoveryService); + } + } + else + { + EventPlatformProxySupport = EventPlatformProxySupport.None; + } + + UseLockedTracerManager = useLockedTracerManager ?? TestOptimization.DefaultUseLockedTracerManager; + } + + public TestOptimizationTracerManagement( + TestOptimizationSettings settings, + EventPlatformProxySupport eventPlatformProxySupport = EventPlatformProxySupport.None, + bool useLockedTracerManager = true) + { + if (settings is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(settings)); + } + + _settings = settings; + DiscoveryService = NullDiscoveryService.Instance; + EventPlatformProxySupport = + eventPlatformProxySupport != EventPlatformProxySupport.None && + Enum.TryParse(settings.ForceAgentsEvpProxy, out var parsedValue) + ? parsedValue + : eventPlatformProxySupport; + UseLockedTracerManager = useLockedTracerManager; + } + + public EventPlatformProxySupport EventPlatformProxySupport { get; } + + public bool UseLockedTracerManager { get; } + + public IDiscoveryService DiscoveryService { get; } + + public TestOptimizationTracerManager? Manager + { + get + { + if (Tracer.Instance.TracerManager is TestOptimizationTracerManager ciTracerManager) + { + return ciTracerManager; + } + + return null; + } + } + + public EventPlatformProxySupport IsEventPlatformProxySupportedByAgent(IDiscoveryService discoveryService) + { + if (discoveryService is NullDiscoveryService) + { + return EventPlatformProxySupport.None; + } + + Log.Debug("TestOptimizationTracerManagement: Waiting for agent configuration..."); + var agentConfiguration = new DiscoveryAgentConfigurationCallback(discoveryService).WaitAndGet(5_000); + if (agentConfiguration is null) + { + Log.Warning("TestOptimizationTracerManagement: Discovery service could not retrieve the agent configuration after 5 seconds."); + return EventPlatformProxySupport.None; + } + + var eventPlatformProxyEndpoint = agentConfiguration.EventPlatformProxyEndpoint; + return EventPlatformProxySupportFromEndpointUrl(eventPlatformProxyEndpoint); + } + + public EventPlatformProxySupport EventPlatformProxySupportFromEndpointUrl(string? eventPlatformProxyEndpoint) + { + if (!string.IsNullOrEmpty(eventPlatformProxyEndpoint)) + { + if (eventPlatformProxyEndpoint?.Contains("/v2") == true) + { + Log.Information("TestOptimizationTracerManagement: Event platform proxy V2 supported by agent."); + return EventPlatformProxySupport.V2; + } + + if (eventPlatformProxyEndpoint?.Contains("/v4") == true) + { + Log.Information("TestOptimizationTracerManagement: Event platform proxy V4 supported by agent."); + return EventPlatformProxySupport.V4; + } + + Log.Information("TestOptimizationTracerManagement: EventPlatformProxyEndpoint: '{EVPEndpoint}' not supported.", eventPlatformProxyEndpoint); + } + else + { + Log.Information("TestOptimizationTracerManagement: Event platform proxy is not supported by the agent. Falling back to the APM protocol."); + } + + return EventPlatformProxySupport.None; + } + + public IApiRequestFactory GetRequestFactory(TracerSettings settings) + { + return GetRequestFactory(settings, TimeSpan.FromSeconds(15)); + } + + public IApiRequestFactory GetRequestFactory(TracerSettings tracerSettings, TimeSpan timeout) + { + IApiRequestFactory? factory; + var exporterSettings = tracerSettings.Exporter; + if (exporterSettings.TracesTransport != TracesTransportType.Default) + { + factory = AgentTransportStrategy.Get( + exporterSettings, + productName: "CI Visibility", + tcpTimeout: null, + AgentHttpHeaderNames.DefaultHeaders, + () => new TraceAgentHttpHeaderHelper(), + uri => uri); + } + else + { +#if NETCOREAPP + Log.Information("TestOptimizationTracerManagement: Using {FactoryType} for trace transport.", nameof(HttpClientRequestFactory)); + factory = new HttpClientRequestFactory( + exporterSettings.AgentUri, + AgentHttpHeaderNames.DefaultHeaders, + handler: new System.Net.Http.HttpClientHandler { AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, }, + timeout: timeout); +#else + Log.Information("TestOptimizationTracerManagement: Using {FactoryType} for trace transport.", nameof(ApiWebRequestFactory)); + factory = new ApiWebRequestFactory(tracerSettings.Exporter.AgentUri, AgentHttpHeaderNames.DefaultHeaders, timeout: timeout); +#endif + if (!string.IsNullOrWhiteSpace(_settings.ProxyHttps)) + { + var proxyHttpsUriBuilder = new UriBuilder(_settings.ProxyHttps!); + + var userName = proxyHttpsUriBuilder.UserName; + var password = proxyHttpsUriBuilder.Password; + + proxyHttpsUriBuilder.UserName = string.Empty; + proxyHttpsUriBuilder.Password = string.Empty; + + if (proxyHttpsUriBuilder.Scheme == "https") + { + // HTTPS proxy is not supported by .NET BCL + Log.Error("TestOptimizationTracerManagement: HTTPS proxy is not supported. ({ProxyHttpsUriBuilder})", proxyHttpsUriBuilder); + return factory; + } + + NetworkCredential? credential = null; + if (!string.IsNullOrWhiteSpace(userName)) + { + credential = new NetworkCredential(userName, password); + } + + Log.Information("TestOptimizationTracerManagement: Setting proxy to: {ProxyHttps}", proxyHttpsUriBuilder.Uri.ToString()); + factory.SetProxy(new WebProxy(proxyHttpsUriBuilder.Uri, true, _settings.ProxyNoProxy, credential), credential); + } + } + + return factory; + } + + public string GetServiceNameFromRepository(string? repository) + { + if (string.IsNullOrEmpty(repository)) + { + return string.Empty; + } + + if (repository!.EndsWith("/") || repository.EndsWith("\\")) + { + repository = repository.Substring(0, repository.Length - 1); + } + + const string gitSuffix = ".git"; + var regex = new Regex(@"[/\\]?([a-zA-Z0-9\-_.]*)$"); + var match = regex.Match(repository); + if (match is { Success: true, Groups.Count: > 1 }) + { + var repoName = match.Groups[1].Value; + return repoName.EndsWith(gitSuffix) ? repoName.Substring(0, repoName.Length - gitSuffix.Length) : repoName; + } + + return string.Empty; + } + + private class DiscoveryAgentConfigurationCallback + { + private readonly ManualResetEventSlim _manualResetEventSlim; + private readonly Action _callback; + private readonly IDiscoveryService _discoveryService; + private AgentConfiguration? _agentConfiguration; + + public DiscoveryAgentConfigurationCallback(IDiscoveryService discoveryService) + { + _manualResetEventSlim = new ManualResetEventSlim(); + LifetimeManager.Instance.AddShutdownTask(_ => _manualResetEventSlim.Set()); + _discoveryService = discoveryService; + _callback = Callback; + _agentConfiguration = null; + _discoveryService.SubscribeToChanges(_callback); + } + + // Waits for the agent configuration to be received, up to the specified timeout. + public AgentConfiguration? WaitAndGet(int timeoutInMs = 5_000) + { + _manualResetEventSlim.Wait(timeoutInMs); + return _agentConfiguration; + } + + // Callback method invoked when the agent configuration is received. + private void Callback(AgentConfiguration agentConfiguration) + { + _agentConfiguration = agentConfiguration; + _manualResetEventSlim.Set(); + _discoveryService.RemoveSubscription(_callback); + Log.Debug("TestOptimizationTracerManagement: Agent configuration received."); + } + } +} diff --git a/tracer/src/Datadog.Trace/Ci/CITracerManager.cs b/tracer/src/Datadog.Trace/Ci/TestOptimizationTracerManager.cs similarity index 95% rename from tracer/src/Datadog.Trace/Ci/CITracerManager.cs rename to tracer/src/Datadog.Trace/Ci/TestOptimizationTracerManager.cs index 6bf6e94f2b56..a256933821a8 100644 --- a/tracer/src/Datadog.Trace/Ci/CITracerManager.cs +++ b/tracer/src/Datadog.Trace/Ci/TestOptimizationTracerManager.cs @@ -1,4 +1,4 @@ -// +// // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // @@ -24,11 +24,11 @@ namespace Datadog.Trace.Ci { - internal class CITracerManager : TracerManager + internal class TestOptimizationTracerManager : TracerManager { - private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); + private static readonly IDatadogLogger Log = DatadogLogging.GetLoggerFor(); - public CITracerManager( + public TestOptimizationTracerManager( TracerSettings settings, IAgentWriter agentWriter, IScopeManager scopeManager, @@ -142,7 +142,7 @@ public void WriteEvent(IEvent @event) ((IEventWriter)AgentWriter).WriteEvent(@event); } - internal class LockedManager : CITracerManager, ILockedTracer + internal class LockedManager : TestOptimizationTracerManager, ILockedTracer { public LockedManager( TracerSettings settings, diff --git a/tracer/src/Datadog.Trace/Ci/CITracerManagerFactory.cs b/tracer/src/Datadog.Trace/Ci/TestOptimizationTracerManagerFactory.cs similarity index 69% rename from tracer/src/Datadog.Trace/Ci/CITracerManagerFactory.cs rename to tracer/src/Datadog.Trace/Ci/TestOptimizationTracerManagerFactory.cs index 9f7e654dc012..05881a34d401 100644 --- a/tracer/src/Datadog.Trace/Ci/CITracerManagerFactory.cs +++ b/tracer/src/Datadog.Trace/Ci/TestOptimizationTracerManagerFactory.cs @@ -1,7 +1,8 @@ -// +// // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -23,19 +24,17 @@ namespace Datadog.Trace.Ci { - internal class CITracerManagerFactory : TracerManagerFactory + internal class TestOptimizationTracerManagerFactory : TracerManagerFactory { - private readonly CIVisibilitySettings _settings; - private readonly IDiscoveryService _discoveryService; + private readonly TestOptimizationSettings _settings; private readonly bool _enabledEventPlatformProxy; - private readonly bool _useLockedManager; + private readonly ITestOptimizationTracerManagement _testOptimizationTracerManagement; - public CITracerManagerFactory(CIVisibilitySettings settings, IDiscoveryService discoveryService, bool enabledEventPlatformProxy = false, bool useLockedManager = true) + public TestOptimizationTracerManagerFactory(TestOptimizationSettings settings, ITestOptimizationTracerManagement testOptimizationTracerManagement, bool enabledEventPlatformProxy = false, bool useLockedManager = true) { _settings = settings; - _discoveryService = discoveryService; _enabledEventPlatformProxy = enabledEventPlatformProxy; - _useLockedManager = useLockedManager; + _testOptimizationTracerManagement = testOptimizationTracerManagement; } protected override TracerManager CreateTracerManagerFrom( @@ -56,13 +55,13 @@ protected override TracerManager CreateTracerManagerFrom( IDynamicConfigurationManager dynamicConfigurationManager, ITracerFlareManager tracerFlareManager) { - telemetry.RecordCiVisibilitySettings(_settings); - if (_useLockedManager) + telemetry.RecordTestOptimizationSettings(_settings); + if (_testOptimizationTracerManagement.UseLockedTracerManager) { - return new CITracerManager.LockedManager(settings, agentWriter, scopeManager, statsd, runtimeMetrics, logSubmissionManager, telemetry, discoveryService, dataStreamsManager, defaultServiceName, gitMetadataTagsProvider, traceSampler, spanSampler, remoteConfigurationManager, dynamicConfigurationManager, tracerFlareManager); + return new TestOptimizationTracerManager.LockedManager(settings, agentWriter, scopeManager, statsd, runtimeMetrics, logSubmissionManager, telemetry, discoveryService, dataStreamsManager, defaultServiceName, gitMetadataTagsProvider, traceSampler, spanSampler, remoteConfigurationManager, dynamicConfigurationManager, tracerFlareManager); } - return new CITracerManager(settings, agentWriter, scopeManager, statsd, runtimeMetrics, logSubmissionManager, telemetry, discoveryService, dataStreamsManager, defaultServiceName, gitMetadataTagsProvider, traceSampler, spanSampler, remoteConfigurationManager, dynamicConfigurationManager, tracerFlareManager); + return new TestOptimizationTracerManager(settings, agentWriter, scopeManager, statsd, runtimeMetrics, logSubmissionManager, telemetry, discoveryService, dataStreamsManager, defaultServiceName, gitMetadataTagsProvider, traceSampler, spanSampler, remoteConfigurationManager, dynamicConfigurationManager, tracerFlareManager); } protected override ITelemetryController CreateTelemetryController(TracerSettings settings, IDiscoveryService discoveryService) @@ -89,7 +88,7 @@ protected override IAgentWriter GetAgentWriter(TracerSettings settings, IDogStat { if (!string.IsNullOrEmpty(_settings.ApiKey)) { - return new CIVisibilityProtocolWriter(_settings, new CIWriterHttpSender(CIVisibility.GetRequestFactory(settings))); + return new CIVisibilityProtocolWriter(_settings, new CIWriterHttpSender(_testOptimizationTracerManagement.GetRequestFactory(settings))); } Environment.FailFast("An API key is required in Agentless mode."); @@ -99,7 +98,7 @@ protected override IAgentWriter GetAgentWriter(TracerSettings settings, IDogStat // With agent scenario: if (_enabledEventPlatformProxy) { - return new CIVisibilityProtocolWriter(_settings, new CIWriterHttpSender(CIVisibility.GetRequestFactory(settings))); + return new CIVisibilityProtocolWriter(_settings, new CIWriterHttpSender(_testOptimizationTracerManagement.GetRequestFactory(settings))); } // Event platform proxy not found @@ -109,6 +108,6 @@ protected override IAgentWriter GetAgentWriter(TracerSettings settings, IDogStat } protected override IDiscoveryService GetDiscoveryService(TracerSettings settings) - => _discoveryService; + => _testOptimizationTracerManagement.DiscoveryService; } } diff --git a/tracer/src/Datadog.Trace/Ci/TestSession.cs b/tracer/src/Datadog.Trace/Ci/TestSession.cs index 28bf8829bb4e..f05267f641a0 100644 --- a/tracer/src/Datadog.Trace/Ci/TestSession.cs +++ b/tracer/src/Datadog.Trace/Ci/TestSession.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -33,6 +34,7 @@ public sealed class TestSession private static readonly AsyncLocal CurrentSession = new(); private static readonly HashSet OpenedTestSessions = new(); + private readonly ITestOptimization _testOptimization; private readonly Span _span; private readonly Dictionary? _environmentVariablesToRestore = null; private IpcServer? _ipcServer = null; @@ -41,7 +43,8 @@ public sealed class TestSession private TestSession(string? command, string? workingDirectory, string? framework, DateTimeOffset? startDate, bool propagateEnvironmentVariables) { // First we make sure that CI Visibility is initialized. - CIVisibility.InitializeFromManualInstrumentation(); + _testOptimization = TestOptimization.Instance; + _testOptimization.InitializeFromManualInstrumentation(); var environment = CIEnvironmentValues.Instance; @@ -93,7 +96,7 @@ private TestSession(string? command, string? workingDirectory, string? framework OpenedTestSessions.Add(this); } - CIVisibility.Log.Debug("### Test Session Created: {Command}", command); + _testOptimization.Log.Debug("### Test Session Created: {Command}", command); if (startDate is null) { @@ -309,8 +312,8 @@ public void Close(TestStatus status, TimeSpan? duration) { if (InternalClose(status, duration)) { - CIVisibility.Log.Debug("### Test Session Flushing after close: {Command}", Command); - CIVisibility.Flush(); + _testOptimization.Log.Debug("### Test Session Flushing after close: {Command}", Command); + _testOptimization.Flush(); } } @@ -334,8 +337,8 @@ public Task CloseAsync(TestStatus status, TimeSpan? duration) { if (InternalClose(status, duration)) { - CIVisibility.Log.Debug("### Test Session Flushing after close: {Command}", Command); - return CIVisibility.FlushAsync(); + _testOptimization.Log.Debug("### Test Session Flushing after close: {Command}", Command); + return _testOptimization.FlushAsync(); } return Task.CompletedTask; @@ -402,7 +405,7 @@ internal bool InternalClose(TestStatus status, TimeSpan? duration) OpenedTestSessions.Remove(this); } - CIVisibility.Log.Debug("### Test Session Closed: {Command} | {Status}", Command, Tags.Status); + _testOptimization.Log.Debug("### Test Session Closed: {Command} | {Status}", Command, Tags.Status); return true; } @@ -492,21 +495,21 @@ internal bool EnableIpcServer() try { var name = $"session_{Tags.SessionId}"; - CIVisibility.Log.Debug("TestSession.Enabling IPC server: {Name}", name); + _testOptimization.Log.Debug("TestSession.Enabling IPC server: {Name}", name); _ipcServer = new IpcServer(name); _ipcServer.SetMessageReceivedCallback(OnIpcMessageReceived); return true; } catch (Exception ex) { - CIVisibility.Log.Error(ex, "Error enabling IPC server"); + _testOptimization.Log.Error(ex, "Error enabling IPC server"); return false; } } private void OnIpcMessageReceived(object message) { - CIVisibility.Log.Debug("TestSession.OnIpcMessageReceived: {Message}", message); + _testOptimization.Log.Debug("TestSession.OnIpcMessageReceived: {Message}", message); // If the session is already finished, we ignore the message if (Interlocked.CompareExchange(ref _finished, 1, 1) == 1) @@ -519,18 +522,18 @@ private void OnIpcMessageReceived(object message) { if (tagMessage.Value is not null) { - CIVisibility.Log.Information("TestSession.ReceiveMessage (meta): {Name}={Value}", tagMessage.Name, tagMessage.Value); + _testOptimization.Log.Information("TestSession.ReceiveMessage (meta): {Name}={Value}", tagMessage.Name, tagMessage.Value); SetTag(tagMessage.Name, tagMessage.Value); } else if (tagMessage.NumberValue is not null) { - CIVisibility.Log.Information("TestSession.ReceiveMessage (metric): {Name}={Value}", tagMessage.Name, tagMessage.NumberValue); + _testOptimization.Log.Information("TestSession.ReceiveMessage (metric): {Name}={Value}", tagMessage.Name, tagMessage.NumberValue); SetTag(tagMessage.Name, tagMessage.NumberValue); } } else if (message is SessionCodeCoverageMessage { Value: >= 0.0 } codeCoverageMessage) { - CIVisibility.Log.Information("TestSession.ReceiveMessage (code coverage): {Value}", codeCoverageMessage.Value); + _testOptimization.Log.Information("TestSession.ReceiveMessage (code coverage): {Value}", codeCoverageMessage.Value); // Adds the global code coverage percentage to the session SetTag(CodeCoverageTags.PercentageOfTotalLines, codeCoverageMessage.Value); diff --git a/tracer/src/Datadog.Trace/Ci/TestSuite.cs b/tracer/src/Datadog.Trace/Ci/TestSuite.cs index eb61fa62df09..7912dcf61a67 100644 --- a/tracer/src/Datadog.Trace/Ci/TestSuite.cs +++ b/tracer/src/Datadog.Trace/Ci/TestSuite.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -25,11 +26,13 @@ public sealed class TestSuite private static readonly AsyncLocal CurrentSuite = new(); private static readonly HashSet OpenedTestSuites = new(); + private readonly ITestOptimization _testOptimization; private readonly Span _span; private int _finished; internal TestSuite(TestModule module, string name, DateTimeOffset? startDate) { + _testOptimization = TestOptimization.Instance; Module = module; Name = name; @@ -54,7 +57,7 @@ internal TestSuite(TestModule module, string name, DateTimeOffset? startDate) OpenedTestSuites.Add(this); } - CIVisibility.Log.Debug("###### New Test Suite Created: {Name} ({Module})", Name, Module.Name); + _testOptimization.Log.Debug("###### New Test Suite Created: {Name} ({Module})", Name, Module.Name); if (startDate is null) { @@ -207,7 +210,7 @@ public void Close(TimeSpan? duration) } Module.RemoveSuite(Name); - CIVisibility.Log.Debug("###### Test Suite Closed: {Name} ({Module}) | {Status}", Name, Module.Name, Tags.Status); + _testOptimization.Log.Debug("###### Test Suite Closed: {Name} ({Module}) | {Status}", Name, Module.Name, Tags.Status); } /// diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Common.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Common.cs index 1ee9f9fc926f..530b1705e0b4 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Common.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Common.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -16,7 +17,7 @@ namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Testing; internal static class Common { - internal static readonly IDatadogLogger Log = CIVisibility.Log; + internal static readonly IDatadogLogger Log = TestOptimization.Instance.Log; internal static string GetParametersValueData(object? paramValue) { @@ -58,7 +59,7 @@ internal static bool ShouldSkip(string testSuite, string testName, object[]? tes try { SynchronizationContext.SetSynchronizationContext(null); - var skippableTests = AsyncUtil.RunSync(() => CIVisibility.GetSkippableTestsFromSuiteAndNameAsync(testSuite, testName)); + var skippableTests = TestOptimization.Instance.SkippableFeature?.GetSkippableTestsFromSuiteAndName(testSuite, testName) ?? []; if (skippableTests.Count > 0) { foreach (var skippableTest in skippableTests) @@ -122,7 +123,7 @@ testMethodArguments is not null && internal static int GetNumberOfExecutionsForDuration(TimeSpan duration) { int numberOfExecutions; - var slowRetriesSettings = CIVisibility.EarlyFlakeDetectionSettings.SlowTestRetries; + var slowRetriesSettings = TestOptimization.Instance.EarlyFlakeDetectionFeature?.EarlyFlakeDetectionSettings.SlowTestRetries ?? default; if (slowRetriesSettings.FiveSeconds.HasValue && duration.TotalSeconds < 5) { numberOfExecutions = slowRetriesSettings.FiveSeconds.Value; @@ -155,9 +156,10 @@ internal static int GetNumberOfExecutionsForDuration(TimeSpan duration) internal static void SetEarlyFlakeDetectionTestTagsAndAbortReason(Test test, bool isRetry, ref long newTestCases, ref long totalTestCases) { // Early flake detection flags - if (CIVisibility.Settings.EarlyFlakeDetectionEnabled == true) + var ciVisibility = TestOptimization.Instance; + if (ciVisibility.Settings.EarlyFlakeDetectionEnabled == true) { - var isTestNew = !CIVisibility.IsAnEarlyFlakeDetectionTest(test.Suite.Module.Name, test.Suite.Name, test.Name ?? string.Empty); + var isTestNew = !ciVisibility.EarlyFlakeDetectionFeature?.IsAnEarlyFlakeDetectionTest(test.Suite.Module.Name, test.Suite.Name, test.Name ?? string.Empty) ?? false; if (isTestNew) { test.SetTag(EarlyFlakeDetectionTags.TestIsNew, "true"); @@ -175,7 +177,7 @@ internal static void SetEarlyFlakeDetectionTestTagsAndAbortReason(Test test, boo internal static void SetFlakyRetryTags(Test test, bool isRetry) { - if (CIVisibility.Settings.FlakyRetryEnabled == true && isRetry) + if (TestOptimization.Instance.Settings.FlakyRetryEnabled == true && isRetry) { test.SetTag(EarlyFlakeDetectionTags.TestIsRetry, "true"); } @@ -183,7 +185,7 @@ internal static void SetFlakyRetryTags(Test test, bool isRetry) internal static void CheckFaultyThreshold(Test test, long nTestCases, long tTestCases) { - if (tTestCases > 0 && CIVisibility.EarlyFlakeDetectionSettings.FaultySessionThreshold is { } faultySessionThreshold and > 0 and < 100) + if (tTestCases > 0 && TestOptimization.Instance.EarlyFlakeDetectionFeature?.EarlyFlakeDetectionSettings.FaultySessionThreshold is { } faultySessionThreshold and > 0 and < 100) { if (((double)nTestCases * 100 / (double)tTestCases) > faultySessionThreshold) { diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/DotnetCommon.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/DotnetCommon.cs index 38d1a815cf1b..15d1238e475d 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/DotnetCommon.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/DotnetCommon.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -27,11 +28,11 @@ internal static class DotnetCommon internal const string DotnetTestIntegrationName = nameof(IntegrationId.DotnetTest); internal const IntegrationId DotnetTestIntegrationId = Configuration.IntegrationId.DotnetTest; - internal static readonly IDatadogLogger Log = Ci.CIVisibility.Log; + internal static readonly IDatadogLogger Log = TestOptimization.Instance.Log; private static bool? _isDataCollectorDomainCache; private static bool? _isMsBuildTaskCache; - internal static bool DotnetTestIntegrationEnabled => CIVisibility.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(DotnetTestIntegrationId); + internal static bool DotnetTestIntegrationEnabled => TestOptimization.Instance.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(DotnetTestIntegrationId); internal static bool IsDataCollectorDomain { @@ -103,9 +104,10 @@ internal static bool IsMsBuildTask return null; } - var ciVisibilitySettings = CIVisibility.Settings; - var agentless = ciVisibilitySettings.Agentless; - var isEvpProxy = CIVisibility.EventPlatformProxySupport != EventPlatformProxySupport.None; + var testOptimization = TestOptimization.Instance; + var testOptimizationSettings = testOptimization.Settings; + var agentless = testOptimizationSettings.Agentless; + var isEvpProxy = (testOptimization.TracerManagement?.EventPlatformProxySupport ?? EventPlatformProxySupport.None) != EventPlatformProxySupport.None; Log.Information("CreateSession: Agentless: {Agentless} | IsEvpProxy: {IsEvpProxy}", agentless, isEvpProxy); @@ -125,16 +127,16 @@ internal static bool IsMsBuildTask } var session = TestSession.InternalGetOrCreate(commandLine, workingDirectory, null, null, true); - session.SetTag(IntelligentTestRunnerTags.TestTestsSkippingEnabled, ciVisibilitySettings.TestsSkippingEnabled == true ? "true" : "false"); - session.SetTag(CodeCoverageTags.Enabled, ciVisibilitySettings.CodeCoverageEnabled == true ? "true" : "false"); - if (ciVisibilitySettings.EarlyFlakeDetectionEnabled == true) + session.SetTag(IntelligentTestRunnerTags.TestTestsSkippingEnabled, testOptimizationSettings.TestsSkippingEnabled == true ? "true" : "false"); + session.SetTag(CodeCoverageTags.Enabled, testOptimizationSettings.CodeCoverageEnabled == true ? "true" : "false"); + if (testOptimizationSettings.EarlyFlakeDetectionEnabled == true) { session.SetTag(EarlyFlakeDetectionTags.Enabled, "true"); } // At session level we know if the ITR is disabled (meaning that no tests will be skipped) // In that case we tell the backend no tests are going to be skipped. - if (ciVisibilitySettings.TestsSkippingEnabled == false) + if (testOptimizationSettings.TestsSkippingEnabled == false) { session.SetTag(IntelligentTestRunnerTags.TestsSkipped, "false"); } @@ -517,7 +519,7 @@ static void UpdateIndexes(List lstArgs, string codeCoverageCollectorPath { isCollectIndex = i; var argValue = arg.Replace(collectProperty, string.Empty) - .Replace("\"", string.Empty); + .Replace("\"", string.Empty); disableCollectInjection = disableCollectInjection || argValue == datadogCoverageCollector; continue; } @@ -526,7 +528,7 @@ static void UpdateIndexes(List lstArgs, string codeCoverageCollectorPath { isTestAdapterPathIndex = i; var argValue = arg.Replace(testAdapterPathProperty, string.Empty) - .Replace("\"", string.Empty); + .Replace("\"", string.Empty); disableTestAdapterInjection = disableTestAdapterInjection || argValue == codeCoverageCollectorPath; } } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/ExecutorGetArgumentProcessorsIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/ExecutorGetArgumentProcessorsIntegration.cs index f71c1d5ad2d7..c5294cb4c748 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/ExecutorGetArgumentProcessorsIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/ExecutorGetArgumentProcessorsIntegration.cs @@ -28,7 +28,7 @@ public class ExecutorGetArgumentProcessorsIntegration { internal static CallTargetState OnMethodBegin(TTarget instance, ref string[]? args, ref TProcessors? processors) { - if (!DotnetCommon.DotnetTestIntegrationEnabled || CIVisibility.Settings.CodeCoverageEnabled != true || args is null) + if (!DotnetCommon.DotnetTestIntegrationEnabled || TestOptimization.Instance.Settings.CodeCoverageEnabled != true || args is null) { return CallTargetState.GetDefault(); } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/TestCommand5ctorIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/TestCommand5ctorIntegration.cs index 5ba603f3884e..6d61f9630339 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/TestCommand5ctorIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/TestCommand5ctorIntegration.cs @@ -29,7 +29,7 @@ public class TestCommand5ctorIntegration { internal static CallTargetState OnMethodBegin(TTarget instance, ref IEnumerable? msbuildArgs, ref IEnumerable? userDefinedArguments, ref IEnumerable? trailingArguments, ref bool noRestore, ref string? msbuildPath) { - if (!DotnetCommon.DotnetTestIntegrationEnabled || CIVisibility.Settings.CodeCoverageEnabled != true || msbuildArgs is null) + if (!DotnetCommon.DotnetTestIntegrationEnabled || TestOptimization.Instance.Settings.CodeCoverageEnabled != true || msbuildArgs is null) { return CallTargetState.GetDefault(); } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/TestCommandctorIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/TestCommandctorIntegration.cs index 7c44d666f202..301d8e83a63f 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/TestCommandctorIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/DotnetTest/TestCommandctorIntegration.cs @@ -29,7 +29,7 @@ public class TestCommandctorIntegration { internal static CallTargetState OnMethodBegin(TTarget instance, ref IEnumerable? msbuildArgs, ref bool noRestore, ref string? msbuildPath) { - if (!DotnetCommon.DotnetTestIntegrationEnabled || CIVisibility.Settings.CodeCoverageEnabled != true || msbuildArgs is null) + if (!DotnetCommon.DotnetTestIntegrationEnabled || TestOptimization.Instance.Settings.CodeCoverageEnabled != true || msbuildArgs is null) { return CallTargetState.GetDefault(); } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/MsTestIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/MsTestIntegration.cs index 260007ef7438..26211410952f 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/MsTestIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/MsTestIntegration.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -83,7 +84,7 @@ internal static class MsTestIntegration private static long _totalTestCases; private static long _newTestCases; - internal static bool IsEnabled => CIVisibility.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(IntegrationId); + internal static bool IsEnabled => TestOptimization.Instance.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(IntegrationId); internal static Test? OnMethodBegin(TTestMethod testMethodInstance, Type type, bool isRetry, DateTimeOffset? startDate = null) where TTestMethod : ITestMethod @@ -112,7 +113,7 @@ internal static class MsTestIntegration if (GetTraits(testMethod) is { } testTraits) { // Unskippable tests - if (CIVisibility.Settings.IntelligentTestRunnerEnabled) + if (TestOptimization.Instance.Settings.IntelligentTestRunnerEnabled) { ShouldSkip(testMethodInstance, out var isUnskippable, out var isForcedRun, testTraits); test.SetTag(IntelligentTestRunnerTags.UnskippableTag, isUnskippable ? "true" : "false"); @@ -122,7 +123,7 @@ internal static class MsTestIntegration test.SetTraits(testTraits); } - else if (CIVisibility.Settings.IntelligentTestRunnerEnabled) + else if (TestOptimization.Instance.Settings.IntelligentTestRunnerEnabled) { // Unskippable tests test.SetTag(IntelligentTestRunnerTags.UnskippableTag, "false"); @@ -267,7 +268,7 @@ internal static bool ShouldSkip(TTestMethod testMethodInfo, out boo isUnskippable = false; isForcedRun = false; - if (CIVisibility.Settings.IntelligentTestRunnerEnabled != true) + if (TestOptimization.Instance.Settings.IntelligentTestRunnerEnabled != true) { return false; } @@ -289,7 +290,7 @@ internal static bool ShouldSkip(TTestMethod testMethodInfo, out boo return default; } - CIVisibility.WaitForSkippableTaskToFinish(); + TestOptimization.Instance.SkippableFeature?.WaitForSkippableTaskToFinish(); return TestModuleByTestAssemblyInfos.GetValue( objTestAssemblyInfo, diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/RunTestsWithSourcesSendSessionEndIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/RunTestsWithSourcesSendSessionEndIntegration.cs index 929e61346792..895965143d9d 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/RunTestsWithSourcesSendSessionEndIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/RunTestsWithSourcesSendSessionEndIntegration.cs @@ -26,7 +26,7 @@ public static class RunTestsWithSourcesSendSessionEndIntegration { internal static CallTargetState OnMethodBegin(TTarget instance) { - CIVisibility.Close(); + TestOptimization.Instance.Close(); return CallTargetState.GetDefault(); } } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestAssemblyInfoExecuteAssemblyCleanupIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestAssemblyInfoExecuteAssemblyCleanupIntegration.cs index 735b2cb7da32..a576f7359e8d 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestAssemblyInfoExecuteAssemblyCleanupIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestAssemblyInfoExecuteAssemblyCleanupIntegration.cs @@ -63,7 +63,7 @@ internal static CallTargetReturn OnMethodEnd(TTarget instance, Exceptio module.Close(); // Because we are auto-instrumenting a VSTest testhost process we need to manually call the shutdown process - CIVisibility.Close(); + TestOptimization.Instance.Close(); } return CallTargetReturn.GetDefault(); diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestAssemblyInfoRunAssemblyCleanupIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestAssemblyInfoRunAssemblyCleanupIntegration.cs index 15b52436b025..c2b041918af1 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestAssemblyInfoRunAssemblyCleanupIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestAssemblyInfoRunAssemblyCleanupIntegration.cs @@ -70,7 +70,7 @@ internal static CallTargetState OnMethodBegin(TTarget instance) module.Close(); // Because we are auto-instrumenting a VSTest testhost process we need to manually call the shutdown process - CIVisibility.Close(); + TestOptimization.Instance.Close(); } return new CallTargetReturn(returnValue); diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestMethodAttributeExecuteIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestMethodAttributeExecuteIntegration.cs index 85ea945a0539..a3fe8a802e77 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestMethodAttributeExecuteIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestMethodAttributeExecuteIntegration.cs @@ -137,11 +137,11 @@ internal static CallTargetState OnMethodBegin(TTarget inst returnValue = (TReturn?)GetFinalResults(results); } } - else if (CIVisibility.Settings.FlakyRetryEnabled == true && resultStatus == TestStatus.Fail) + else if (TestOptimization.Instance.Settings.FlakyRetryEnabled == true && resultStatus == TestStatus.Fail) { // Flaky retry is enabled and the test failed - Interlocked.CompareExchange(ref _totalRetries, CIVisibility.Settings.TotalFlakyRetryCount, -1); - var remainingRetries = CIVisibility.Settings.FlakyRetryCount; + Interlocked.CompareExchange(ref _totalRetries, TestOptimization.Instance.Settings.TotalFlakyRetryCount, -1); + var remainingRetries = TestOptimization.Instance.Settings.FlakyRetryCount; if (remainingRetries > 0) { // Handle retries @@ -151,7 +151,7 @@ internal static CallTargetState OnMethodBegin(TTarget inst { if (Interlocked.Decrement(ref _totalRetries) <= 0) { - Common.Log.Debug("FlakyRetry: Exceeded number of total retries. [{Number}]", CIVisibility.Settings.TotalFlakyRetryCount); + Common.Log.Debug("FlakyRetry: Exceeded number of total retries. [{Number}]", TestOptimization.Instance.Settings.TotalFlakyRetryCount); break; } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/UnitTestRunnerRunCleanupIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/UnitTestRunnerRunCleanupIntegration.cs index 2d000d04abcd..0a327e525b7a 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/UnitTestRunnerRunCleanupIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/UnitTestRunnerRunCleanupIntegration.cs @@ -43,7 +43,7 @@ public static class UnitTestRunnerRunCleanupIntegration module.Close(); // Because we are auto-instrumenting a VSTest testhost process we need to manually call the shutdown process - CIVisibility.Close(); + TestOptimization.Instance.Close(); } return new CallTargetReturn(returnValue); diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/CIVisibilityTestCommand.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/CIVisibilityTestCommand.cs index 45d2e752a6f2..90e9b4bc559a 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/CIVisibilityTestCommand.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/CIVisibilityTestCommand.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -24,10 +25,8 @@ public CIVisibilityTestCommand(ITestCommand innerCommand) [DuckReverseMethod] public object? Execute(object contextObject) { - Interlocked.CompareExchange(ref _totalRetries, CIVisibility.Settings.TotalFlakyRetryCount, -1); - var context = contextObject.TryDuckCast(out var contextWithRepeatCount) ? - contextWithRepeatCount : - contextObject.DuckCast(); + Interlocked.CompareExchange(ref _totalRetries, TestOptimization.Instance.Settings.TotalFlakyRetryCount, -1); + var context = contextObject.TryDuckCast(out var contextWithRepeatCount) ? contextWithRepeatCount : contextObject.DuckCast(); var executionNumber = 0; var result = ExecuteTest(context, executionNumber++, out var isTestNew, out var duration); var resultStatus = result.ResultState.Status; @@ -65,14 +64,14 @@ public CIVisibilityTestCommand(ITestCommand innerCommand) Common.Log.Debug("EFD: All retries were executed."); } } - else if (resultStatus == TestStatus.Failed && CIVisibility.Settings.FlakyRetryEnabled == true) + else if (resultStatus == TestStatus.Failed && TestOptimization.Instance.Settings.FlakyRetryEnabled == true) { // ************************************************************** // Flaky retry mode // ************************************************************** // Get retries number - var remainingRetries = CIVisibility.Settings.FlakyRetryCount; + var remainingRetries = TestOptimization.Instance.Settings.FlakyRetryCount; // Retries var retryNumber = 0; @@ -80,7 +79,7 @@ public CIVisibilityTestCommand(ITestCommand innerCommand) { if (Interlocked.Decrement(ref _totalRetries) <= 0) { - Common.Log.Debug("FlakyRetry: Exceeded number of total retries. [{Number}]", CIVisibility.Settings.TotalFlakyRetryCount); + Common.Log.Debug("FlakyRetry: Exceeded number of total retries. [{Number}]", TestOptimization.Instance.Settings.TotalFlakyRetryCount); break; } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitIntegration.cs index 1174cc158e8b..9648e7911052 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitIntegration.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -34,7 +35,7 @@ internal static class NUnitIntegration private static long _totalTestCases; private static long _newTestCases; - internal static bool IsEnabled => CIVisibility.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(IntegrationId); + internal static bool IsEnabled => TestOptimization.Instance.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(IntegrationId); internal static Test? GetOrCreateTest(ITest currentTest, int repeatCount = 0) { @@ -163,7 +164,7 @@ internal static bool ShouldSkip(ITest currentTest, out bool isUnskippable, out b isUnskippable = false; isForcedRun = false; - if (CIVisibility.Settings.IntelligentTestRunnerEnabled != true) + if (TestOptimization.Instance.Settings.IntelligentTestRunnerEnabled != true) { return false; } @@ -276,7 +277,7 @@ internal static void GetExceptionAndMessage(ITestResult result, out string excep if (traits?.Count > 0) { // Unskippable test - if (CIVisibility.Settings.IntelligentTestRunnerEnabled) + if (TestOptimization.Instance.Settings.IntelligentTestRunnerEnabled) { ShouldSkip(currentTest, out var isUnskippable, out var isForcedRun, traits); test.SetTag(IntelligentTestRunnerTags.UnskippableTag, isUnskippable ? "true" : "false"); @@ -289,7 +290,7 @@ internal static void GetExceptionAndMessage(ITestResult result, out string excep else { // Unskippable test - if (CIVisibility.Settings.IntelligentTestRunnerEnabled) + if (TestOptimization.Instance.Settings.IntelligentTestRunnerEnabled) { test.SetTag(IntelligentTestRunnerTags.UnskippableTag, "false"); test.SetTag(IntelligentTestRunnerTags.ForcedRunTag, "false"); diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitWorkItemPerformWorkIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitWorkItemPerformWorkIntegration.cs index 99d6e541032e..c46486a2afd2 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitWorkItemPerformWorkIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitWorkItemPerformWorkIntegration.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -52,7 +53,7 @@ internal static CallTargetState OnMethodBegin(TTarget instance) case "Assembly" when NUnitIntegration.GetTestModuleFrom(item) is null && item.Instance.TryDuckCast(out var itemAssembly): var assemblyName = itemAssembly.Assembly?.GetName().Name ?? string.Empty; var frameworkVersion = item.Type.Assembly.GetName().Version?.ToString() ?? string.Empty; - CIVisibility.WaitForSkippableTaskToFinish(); + TestOptimization.Instance.SkippableFeature?.WaitForSkippableTaskToFinish(); var newModule = TestModule.InternalCreate(assemblyName, CommonTags.TestingFrameworkNameNUnit, frameworkVersion); newModule.EnableIpcClient(); NUnitIntegration.SetTestModuleTo(item, newModule); @@ -64,7 +65,7 @@ internal static CallTargetState OnMethodBegin(TTarget instance) if (NUnitIntegration.ShouldSkip(item, out _, out _)) { var testMethod = item.Method?.MethodInfo; - Common.Log.Debug("ITR: Test skipped: {Class}.{Name}", testMethod?.DeclaringType?.FullName, testMethod?.Name); + Common.Log.Debug("NUnitWorkItemPerformWorkIntegration: Test skipped by test skipping feature: {Class}.{Name}", testMethod?.DeclaringType?.FullName, testMethod?.Name); item.RunState = RunState.Ignored; item.Properties.Set(NUnitIntegration.SkipReasonKey, IntelligentTestRunnerTags.SkippedByReason); } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitWorkItemWorkItemCompleteIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitWorkItemWorkItemCompleteIntegration.cs index 2950074007c4..83140359e795 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitWorkItemWorkItemCompleteIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/NUnit/NUnitWorkItemWorkItemCompleteIntegration.cs @@ -63,7 +63,7 @@ internal static CallTargetState OnMethodBegin(TTarget instance) module.Close(); // Because we are auto-instrumenting a VSTest testhost process we need to manually call the shutdown process - CIVisibility.Close(); + TestOptimization.Instance.Close(); break; case "TestFixture" when NUnitIntegration.GetTestSuiteFrom(item) is { } suite: diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/PlatformAssemblyResolverAssemblyResolverEventIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/PlatformAssemblyResolverAssemblyResolverEventIntegration.cs index e717a02f0f7b..023eb90a63f3 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/PlatformAssemblyResolverAssemblyResolverEventIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/PlatformAssemblyResolverAssemblyResolverEventIntegration.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -32,7 +33,7 @@ public class PlatformAssemblyResolverAssemblyResolverEventIntegration { private const string IntegrationName = "TestPlatformAssemblyResolver"; - private static readonly Assembly CiVisibilityAssembly = typeof(Ci.CIVisibility).Assembly; + private static readonly Assembly CiVisibilityAssembly = typeof(Ci.TestOptimization).Assembly; internal static CallTargetState OnMethodBegin(TTarget instance) { diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Selenium/SeleniumCommon.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Selenium/SeleniumCommon.cs index a5404a215e40..972641167d4c 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Selenium/SeleniumCommon.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Selenium/SeleniumCommon.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -25,6 +26,7 @@ internal static class SeleniumCommon internal const string CommandClose = "close"; internal const string CommandQuit = "quit"; private const string CookieName = "datadog-ci-visibility-test-execution-id"; + private const string RumStopSessionScript = """ if (window.DD_RUM && window.DD_RUM.stopSession) { window.DD_RUM.stopSession(); @@ -34,11 +36,11 @@ internal static class SeleniumCommon } """; - internal static readonly IDatadogLogger Log = CIVisibility.Log; + internal static readonly IDatadogLogger Log = TestOptimization.Instance.Log; private static Type? _seleniumCookieType; private static long _openPageCount; - internal static bool IsEnabled => CIVisibility.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(IntegrationId); + internal static bool IsEnabled => TestOptimization.Instance.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(IntegrationId); internal static void OnBeforePageLoad(TTarget instance, Dictionary? parameters) where TTarget : IWebDriverProxy => PreClose(instance); @@ -155,8 +157,8 @@ private static void CloseAndFlush(TTarget instance, Test? test) if (instance.ExecuteScript(RumStopSessionScript, null) is true) { test.GetTags().IsRumActive = "true"; - Log.Information("RUM flush script has been called, waiting for {RumFlushWaitMillis}ms.", CIVisibility.Settings.RumFlushWaitMillis); - Thread.Sleep(CIVisibility.Settings.RumFlushWaitMillis); + Log.Information("RUM flush script has been called, waiting for {RumFlushWaitMillis}ms.", TestOptimization.Instance.Settings.RumFlushWaitMillis); + Thread.Sleep(TestOptimization.Instance.Settings.RumFlushWaitMillis); } // Delete injected RUM session cookie diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XUnitTestAssemblyRunnerRunV3Integration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XUnitTestAssemblyRunnerRunV3Integration.cs index 0814ae18fdfd..4f916ab92c65 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XUnitTestAssemblyRunnerRunV3Integration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XUnitTestAssemblyRunnerRunV3Integration.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -57,7 +58,7 @@ internal static CallTargetState OnMethodBegin(TTarget instanc } } - CIVisibility.WaitForSkippableTaskToFinish(); + TestOptimization.Instance.SkippableFeature?.WaitForSkippableTaskToFinish(); var module = TestModule.InternalCreate(testBundleString, CommonTags.TestingFrameworkNameXUnitV3, frameworkType.Assembly.GetName().Version?.ToString() ?? string.Empty); module.EnableIpcClient(); return new CallTargetState(null, module); @@ -87,7 +88,7 @@ internal static async Task OnAsyncMethodEnd(TTarget i await testModule.CloseAsync().ConfigureAwait(false); // Because we are auto-instrumenting a VSTest testhost process we need to manually call the shutdown process - CIVisibility.Close(); + TestOptimization.Instance.Close(); } return returnValue; diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XUnitTestMethodRunnerBaseRunTestCaseV3Integration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XUnitTestMethodRunnerBaseRunTestCaseV3Integration.cs index 449c4f5ac581..67b7e2c1053c 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XUnitTestMethodRunnerBaseRunTestCaseV3Integration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XUnitTestMethodRunnerBaseRunTestCaseV3Integration.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; using System.ComponentModel; @@ -44,7 +45,7 @@ internal static CallTargetState OnMethodBegin(TTar return CallTargetState.GetDefault(); } - Interlocked.CompareExchange(ref _totalRetries, CIVisibility.Settings.TotalFlakyRetryCount, -1); + Interlocked.CompareExchange(ref _totalRetries, TestOptimization.Instance.Settings.TotalFlakyRetryCount, -1); var testcase = testcaseOriginal.DuckCast()!; var testRunnerData = new TestRunnerStruct @@ -65,7 +66,7 @@ internal static CallTargetState OnMethodBegin(TTar // Check if the test should be skipped by the ITR if (XUnitIntegration.ShouldSkip(ref testRunnerData, out _, out _)) { - Common.Log.Debug("ITR: Test skipped: {Class}.{Name}", testcase.TestClass?.ToString() ?? string.Empty, testcase.TestMethod?.Method.Name ?? string.Empty); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: Test skipped by test skipping feature: {Class}.{Name}", testcase.TestClass?.ToString() ?? string.Empty, testcase.TestMethod?.Method.Name ?? string.Empty); // Refresh values after skip reason change, and create Skip by ITR span. testcase.SkipReason = IntelligentTestRunnerTags.SkippedByReason; XUnitIntegration.CreateTest(ref testRunnerData); @@ -75,12 +76,12 @@ internal static CallTargetState OnMethodBegin(TTar if (testRunnerData.SkipReason is not null) { // Skip test support - Common.Log.Debug("Skipping test: {Class}.{Name} Reason: {Reason}", testcase.TestClass?.ToString() ?? string.Empty, testcase.TestMethod?.Method.Name ?? string.Empty, testRunnerData.SkipReason); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: Skipping test: {Class}.{Name} Reason: {Reason}", testcase.TestClass?.ToString() ?? string.Empty, testcase.TestMethod?.Method.Name ?? string.Empty, testRunnerData.SkipReason); XUnitIntegration.CreateTest(ref testRunnerData); return CallTargetState.GetDefault(); } - if (CIVisibility.Settings.EarlyFlakeDetectionEnabled != true && CIVisibility.Settings.FlakyRetryEnabled != true) + if (TestOptimization.Instance.Settings.EarlyFlakeDetectionEnabled != true && TestOptimization.Instance.Settings.FlakyRetryEnabled != true) { return CallTargetState.GetDefault(); } @@ -93,7 +94,7 @@ internal static CallTargetState OnMethodBegin(TTar retryMetadata = retryMessageBus.GetMetadata(testcase.UniqueID); if (retryMetadata.ExecutionIndex == 0) { - retryMetadata.FlakyRetryEnabled = CIVisibility.Settings.EarlyFlakeDetectionEnabled != true && CIVisibility.Settings.FlakyRetryEnabled == true; + retryMetadata.FlakyRetryEnabled = TestOptimization.Instance.Settings.EarlyFlakeDetectionEnabled != true && TestOptimization.Instance.Settings.FlakyRetryEnabled == true; } } @@ -121,14 +122,14 @@ context.Instance is not null && _runSummaryFieldCount ??= typeof(TReturn).GetFields().Length; if (_runSummaryFieldCount != 5) { - Common.Log.Warning("RunSummary type doesn't have the field count we are expecting. Flushing messages from RetryMessageBus"); + Common.Log.Warning("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: RunSummary type doesn't have the field count we are expecting. Flushing messages from RetryMessageBus"); retryMessageBus.FlushMessages(retryMetadata.UniqueID); return returnValue; } if (Marshal.SizeOf(returnValue) != Marshal.SizeOf()) { - Common.Log.Warning("RunSummary type doesn't have the size we are expecting. Flushing messages from RetryMessageBus"); + Common.Log.Warning("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: RunSummary type doesn't have the size we are expecting. Flushing messages from RetryMessageBus"); retryMessageBus.FlushMessages(retryMetadata.UniqueID); return returnValue; } @@ -143,7 +144,7 @@ context.Instance is not null && // Let's make decisions based on the first execution regarding slow tests or retry failed test feature if (isFlakyRetryEnabled) { - retryMetadata.TotalExecutions = CIVisibility.Settings.FlakyRetryCount + 1; + retryMetadata.TotalExecutions = TestOptimization.Instance.Settings.FlakyRetryCount + 1; } else { @@ -162,17 +163,17 @@ context.Instance is not null && var remainingTotalRetries = Interlocked.Decrement(ref _totalRetries); if (runSummaryUnsafe.Failed == 0) { - Common.Log.Debug("EFD/Retry: [FlakyRetryEnabled] A non failed test execution was detected, skipping the remaining executions."); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: EFD/Retry: [FlakyRetryEnabled] A non failed test execution was detected, skipping the remaining executions."); doRetry = false; } else if (runSummaryUnsafe.NotRun > 0) { - Common.Log.Debug("EFD/Retry: [FlakyRetryEnabled] A NotRun test was detected, skipping the remaining executions."); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: EFD/Retry: [FlakyRetryEnabled] A NotRun test was detected, skipping the remaining executions."); doRetry = false; } else if (remainingTotalRetries < 1) { - Common.Log.Debug("EFD/Retry: [FlakyRetryEnabled] Exceeded number of total retries. [{Number}]", CIVisibility.Settings.TotalFlakyRetryCount); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: EFD/Retry: [FlakyRetryEnabled] Exceeded number of total retries. [{Number}]", TestOptimization.Instance.Settings.TotalFlakyRetryCount); doRetry = false; } } @@ -181,17 +182,17 @@ context.Instance is not null && { var retryNumber = retryMetadata.ExecutionIndex + 1; // Set the retry as a continuation of this execution. This will be executing recursively until the execution count is 0/ - Common.Log.Debug("EFD/Retry: [Retry {Num}] Test class runner is duck casted, running a retry. [Current retry value is {Value}]", retryNumber, retryMetadata.ExecutionNumber); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: EFD/Retry: [Retry {Num}] Test class runner is duck casted, running a retry. [Current retry value is {Value}]", retryNumber, retryMetadata.ExecutionNumber); var mrunner = instance.DuckCast(); // Decrement the execution number (the method body will do the execution) retryMetadata.ExecutionNumber--; var innerReturnValue = (TReturn)await mrunner.RunTestCase(context.Instance, testcase.Instance); - Common.Log.Debug("EFD/Retry: [Retry {Num}] Retry finished. [Current retry value is {Value}]. DisplayName: {DisplayName}", retryNumber, retryMetadata.ExecutionNumber, testcase.TestCaseDisplayName); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: EFD/Retry: [Retry {Num}] Retry finished. [Current retry value is {Value}]. DisplayName: {DisplayName}", retryNumber, retryMetadata.ExecutionNumber, testcase.TestCaseDisplayName); var innerReturnValueUnsafe = Unsafe.As(ref innerReturnValue); - Common.Log.Debug("EFD/Retry: [Retry {Num}] Aggregating results.", retryNumber); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: EFD/Retry: [Retry {Num}] Aggregating results.", retryNumber); runSummaryUnsafe.Total += innerReturnValueUnsafe.Total; runSummaryUnsafe.Failed += innerReturnValueUnsafe.Failed; runSummaryUnsafe.Skipped += innerReturnValueUnsafe.Skipped; @@ -203,11 +204,11 @@ context.Instance is not null && { if (isFlakyRetryEnabled && runSummaryUnsafe.Failed == 0) { - Common.Log.Debug("EFD/Retry: [FlakyRetryEnabled] A non failed test execution was detected."); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: EFD/Retry: [FlakyRetryEnabled] A non failed test execution was detected."); } else { - Common.Log.Debug("EFD/Retry: All retries were executed."); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: EFD/Retry: All retries were executed."); } } @@ -217,7 +218,7 @@ context.Instance is not null && // Let's clear the failed and skipped runs if we have at least one successful run #pragma warning disable DDLOG004 - Common.Log.Debug($"EFD/Retry: Summary: {testcase.TestCaseDisplayName} [Total: {runSummaryUnsafe.Total}, Failed: {runSummaryUnsafe.Failed}, Skipped: {runSummaryUnsafe.Skipped}]"); + Common.Log.Debug($"XUnitTestMethodRunnerBaseRunTestCaseV3Integration: EFD/Retry: Summary: {testcase.TestCaseDisplayName} [Total: {runSummaryUnsafe.Total}, Failed: {runSummaryUnsafe.Failed}, Skipped: {runSummaryUnsafe.Skipped}]"); #pragma warning restore DDLOG004 var passed = runSummaryUnsafe.Total - runSummaryUnsafe.Skipped - runSummaryUnsafe.Failed; if (passed > 0) @@ -239,7 +240,7 @@ context.Instance is not null && runSummaryUnsafe.Failed = 1; } - Common.Log.Debug("{Message}", $"EFD/Retry: Returned summary: {testcase.TestCaseDisplayName} [Total: {runSummaryUnsafe.Total}, Failed: {runSummaryUnsafe.Failed}, Skipped: {runSummaryUnsafe.Skipped}]"); + Common.Log.Debug("XUnitTestMethodRunnerBaseRunTestCaseV3Integration: {Message}", $"EFD/Retry: Returned summary: {testcase.TestCaseDisplayName} [Total: {runSummaryUnsafe.Total}, Failed: {runSummaryUnsafe.Failed}, Skipped: {runSummaryUnsafe.Skipped}]"); } } else diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XunitTestMethodRunnerContextCtorV3Integration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XunitTestMethodRunnerContextCtorV3Integration.cs index a040c6a80758..70291483f0fa 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XunitTestMethodRunnerContextCtorV3Integration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/V3/XunitTestMethodRunnerContextCtorV3Integration.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; using System.ComponentModel; @@ -52,8 +53,8 @@ internal static CallTargetState OnMethodBegin + #nullable enable using System; @@ -22,7 +23,7 @@ internal static class XUnitIntegration private static long _totalTestCases; private static long _newTestCases; - internal static bool IsEnabled => CIVisibility.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(IntegrationId); + internal static bool IsEnabled => TestOptimization.Instance.IsRunning && Tracer.Instance.Settings.IsIntegrationEnabled(IntegrationId); internal static Test? CreateTest(ref TestRunnerStruct runnerInstance, TestCaseMetadata? testCaseMetadata = null) { @@ -75,7 +76,7 @@ internal static class XUnitIntegration if (runnerInstance.TestCase.Traits is { } traits) { // Unskippable tests support - if (CIVisibility.Settings.IntelligentTestRunnerEnabled) + if (TestOptimization.Instance.Settings.IntelligentTestRunnerEnabled) { ShouldSkip(ref runnerInstance, out var isUnskippable, out var isForcedRun, traits); test.SetTag(IntelligentTestRunnerTags.UnskippableTag, isUnskippable ? "true" : "false"); @@ -85,7 +86,7 @@ internal static class XUnitIntegration test.SetTraits(traits); } - else if (CIVisibility.Settings.IntelligentTestRunnerEnabled) + else if (TestOptimization.Instance.Settings.IntelligentTestRunnerEnabled) { // Unskippable tests support test.SetTag(IntelligentTestRunnerTags.UnskippableTag, "false"); @@ -93,9 +94,9 @@ internal static class XUnitIntegration } // Early flake detection flags - if (CIVisibility.Settings.EarlyFlakeDetectionEnabled == true) + if (TestOptimization.Instance.Settings.EarlyFlakeDetectionEnabled == true) { - var testIsNew = !CIVisibility.IsAnEarlyFlakeDetectionTest(test.Suite.Module.Name, test.Suite.Name, test.Name ?? string.Empty); + var testIsNew = !TestOptimization.Instance.EarlyFlakeDetectionFeature?.IsAnEarlyFlakeDetectionTest(test.Suite.Module.Name, test.Suite.Name, test.Name ?? string.Empty) ?? false; if (testIsNew) { test.SetTag(EarlyFlakeDetectionTags.TestIsNew, "true"); @@ -126,9 +127,9 @@ internal static class XUnitIntegration } // Flaky retries - if (CIVisibility.Settings.FlakyRetryEnabled == true) + if (TestOptimization.Instance.Settings.FlakyRetryEnabled == true) { - if (testCaseMetadata is { ExecutionIndex: >0 }) + if (testCaseMetadata is { ExecutionIndex: > 0 }) { test.SetTag(EarlyFlakeDetectionTags.TestIsRetry, "true"); } @@ -189,7 +190,7 @@ internal static void FinishTest(Test test, IExceptionAggregator? exceptionAggreg } catch (Exception ex) { - CIVisibility.Log.Warning(ex, "Error finishing test scope"); + TestOptimization.Instance.Log.Warning(ex, "Error finishing test scope"); test.Close(TestStatus.Pass); } } @@ -199,7 +200,7 @@ internal static bool ShouldSkip(ref TestRunnerStruct runnerInstance, out bool is isUnskippable = false; isForcedRun = false; - if (CIVisibility.Settings.IntelligentTestRunnerEnabled != true) + if (TestOptimization.Instance.Settings.IntelligentTestRunnerEnabled != true) { return false; } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestAssemblyRunnerBeforeTestAssemblyFinishedAsyncIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestAssemblyRunnerBeforeTestAssemblyFinishedAsyncIntegration.cs index af2b278dea9b..adf6d038510c 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestAssemblyRunnerBeforeTestAssemblyFinishedAsyncIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestAssemblyRunnerBeforeTestAssemblyFinishedAsyncIntegration.cs @@ -52,7 +52,7 @@ internal static async Task OnAsyncMethodEnd(TTarget i await testModule.CloseAsync().ConfigureAwait(false); // Because we are auto-instrumenting a VSTest testhost process we need to manually call the shutdown process - CIVisibility.Close(); + TestOptimization.Instance.Close(); } return returnValue; diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestAssemblyRunnerRunAsyncIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestAssemblyRunnerRunAsyncIntegration.cs index 27878392a9ec..ba3c2c5873c6 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestAssemblyRunnerRunAsyncIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestAssemblyRunnerRunAsyncIntegration.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -63,7 +64,7 @@ internal static CallTargetState OnMethodBegin(TTarget instance) } } - CIVisibility.WaitForSkippableTaskToFinish(); + TestOptimization.Instance.SkippableFeature?.WaitForSkippableTaskToFinish(); var module = TestModule.InternalCreate(testBundleString, CommonTags.TestingFrameworkNameXUnit, frameworkType.Assembly.GetName().Version?.ToString() ?? string.Empty); module.EnableIpcClient(); return new CallTargetState(null, module); @@ -113,7 +114,7 @@ internal static async Task OnAsyncMethodEnd(TTarget i await testModule.CloseAsync().ConfigureAwait(false); // Because we are auto-instrumenting a VSTest testhost process we need to manually call the shutdown process - CIVisibility.Close(); + TestOptimization.Instance.Close(); } return returnValue; diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestRunnerRunAsyncIntegration.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestRunnerRunAsyncIntegration.cs index 9d1534455a7e..d692263d0ae5 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestRunnerRunAsyncIntegration.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/XUnit/XUnitTestRunnerRunAsyncIntegration.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -47,7 +48,7 @@ internal static CallTargetState OnMethodBegin(TTarget instance) return CallTargetState.GetDefault(); } - Interlocked.CompareExchange(ref _totalRetries, CIVisibility.Settings.TotalFlakyRetryCount, -1); + Interlocked.CompareExchange(ref _totalRetries, TestOptimization.Instance.Settings.TotalFlakyRetryCount, -1); var runnerInstance = instance.DuckCast(); ITestRunner? testRunnerInstance = null; @@ -57,7 +58,7 @@ internal static CallTargetState OnMethodBegin(TTarget instance) { if (instance.TryDuckCast(out testRunnerInstance)) { - Common.Log.Debug("ITR: Test skipped: {Class}.{Name}", runnerInstance.TestClass?.ToString() ?? string.Empty, runnerInstance.TestMethod?.Name ?? string.Empty); + Common.Log.Debug("XUnitTestRunnerRunAsyncIntegration: Test skipped by test skipping feature: {Class}.{Name}", runnerInstance.TestClass?.ToString() ?? string.Empty, runnerInstance.TestMethod?.Name ?? string.Empty); // Refresh values after skip reason change, and create Skip by ITR span. runnerInstance.SkipReason = IntelligentTestRunnerTags.SkippedByReason; testRunnerInstance.SkipReason = runnerInstance.SkipReason; @@ -73,8 +74,8 @@ internal static CallTargetState OnMethodBegin(TTarget instance) return CallTargetState.GetDefault(); } - if (CIVisibility.Settings.EarlyFlakeDetectionEnabled != true && - CIVisibility.Settings.FlakyRetryEnabled != true) + if (TestOptimization.Instance.Settings.EarlyFlakeDetectionEnabled != true && + TestOptimization.Instance.Settings.FlakyRetryEnabled != true) { return CallTargetState.GetDefault(); } @@ -104,7 +105,7 @@ internal static CallTargetState OnMethodBegin(TTarget instance) retryMessageBus = new RetryMessageBus(duckMessageBus, 1, 1); // EFD is disabled but FlakeRetry is enabled retryMetadata = retryMessageBus.GetMetadata(runnerInstance.TestCase.UniqueID); - retryMetadata.FlakyRetryEnabled = CIVisibility.Settings.EarlyFlakeDetectionEnabled != true && CIVisibility.Settings.FlakyRetryEnabled == true; + retryMetadata.FlakyRetryEnabled = TestOptimization.Instance.Settings.EarlyFlakeDetectionEnabled != true && TestOptimization.Instance.Settings.FlakyRetryEnabled == true; testRunnerInstance.MessageBus = retryMessageBus.DuckImplement(_messageBusInterfaceType); } else @@ -143,7 +144,7 @@ internal static async Task OnAsyncMethodEnd(TTarget i // Let's make decisions based on the first execution regarding slow tests or retry failed test feature if (isFlakyRetryEnabled) { - retryMetadata.TotalExecutions = CIVisibility.Settings.FlakyRetryCount + 1; + retryMetadata.TotalExecutions = TestOptimization.Instance.Settings.FlakyRetryCount + 1; } else { @@ -167,7 +168,7 @@ internal static async Task OnAsyncMethodEnd(TTarget i } else if (remainingTotalRetries < 1) { - Common.Log.Debug("EFD/Retry: [FlakyRetryEnabled] Exceeded number of total retries. [{Number}]", CIVisibility.Settings.TotalFlakyRetryCount); + Common.Log.Debug("EFD/Retry: [FlakyRetryEnabled] Exceeded number of total retries. [{Number}]", TestOptimization.Instance.Settings.TotalFlakyRetryCount); doRetry = false; } } diff --git a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs index da30fb1c4886..3d14d6162e9e 100644 --- a/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs +++ b/tracer/src/Datadog.Trace/ClrProfiler/Instrumentation.cs @@ -39,6 +39,7 @@ public static class Instrumentation /// Indicates whether we're initializing Instrumentation for the first time /// private static int _firstInitialization = 1; + private static int _firstNonNativePartsInitialization = 1; /// @@ -253,9 +254,10 @@ internal static void InitializeNoNativeParts(Stopwatch sw = null) try { // ensure global instance is created if it's not already - if (CIVisibility.Enabled) + var testOptimization = TestOptimization.Instance; + if (testOptimization.Enabled) { - CIVisibility.Initialize(); + testOptimization.Initialize(); } else { diff --git a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs index e4a6aef562e8..ae3313eb5af4 100644 --- a/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs +++ b/tracer/src/Datadog.Trace/Configuration/TracerSettings.cs @@ -175,7 +175,7 @@ _ when x.ToBoolean() is { } boolean => boolean, if (string.IsNullOrEmpty(serviceName)) { // Extract repository name from the git url and use it as a default service name. - ciVisServiceName = CIVisibility.GetServiceNameFromRepository(CIEnvironmentValues.Instance.Repository); + ciVisServiceName = TestOptimization.Instance.TracerManagement?.GetServiceNameFromRepository(CIEnvironmentValues.Instance.Repository); isUserProvidedTestServiceTag = false; } diff --git a/tracer/src/Datadog.Trace/Datadog.Trace.csproj b/tracer/src/Datadog.Trace/Datadog.Trace.csproj index 0bbbe7fab893..e1fed045f0a5 100644 --- a/tracer/src/Datadog.Trace/Datadog.Trace.csproj +++ b/tracer/src/Datadog.Trace/Datadog.Trace.csproj @@ -157,4 +157,4 @@ $(DefineConstants);ENABLE_IL2CPP - + \ No newline at end of file diff --git a/tracer/src/Datadog.Trace/ExtensionMethods/TaskExtensions.cs b/tracer/src/Datadog.Trace/ExtensionMethods/TaskExtensions.cs new file mode 100644 index 000000000000..f0119fbbd88e --- /dev/null +++ b/tracer/src/Datadog.Trace/ExtensionMethods/TaskExtensions.cs @@ -0,0 +1,90 @@ +// +// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. +// + +#nullable enable +using System; +using System.Threading; +using System.Threading.Tasks; +using Datadog.Trace.Util; + +namespace Datadog.Trace.ExtensionMethods; + +internal static class TaskExtensions +{ + public static void SafeWait(this Task task) + { + if (task.IsCompleted) + { + return; + } + + var originalContext = SynchronizationContext.Current; + try + { + // Set the synchronization context to null to avoid deadlocks. + SynchronizationContext.SetSynchronizationContext(null); + + // Wait synchronously for the task to complete. + task.GetAwaiter().GetResult(); + } + finally + { + // Restore the original synchronization context. + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + public static T SafeGetResult(this Task task) + { + if (task.IsCompleted) + { + return task.Result; + } + + var originalContext = SynchronizationContext.Current; + try + { + // Set the synchronization context to null to avoid deadlocks. + SynchronizationContext.SetSynchronizationContext(null); + + // Wait synchronously for the task to complete. + return task.GetAwaiter().GetResult(); + } + finally + { + // Restore the original synchronization context. + SynchronizationContext.SetSynchronizationContext(originalContext); + } + } + + public static bool SafeWait(this Func funcTask, int millisecondsTimeout) + { + var originalContext = SynchronizationContext.Current; + using var cts = new CancellationTokenSource(millisecondsTimeout); + try + { + // Set the synchronization context to null to avoid deadlocks. + SynchronizationContext.SetSynchronizationContext(null); + + // Wait synchronously for the task to complete. + AsyncUtil.RunSync(funcTask, cts.Token); + if (cts.IsCancellationRequested) + { + return false; + } + } + catch (TaskCanceledException) + { + return false; + } + finally + { + // Restore the original synchronization context. + SynchronizationContext.SetSynchronizationContext(originalContext); + } + + return true; + } +} diff --git a/tracer/src/Datadog.Trace/SpanContext.cs b/tracer/src/Datadog.Trace/SpanContext.cs index 63236b61ef3a..cc5f32039c2e 100644 --- a/tracer/src/Datadog.Trace/SpanContext.cs +++ b/tracer/src/Datadog.Trace/SpanContext.cs @@ -158,8 +158,8 @@ internal SpanContext(ISpanContext parent, TraceContext traceContext, string serv private SpanContext(TraceId traceId, string serviceName) { TraceId128 = traceId == Trace.TraceId.Zero - ? RandomIdGenerator.Shared.NextTraceId(useAllBits: false) - : traceId; + ? RandomIdGenerator.Shared.NextTraceId(useAllBits: false) + : traceId; ServiceName = serviceName; @@ -167,7 +167,7 @@ private SpanContext(TraceId traceId, string serviceName) // we need to ensure new SpanContext created by this .ctor has the CI Visibility origin // tag if the CI Visibility mode is running to ensure the correct propagation // to children spans and distributed trace. - if (CIVisibility.IsRunning) + if (TestOptimization.Instance.IsRunning) { Origin = Ci.Tags.TestTags.CIAppTestOriginName; } @@ -416,17 +416,17 @@ bool IReadOnlyDictionary.TryGetValue(string key, out string valu private static TraceId GetTraceId(ISpanContext context, TraceId fallback) { return context switch - { - // if there is no context or it has a zero trace id, - // use the specified fallback value - null or { TraceId: 0 } => fallback, + { + // if there is no context or it has a zero trace id, + // use the specified fallback value + null or { TraceId: 0 } => fallback, - // use the 128-bit trace id from SpanContext if possible - SpanContext sc => sc.TraceId128, + // use the 128-bit trace id from SpanContext if possible + SpanContext sc => sc.TraceId128, - // otherwise use the 64-bit trace id from ISpanContext - _ => (TraceId)context.TraceId - }; + // otherwise use the 64-bit trace id from ISpanContext + _ => (TraceId)context.TraceId + }; } /// diff --git a/tracer/src/Datadog.Trace/Telemetry/ITelemetryController.cs b/tracer/src/Datadog.Trace/Telemetry/ITelemetryController.cs index f21c033a3a9c..0658fa893877 100644 --- a/tracer/src/Datadog.Trace/Telemetry/ITelemetryController.cs +++ b/tracer/src/Datadog.Trace/Telemetry/ITelemetryController.cs @@ -42,9 +42,9 @@ internal interface ITelemetryController public void RecordProfilerSettings(Profiler profiler); /// - /// Called to record ci-vis-related telemetry + /// Called to record test-optimization-related telemetry /// - public void RecordCiVisibilitySettings(CIVisibilitySettings settings); + public void RecordTestOptimizationSettings(TestOptimizationSettings settings); /// /// Dispose resources for sending telemetry diff --git a/tracer/src/Datadog.Trace/Telemetry/NullTelemetryController.cs b/tracer/src/Datadog.Trace/Telemetry/NullTelemetryController.cs index 2a6c2187e418..71b165c4c725 100644 --- a/tracer/src/Datadog.Trace/Telemetry/NullTelemetryController.cs +++ b/tracer/src/Datadog.Trace/Telemetry/NullTelemetryController.cs @@ -34,7 +34,7 @@ public void RecordProfilerSettings(Profiler profiler) { } - public void RecordCiVisibilitySettings(CIVisibilitySettings settings) + public void RecordTestOptimizationSettings(TestOptimizationSettings settings) { } diff --git a/tracer/src/Datadog.Trace/Telemetry/TelemetryController.cs b/tracer/src/Datadog.Trace/Telemetry/TelemetryController.cs index a1e2f358a4bf..66b1d9f9b280 100644 --- a/tracer/src/Datadog.Trace/Telemetry/TelemetryController.cs +++ b/tracer/src/Datadog.Trace/Telemetry/TelemetryController.cs @@ -126,10 +126,10 @@ public void RecordProfilerSettings(Profiler profiler) _configuration.Record(ConfigTelemetryData.CodeHotspotsEnabled, profiler.ContextTracker.IsEnabled, ConfigurationOrigins.Default); } - public void RecordCiVisibilitySettings(CIVisibilitySettings settings) + public void RecordTestOptimizationSettings(TestOptimizationSettings settings) { - // CI Vis records the settings _directly_ in the global config so don't need to record them again here - _logTagBuilder.Update(settings, CIVisibility.Enabled); + // Test optimization records the settings _directly_ in the global config so don't need to record them again here + _logTagBuilder.Update(settings, TestOptimization.Instance.Enabled); } public void IntegrationRunning(IntegrationId integrationId) @@ -386,7 +386,7 @@ public void Update(TracerSettings settings) _isUpdateRequired = true; } - public void Update(CIVisibilitySettings settings, bool enabled) + public void Update(TestOptimizationSettings settings, bool enabled) { // We don't actually need to record these, because they're added to the global config // This isn't nice, as it calls the static property, diff --git a/tracer/src/Datadog.Trace/TraceContext.cs b/tracer/src/Datadog.Trace/TraceContext.cs index 4efb42b5486b..764e5fac0718 100644 --- a/tracer/src/Datadog.Trace/TraceContext.cs +++ b/tracer/src/Datadog.Trace/TraceContext.cs @@ -119,9 +119,7 @@ internal AppSecRequestContext AppSecRequestContext internal bool WafExecuted { get; set; } internal static TraceContext? GetTraceContext(in ArraySegment spans) => - spans.Count > 0 ? - spans.Array![spans.Offset].Context.TraceContext : - null; + spans.Count > 0 ? spans.Array![spans.Offset].Context.TraceContext : null; internal void EnableIastInRequest() { @@ -189,7 +187,7 @@ public void CloseSpan(Span span) _spans = default; TelemetryFactory.Metrics.RecordCountTraceSegmentsClosed(); } - else if (CIVisibility.IsRunning && span.IsCiVisibilitySpan()) + else if (TestOptimization.Instance.IsRunning && span.IsCiVisibilitySpan()) { // TestSession, TestModule, TestSuite, Test and Browser spans are part of CI Visibility // all of them are known to be Root spans, so we can flush them as soon as they are closed diff --git a/tracer/src/Datadog.Trace/TracerManagerFactory.cs b/tracer/src/Datadog.Trace/TracerManagerFactory.cs index 2d16e877521a..f7f1821b58d6 100644 --- a/tracer/src/Datadog.Trace/TracerManagerFactory.cs +++ b/tracer/src/Datadog.Trace/TracerManagerFactory.cs @@ -225,7 +225,7 @@ protected virtual bool ShouldEnableRemoteConfiguration(TracerSettings settings) => settings.IsRemoteConfigurationAvailable; /// - /// Can be overriden to create a different , e.g. + /// Can be overriden to create a different , e.g. /// protected virtual TracerManager CreateTracerManagerFrom( TracerSettings settings, diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/Agent/ApmAgentWriterTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/Agent/ApmAgentWriterTests.cs index 9f0392cf6263..03312a522610 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/Agent/ApmAgentWriterTests.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/Agent/ApmAgentWriterTests.cs @@ -24,7 +24,7 @@ public ApmAgentWriterTests() { var tracer = new Mock(); tracer.Setup(x => x.DefaultServiceName).Returns("Default"); - _settings = Ci.Configuration.CIVisibilitySettings.FromDefaultSources().TracerSettings; + _settings = Ci.Configuration.TestOptimizationSettings.FromDefaultSources().TracerSettings; _api = new Mock(); _ciAgentWriter = new ApmAgentWriter(_api.Object); diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/Agent/CiVisibilityProtocolWriterTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/Agent/CiVisibilityProtocolWriterTests.cs index 1aecdc203ef4..3fc8f986544e 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/Agent/CiVisibilityProtocolWriterTests.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/Agent/CiVisibilityProtocolWriterTests.cs @@ -35,7 +35,7 @@ public class CiVisibilityProtocolWriterTests [Fact] public async Task AgentlessTestEventTest() { - var settings = CIVisibility.Settings; + var settings = TestOptimization.Instance.Settings; var sender = new Mock(); var agentlessWriter = new CIVisibilityProtocolWriter(settings, sender.Object); @@ -49,11 +49,12 @@ public async Task AgentlessTestEventTest() byte[] finalPayload = null; sender.Setup(x => x.SendPayloadAsync(It.IsAny())) - .Returns(payload => - { - finalPayload = payload.ToArray(); - return Task.CompletedTask; - }); + .Returns( + payload => + { + finalPayload = payload.ToArray(); + return Task.CompletedTask; + }); var trace = new[] { span }; agentlessWriter.WriteTrace(new ArraySegment(trace)); @@ -65,7 +66,7 @@ public async Task AgentlessTestEventTest() [Fact] public async Task AgentlessStreamTestEventTest() { - var settings = CIVisibility.Settings; + var settings = TestOptimization.Instance.Settings; var sender = new Mock(); var agentlessWriter = new CIVisibilityProtocolWriter(settings, sender.Object); @@ -81,11 +82,12 @@ public async Task AgentlessStreamTestEventTest() byte[] finalPayload = null; sender.Setup(x => x.SendPayloadAsync(It.IsAny())) - .Returns(payload => - { - finalPayload = payload.ToArray(); - return Task.CompletedTask; - }); + .Returns( + payload => + { + finalPayload = payload.ToArray(); + return Task.CompletedTask; + }); var trace = new[] { span }; agentlessWriter.WriteTrace(new ArraySegment(trace)); @@ -97,7 +99,7 @@ public async Task AgentlessStreamTestEventTest() [Fact] public async Task AgentlessCodeCoverageEvent() { - var settings = CIVisibility.Settings; + var settings = TestOptimization.Instance.Settings; var sender = new Mock(); var agentlessWriter = new CIVisibilityProtocolWriter(settings, sender.Object); var coveragePayload = new TestCoverage @@ -121,11 +123,12 @@ public async Task AgentlessCodeCoverageEvent() MultipartFormItem[] finalFormItems = null; sender.Setup(x => x.SendPayloadAsync(It.IsAny())) - .Returns(payload => - { - finalFormItems = payload.ToArray(); - return Task.CompletedTask; - }); + .Returns( + payload => + { + finalFormItems = payload.ToArray(); + return Task.CompletedTask; + }); agentlessWriter.WriteEvent(coveragePayload); await agentlessWriter.FlushTracesAsync(); // Force a flush to make sure the trace is written to the API @@ -170,7 +173,7 @@ public async Task AgentlessCodeCoverageCompressedPayloadTest() [Fact] public async Task SlowSenderTest() { - var settings = CIVisibility.Settings; + var settings = TestOptimization.Instance.Settings; var flushTcs = new TaskCompletionSource(); var sender = new Mock(); @@ -178,11 +181,12 @@ public async Task SlowSenderTest() var lstPayloads = new List(); sender.Setup(x => x.SendPayloadAsync(It.IsAny())) - .Returns(payload => - { - lstPayloads.Add(payload.ToArray()); - return flushTcs.Task; - }); + .Returns( + payload => + { + lstPayloads.Add(payload.ToArray()); + return flushTcs.Task; + }); var span = new Span(new SpanContext(1, 1), DateTimeOffset.UtcNow); var expectedPayload = new Ci.Agent.Payloads.CITestCyclePayload(settings); @@ -224,7 +228,7 @@ public async Task SlowSenderTest() [Fact] public async Task ConcurrencyFlushTest() { - var settings = CIVisibility.Settings; + var settings = TestOptimization.Instance.Settings; var sender = new Mock(); // We set 8 threads of concurrency and a batch interval of 10 seconds to avoid the autoflush. var agentlessWriter = new CIVisibilityProtocolWriter(settings, sender.Object, concurrency: 8, batchInterval: 10_000); @@ -233,15 +237,16 @@ public async Task ConcurrencyFlushTest() const int numSpans = 2_000; sender.Setup(x => x.SendPayloadAsync(It.IsAny())) - .Returns(async payload => - { - lock (lstPayloads) + .Returns( + async payload => { - lstPayloads.Add(payload.ToArray()); - } + lock (lstPayloads) + { + lstPayloads.Add(payload.ToArray()); + } - await Task.Delay(150).ConfigureAwait(false); - }); + await Task.Delay(150).ConfigureAwait(false); + }); for (ulong i = 0; i < numSpans; i++) { @@ -312,7 +317,7 @@ public void CoverageBufferTest() { int headerSize = Ci.Agent.Payloads.EventsBuffer.HeaderSize + Ci.Agent.Payloads.MultipartPayload.HeaderSize; - var settings = CIVisibility.Settings; + var settings = TestOptimization.Instance.Settings; int bufferSize = headerSize + 1024; int maxBufferSize = (int)(4.5 * 1024 * 1024); diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/CIVisibilityTests.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/CIVisibilityTests.cs index f62dfd9284bd..14da23cb00d5 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/CIVisibilityTests.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/CIVisibilityTests.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using Datadog.Trace.Ci; +using Datadog.Trace.Ci.Configuration; +using Datadog.Trace.Ci.Net; using Datadog.Trace.TestHelpers; using Xunit; @@ -12,6 +14,8 @@ namespace Datadog.Trace.ClrProfiler.IntegrationTests.CI { public class CIVisibilityTests { + private static readonly ITestOptimizationTracerManagement TracerManagement = new TestOptimizationTracerManagement(TestOptimizationSettings.FromDefaultSources()); + public static IEnumerable GetParserData() { yield return @@ -96,14 +100,14 @@ public static IEnumerable GetParserData() [InlineData("%^&*", "")] public void GetServiceNameFromRepository(string repository, string serviceName) { - Assert.Equal(serviceName, Ci.CIVisibility.GetServiceNameFromRepository(repository)); + Assert.Equal(serviceName, TracerManagement.GetServiceNameFromRepository(repository)); } [SkippableTheory] [MemberData(nameof(GetParserData))] public void CustomTestConfigurationParser(SerializableDictionary tags, SerializableDictionary expected) { - Assert.Equal(expected, IntelligentTestRunnerClient.GetCustomTestsConfigurations(tags?.ToDictionary())); + Assert.Equal(expected, TestOptimizationClient.GetCustomTestsConfigurations(tags?.ToDictionary())); } } } diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/TestingFrameworkEvpTest.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/TestingFrameworkEvpTest.cs index 3454bcdcc17d..0851c74002a8 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/TestingFrameworkEvpTest.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/TestingFrameworkEvpTest.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -165,7 +166,7 @@ protected virtual void CheckRuntimeValues(MockCIVisibilityTest targetTest) AssertTargetSpanExists(targetTest, CommonTags.RuntimeArchitecture); AssertTargetSpanExists(targetTest, CommonTags.OSArchitecture); AssertTargetSpanExists(targetTest, CommonTags.OSPlatform); - AssertTargetSpanEqual(targetTest, CommonTags.OSVersion, CIVisibility.GetOperatingSystemVersion()); + AssertTargetSpanEqual(targetTest, CommonTags.OSVersion, TestOptimization.Instance.HostInfo.GetOperatingSystemVersion() ?? string.Empty); targetTest.Metrics[CommonTags.LogicalCpuCount].Should().Be(Environment.ProcessorCount); targetTest.Metrics.Remove(CommonTags.LogicalCpuCount); } diff --git a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/TestingFrameworkTest.cs b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/TestingFrameworkTest.cs index d320f53c6040..1a5299439b40 100644 --- a/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/TestingFrameworkTest.cs +++ b/tracer/test/Datadog.Trace.ClrProfiler.IntegrationTests/CI/TestingFrameworkTest.cs @@ -2,6 +2,7 @@ // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // + #nullable enable using System; @@ -128,7 +129,7 @@ protected virtual void CheckRuntimeValues(MockSpan targetSpan) AssertTargetSpanExists(targetSpan, CommonTags.RuntimeArchitecture); AssertTargetSpanExists(targetSpan, CommonTags.OSArchitecture); AssertTargetSpanExists(targetSpan, CommonTags.OSPlatform); - AssertTargetSpanEqual(targetSpan, CommonTags.OSVersion, CIVisibility.GetOperatingSystemVersion()); + AssertTargetSpanEqual(targetSpan, CommonTags.OSVersion, TestOptimization.Instance.HostInfo.GetOperatingSystemVersion() ?? string.Empty); } protected virtual void CheckOriginTag(MockSpan targetSpan) @@ -218,29 +219,30 @@ protected virtual void CheckSimpleErrorTest(MockSpan targetSpan) protected void SetCIEnvironmentValues() { var current = GitInfo.GetCurrent(); - var ciDictionaryValues = DefineCIEnvironmentValues(new Dictionary - { - [CIEnvironmentValues.Constants.AzureTFBuild] = "1", - [CIEnvironmentValues.Constants.AzureSystemTeamProjectId] = "TeamProjectId", - [CIEnvironmentValues.Constants.AzureBuildBuildId] = "BuildId", - [CIEnvironmentValues.Constants.AzureSystemJobId] = "JobId", - [CIEnvironmentValues.Constants.AzureBuildSourcesDirectory] = current.SourceRoot ?? string.Empty, - [CIEnvironmentValues.Constants.AzureBuildDefinitionName] = "DefinitionName", - [CIEnvironmentValues.Constants.AzureSystemTeamFoundationServerUri] = "https://foundation.server.url/", - [CIEnvironmentValues.Constants.AzureSystemStageDisplayName] = "StageDisplayName", - [CIEnvironmentValues.Constants.AzureSystemJobDisplayName] = "JobDisplayName", - [CIEnvironmentValues.Constants.AzureSystemTaskInstanceId] = "TaskInstanceId", - [CIEnvironmentValues.Constants.AzureSystemPullRequestSourceRepositoryUri] = "git@github.com:DataDog/dd-trace-dotnet.git", - [CIEnvironmentValues.Constants.AzureBuildRepositoryUri] = "git@github.com:DataDog/dd-trace-dotnet.git", - [CIEnvironmentValues.Constants.AzureSystemPullRequestSourceCommitId] = "3245605c3d1edc67226d725799ee969c71f7632b", - [CIEnvironmentValues.Constants.AzureBuildSourceVersion] = "3245605c3d1edc67226d725799ee969c71f7632b", - [CIEnvironmentValues.Constants.AzureSystemPullRequestSourceBranch] = "main", - [CIEnvironmentValues.Constants.AzureBuildSourceBranch] = "main", - [CIEnvironmentValues.Constants.AzureBuildSourceBranchName] = "main", - [CIEnvironmentValues.Constants.AzureBuildSourceVersionMessage] = "Fake commit for testing", - [CIEnvironmentValues.Constants.AzureBuildRequestedForId] = "AuthorName", - [CIEnvironmentValues.Constants.AzureBuildRequestedForEmail] = "author@company.com", - }); + var ciDictionaryValues = DefineCIEnvironmentValues( + new Dictionary + { + [CIEnvironmentValues.Constants.AzureTFBuild] = "1", + [CIEnvironmentValues.Constants.AzureSystemTeamProjectId] = "TeamProjectId", + [CIEnvironmentValues.Constants.AzureBuildBuildId] = "BuildId", + [CIEnvironmentValues.Constants.AzureSystemJobId] = "JobId", + [CIEnvironmentValues.Constants.AzureBuildSourcesDirectory] = current.SourceRoot ?? string.Empty, + [CIEnvironmentValues.Constants.AzureBuildDefinitionName] = "DefinitionName", + [CIEnvironmentValues.Constants.AzureSystemTeamFoundationServerUri] = "https://foundation.server.url/", + [CIEnvironmentValues.Constants.AzureSystemStageDisplayName] = "StageDisplayName", + [CIEnvironmentValues.Constants.AzureSystemJobDisplayName] = "JobDisplayName", + [CIEnvironmentValues.Constants.AzureSystemTaskInstanceId] = "TaskInstanceId", + [CIEnvironmentValues.Constants.AzureSystemPullRequestSourceRepositoryUri] = "git@github.com:DataDog/dd-trace-dotnet.git", + [CIEnvironmentValues.Constants.AzureBuildRepositoryUri] = "git@github.com:DataDog/dd-trace-dotnet.git", + [CIEnvironmentValues.Constants.AzureSystemPullRequestSourceCommitId] = "3245605c3d1edc67226d725799ee969c71f7632b", + [CIEnvironmentValues.Constants.AzureBuildSourceVersion] = "3245605c3d1edc67226d725799ee969c71f7632b", + [CIEnvironmentValues.Constants.AzureSystemPullRequestSourceBranch] = "main", + [CIEnvironmentValues.Constants.AzureBuildSourceBranch] = "main", + [CIEnvironmentValues.Constants.AzureBuildSourceBranchName] = "main", + [CIEnvironmentValues.Constants.AzureBuildSourceVersionMessage] = "Fake commit for testing", + [CIEnvironmentValues.Constants.AzureBuildRequestedForId] = "AuthorName", + [CIEnvironmentValues.Constants.AzureBuildRequestedForEmail] = "author@company.com", + }); foreach (var item in ciDictionaryValues) { diff --git a/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs b/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs index 80e28594291a..87aa80a68a82 100644 --- a/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs +++ b/tracer/test/Datadog.Trace.DuckTyping.Tests/GetAssemblyTests.cs @@ -49,7 +49,7 @@ public void GetAssemblyTest() * WARNING: This number is expected to change if you add * a another test to the ducktype assembly. */ - if (!CIVisibility.IsRunning) + if (!TestOptimization.Instance.IsRunning) { #if NETFRAMEWORK asmDuckTypes.Should().Be(1131); diff --git a/tracer/test/Datadog.Trace.Tests/Configuration/CIVisibilitySettingsTests.cs b/tracer/test/Datadog.Trace.Tests/Configuration/TestOptimizationSettingsTests.cs similarity index 79% rename from tracer/test/Datadog.Trace.Tests/Configuration/CIVisibilitySettingsTests.cs rename to tracer/test/Datadog.Trace.Tests/Configuration/TestOptimizationSettingsTests.cs index 2e5a49003d5a..ba76df9b72eb 100644 --- a/tracer/test/Datadog.Trace.Tests/Configuration/CIVisibilitySettingsTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Configuration/TestOptimizationSettingsTests.cs @@ -1,4 +1,4 @@ -// +// // Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License. // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // @@ -13,7 +13,7 @@ namespace Datadog.Trace.Tests.Configuration { - public class CIVisibilitySettingsTests : SettingsTestsBase + public class TestOptimizationSettingsTests : SettingsTestsBase { private static readonly string ExpectedExcludedSession = "/session/FakeSessionIdForPollingPurposes".ToUpperInvariant(); @@ -22,7 +22,7 @@ public class CIVisibilitySettingsTests : SettingsTestsBase public void Enabled(string value, bool? expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.Enabled, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.Enabled.Should().Be(expected); } @@ -32,7 +32,7 @@ public void Enabled(string value, bool? expected) public void Agentless(string value, bool expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.AgentlessEnabled, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.Agentless.Should().Be(expected); } @@ -42,7 +42,7 @@ public void Agentless(string value, bool expected) public void Logs(string value, bool expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.Logs, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.Logs.Should().Be(expected); } @@ -52,7 +52,7 @@ public void Logs(string value, bool expected) public void ApiKey(string value, string expected) { var source = CreateConfigurationSource((ConfigurationKeys.ApiKey, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.ApiKey.Should().Be(expected); } @@ -62,7 +62,7 @@ public void ApiKey(string value, string expected) public void Site(string value, string expected) { var source = CreateConfigurationSource((ConfigurationKeys.Site, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.Site.Should().Be(expected); } @@ -72,7 +72,7 @@ public void Site(string value, string expected) public void AgentlessUrl(string value, string expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.AgentlessUrl, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.AgentlessUrl.Should().Be(expected); } @@ -81,7 +81,7 @@ public void AgentlessUrl(string value, string expected) public void MaximumAgentlessPayloadSize() { var source = CreateConfigurationSource(); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.MaximumAgentlessPayloadSize.Should().Be(5 * 1024 * 1024); } @@ -91,7 +91,7 @@ public void MaximumAgentlessPayloadSize() public void ProxyHttps(string value, string expected) { var source = CreateConfigurationSource((ConfigurationKeys.Proxy.ProxyHttps, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.ProxyHttps.Should().Be(expected); } @@ -104,7 +104,7 @@ public void ProxyHttps(string value, string expected) public void ProxyNoProxy(string value, string[] expected) { var source = CreateConfigurationSource((ConfigurationKeys.Proxy.ProxyNoProxy, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.ProxyNoProxy.Should().BeEquivalentTo(expected); } @@ -114,7 +114,7 @@ public void ProxyNoProxy(string value, string[] expected) public void IntelligentTestRunnerEnabled(string value, bool expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.IntelligentTestRunnerEnabled, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.IntelligentTestRunnerEnabled.Should().Be(expected); } @@ -124,7 +124,7 @@ public void IntelligentTestRunnerEnabled(string value, bool expected) public void TestsSkippingEnabled(string value, bool? expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.TestsSkippingEnabled, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.TestsSkippingEnabled.Should().Be(expected); } @@ -134,7 +134,7 @@ public void TestsSkippingEnabled(string value, bool? expected) public void CodeCoverageEnabled(string value, bool? expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.CodeCoverage, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.CodeCoverageEnabled.Should().Be(expected); } @@ -144,7 +144,7 @@ public void CodeCoverageEnabled(string value, bool? expected) public void CodeCoverageSnkFilePath(string value, string expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.CodeCoverageSnkFile, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.CodeCoverageSnkFilePath.Should().Be(expected); } @@ -154,7 +154,7 @@ public void CodeCoverageSnkFilePath(string value, string expected) public void CodeCoveragePath(string value, string expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.CodeCoveragePath, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.CodeCoveragePath.Should().Be(expected); } @@ -164,7 +164,7 @@ public void CodeCoveragePath(string value, string expected) public void CodeCoverageEnableJitOptimizations(string value, bool expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.CodeCoverageEnableJitOptimizations, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.CodeCoverageEnableJitOptimizations.Should().Be(expected); } @@ -174,7 +174,7 @@ public void CodeCoverageEnableJitOptimizations(string value, bool expected) public void GitUploadEnabled(string value, bool? expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.GitUploadEnabled, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.GitUploadEnabled.Should().Be(expected); } @@ -184,7 +184,7 @@ public void GitUploadEnabled(string value, bool? expected) public void ForceAgentsEvpProxy(string value, string expected) { var source = CreateConfigurationSource((ConfigurationKeys.CIVisibility.ForceAgentsEvpProxy, value)); - var settings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); settings.ForceAgentsEvpProxy.Should().Be(expected); } @@ -196,7 +196,7 @@ public void AddsUserProvidedTestServiceTagToGlobalTags(string serviceName, strin { var source = CreateConfigurationSource((ConfigurationKeys.ServiceName, serviceName)); - var ciVisSettings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var ciVisSettings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); var tracerSettings = ciVisSettings.InitializeTracerSettings([source]); tracerSettings.GlobalTags.Should() @@ -213,7 +213,7 @@ public void ServiceNameIsNormalized() var source = CreateConfigurationSource((ConfigurationKeys.ServiceName, originalName)); - var ciVisSettings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var ciVisSettings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); var tracerSettings = ciVisSettings.InitializeTracerSettings([source]); tracerSettings.ServiceName.Should().Be(normalizedName); @@ -224,7 +224,7 @@ public void AddsFakeSessionToExcludedHttpClientUrls() { var source = CreateConfigurationSource(); - var ciVisSettings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var ciVisSettings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); var tracerSettings = ciVisSettings.InitializeTracerSettings([source]); tracerSettings.HttpClientExcludedUrlSubstrings @@ -238,7 +238,7 @@ public void AddsFakeSessionToExcludedHttpClientUrls_WhenUrlsAlreadyExist() var source = CreateConfigurationSource( (ConfigurationKeys.HttpClientExcludedUrlSubstrings, "/some-url/path")); - var ciVisSettings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var ciVisSettings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); var tracerSettings = ciVisSettings.InitializeTracerSettings([source]); tracerSettings.HttpClientExcludedUrlSubstrings @@ -253,7 +253,7 @@ public void AddsFakeSessionToExcludedHttpClientUrls_WhenRunningInAas() (ConfigurationKeys.AzureAppService.AzureAppServicesContextKey, "true"), (ConfigurationKeys.HttpClientExcludedUrlSubstrings, "/some-url/path")); - var ciVisSettings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var ciVisSettings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); var tracerSettings = ciVisSettings.InitializeTracerSettings([source]); tracerSettings.HttpClientExcludedUrlSubstrings @@ -267,7 +267,7 @@ public void WhenLogsEnabled_AddsDirectSubmission() var source = CreateConfigurationSource( (ConfigurationKeys.CIVisibility.Logs, "true")); - var ciVisSettings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var ciVisSettings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); var tracerSettings = ciVisSettings.InitializeTracerSettings([source]); tracerSettings.LogSubmissionSettings @@ -282,7 +282,7 @@ public void WhenLogsNotEnabled_DoesNotAddDirectSubmission() { var source = CreateConfigurationSource(); - var ciVisSettings = new CIVisibilitySettings(source, NullConfigurationTelemetry.Instance); + var ciVisSettings = new TestOptimizationSettings(source, NullConfigurationTelemetry.Instance); var tracerSettings = ciVisSettings.InitializeTracerSettings([source]); tracerSettings.LogSubmissionSettings diff --git a/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/DuckTypingTests.cs b/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/DuckTypingTests.cs index d4f1fd8ed927..3480316fa2a2 100644 --- a/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/DuckTypingTests.cs +++ b/tracer/test/Datadog.Trace.Tests/ManualInstrumentation/DuckTypingTests.cs @@ -3,8 +3,9 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc. // +#pragma warning disable SA1516 extern alias DatadogTraceManual; - +#pragma warning restore SA1516 using System; using System.Linq; using System.Reflection; @@ -146,8 +147,8 @@ public void CanDuckTypeManualTestSessionAsISession() } finally { - CIVisibility.Close(); - CIVisibility.Reset(); + TestOptimization.Instance.Close(); + TestOptimization.Instance.Reset(); } } diff --git a/tracer/test/Datadog.Trace.Tests/Telemetry/CallTargetInvokerTelemetryTests.cs b/tracer/test/Datadog.Trace.Tests/Telemetry/CallTargetInvokerTelemetryTests.cs index 97ce7563e83f..f79dc9dd754a 100644 --- a/tracer/test/Datadog.Trace.Tests/Telemetry/CallTargetInvokerTelemetryTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Telemetry/CallTargetInvokerTelemetryTests.cs @@ -130,7 +130,7 @@ public void RecordProfilerSettings(Profiler profiler) { } - public void RecordCiVisibilitySettings(CIVisibilitySettings settings) + public void RecordTestOptimizationSettings(TestOptimizationSettings settings) { } diff --git a/tracer/test/Datadog.Trace.Tests/Telemetry/TelemetryControllerLogTagBuilderTests.cs b/tracer/test/Datadog.Trace.Tests/Telemetry/TelemetryControllerLogTagBuilderTests.cs index f0ab94ef77f8..bd17ef5cd3dc 100644 --- a/tracer/test/Datadog.Trace.Tests/Telemetry/TelemetryControllerLogTagBuilderTests.cs +++ b/tracer/test/Datadog.Trace.Tests/Telemetry/TelemetryControllerLogTagBuilderTests.cs @@ -25,7 +25,7 @@ public void TagBuilder_ReturnsExpectedDefaults() public void TagBuilder_UpdateCiVisTag() { var builder = new TelemetryController.TagBuilder(); - builder.Update(new CIVisibilitySettings(NullConfigurationSource.Instance, NullConfigurationTelemetry.Instance), enabled: true); + builder.Update(new TestOptimizationSettings(NullConfigurationSource.Instance, NullConfigurationTelemetry.Instance), enabled: true); builder.GetLogTags().Should().Be("ci:1,asm:0,prof:0,dyn:0"); } @@ -56,7 +56,7 @@ public void TagBuilder_AddsEverything() builder.Update(TelemetryProductType.Profiler, enabled: true); builder.Update(TelemetryProductType.DynamicInstrumentation, enabled: true); builder.Update(TelemetryProductType.AppSec, enabled: true); - builder.Update(new CIVisibilitySettings(NullConfigurationSource.Instance, NullConfigurationTelemetry.Instance), enabled: true); + builder.Update(new TestOptimizationSettings(NullConfigurationSource.Instance, NullConfigurationTelemetry.Instance), enabled: true); builder.GetLogTags().Should().Be("ci:1,asm:1,prof:1,dyn:1,azf"); } @@ -68,7 +68,7 @@ public void TagBuilder_WhenAdded() builder.Update(TelemetryProductType.Profiler, enabled: true); builder.Update(TelemetryProductType.DynamicInstrumentation, enabled: true); builder.Update(TelemetryProductType.AppSec, enabled: true); - builder.Update(new CIVisibilitySettings(NullConfigurationSource.Instance, NullConfigurationTelemetry.Instance), enabled: true); + builder.Update(new TestOptimizationSettings(NullConfigurationSource.Instance, NullConfigurationTelemetry.Instance), enabled: true); builder.GetLogTags().Should().Be("ci:1,asm:1,prof:1,dyn:1,azf"); } } diff --git a/tracer/test/Datadog.Trace.Tests/TracerInstanceTest.cs b/tracer/test/Datadog.Trace.Tests/TracerInstanceTest.cs index e02f2607994e..a3ac609033a4 100644 --- a/tracer/test/Datadog.Trace.Tests/TracerInstanceTest.cs +++ b/tracer/test/Datadog.Trace.Tests/TracerInstanceTest.cs @@ -65,7 +65,7 @@ public async Task LockedTracerInstanceSwap() Assert.Throws(() => Tracer.Instance = null); Assert.Throws(() => TracerManager.ReplaceGlobalManager(null, TracerManagerFactory.Instance)); - Assert.Throws(() => TracerManager.ReplaceGlobalManager(null, new CITracerManagerFactory(CIVisibility.Settings, NullDiscoveryService.Instance, false))); + Assert.Throws(() => TracerManager.ReplaceGlobalManager(null, new TestOptimizationTracerManagerFactory(TestOptimization.Instance.Settings, TestOptimization.Instance.TracerManagement!, false))); } [Fact] @@ -75,25 +75,27 @@ public void ReplacingGlobalTracerManagerMidTraceWritesTheTrace() using (var agent = MockTracerAgent.Create(null, agentPort)) { - var oldSettings = TracerSettings.Create(new() - { - { ConfigurationKeys.AgentUri, new Uri($"http://127.0.0.1:{agent.Port}") }, - { ConfigurationKeys.TracerMetricsEnabled, false }, - { ConfigurationKeys.StartupDiagnosticLogEnabled, false }, - { ConfigurationKeys.GlobalTags, "test-tag:original-value" }, - }); + var oldSettings = TracerSettings.Create( + new() + { + { ConfigurationKeys.AgentUri, new Uri($"http://127.0.0.1:{agent.Port}") }, + { ConfigurationKeys.TracerMetricsEnabled, false }, + { ConfigurationKeys.StartupDiagnosticLogEnabled, false }, + { ConfigurationKeys.GlobalTags, "test-tag:original-value" }, + }); Tracer.Configure(oldSettings); var scope = Tracer.Instance.StartActive("Test span"); (scope.Span as Span).IsRootSpan.Should().BeTrue(); - var newSettings = TracerSettings.Create(new() - { - { ConfigurationKeys.AgentUri, new Uri($"http://127.0.0.1:{agent.Port}") }, - { ConfigurationKeys.TracerMetricsEnabled, false }, - { ConfigurationKeys.StartupDiagnosticLogEnabled, false }, - { ConfigurationKeys.GlobalTags, "test-tag:new-value" }, - }); + var newSettings = TracerSettings.Create( + new() + { + { ConfigurationKeys.AgentUri, new Uri($"http://127.0.0.1:{agent.Port}") }, + { ConfigurationKeys.TracerMetricsEnabled, false }, + { ConfigurationKeys.StartupDiagnosticLogEnabled, false }, + { ConfigurationKeys.GlobalTags, "test-tag:new-value" }, + }); Tracer.Configure(newSettings); diff --git a/tracer/test/Datadog.Trace.Tools.Runner.IntegrationTests/CiRunCommandTests.cs b/tracer/test/Datadog.Trace.Tools.Runner.IntegrationTests/CiRunCommandTests.cs index 3ac4a9f487df..061e17d6dfdb 100644 --- a/tracer/test/Datadog.Trace.Tools.Runner.IntegrationTests/CiRunCommandTests.cs +++ b/tracer/test/Datadog.Trace.Tools.Runner.IntegrationTests/CiRunCommandTests.cs @@ -55,7 +55,7 @@ public void OpenCoverCodeCoverage() private void RunExternalCoverageTest(string filePath) { - CIVisibility.Reset(); + TestOptimization.Instance.Reset(); EnvironmentHelpers.SetEnvironmentVariable(Configuration.ConfigurationKeys.DebugEnabled, "1"); string command = null; string arguments = null; diff --git a/tracer/test/Datadog.Trace.Tools.Runner.IntegrationTests/CustomTestFramework.cs b/tracer/test/Datadog.Trace.Tools.Runner.IntegrationTests/CustomTestFramework.cs index 2120a20aab32..39044c48a42f 100644 --- a/tracer/test/Datadog.Trace.Tools.Runner.IntegrationTests/CustomTestFramework.cs +++ b/tracer/test/Datadog.Trace.Tools.Runner.IntegrationTests/CustomTestFramework.cs @@ -16,7 +16,7 @@ public class CustomTestFramework : TestHelpers.CustomTestFramework public CustomTestFramework(IMessageSink messageSink) : base(messageSink) { - CIVisibility.UseLockedTracerManager = false; + TestOptimization.DefaultUseLockedTracerManager = false; } } } diff --git a/tracer/test/Datadog.Trace.Tools.Runner.Tests/CoverageRewriteTests.cs b/tracer/test/Datadog.Trace.Tools.Runner.Tests/CoverageRewriteTests.cs index 0ca002558ef9..7c3df6936ed0 100644 --- a/tracer/test/Datadog.Trace.Tools.Runner.Tests/CoverageRewriteTests.cs +++ b/tracer/test/Datadog.Trace.Tools.Runner.Tests/CoverageRewriteTests.cs @@ -147,7 +147,7 @@ public async Task NoFilter(string coverageMode) // Apply rewriter process var covSettings = new CoverageSettings(null, string.Empty); - covSettings.CIVisibility.SetCodeCoverageMode(coverageMode); + covSettings.TestOptimization.SetCodeCoverageMode(coverageMode); var asmProcessor = new AssemblyProcessor(tempFileName, covSettings); asmProcessor.Process(); @@ -178,7 +178,7 @@ public async Task WithFilters(string targetSnapshot, string configurationSetting configurationElement.LoadXml(configurationSettingsXml); var covSettings = new CoverageSettings(configurationElement.DocumentElement, string.Empty); - covSettings.CIVisibility.SetCodeCoverageMode(coverageMode); + covSettings.TestOptimization.SetCodeCoverageMode(coverageMode); var asmProcessor = new AssemblyProcessor(tempFileName, covSettings); asmProcessor.Process(); diff --git a/tracer/test/benchmarks/Benchmarks.Trace/CIVisibilityProtocolWriterBenchmark.cs b/tracer/test/benchmarks/Benchmarks.Trace/CIVisibilityProtocolWriterBenchmark.cs index 538ffcedd319..8939d1e7c42d 100644 --- a/tracer/test/benchmarks/Benchmarks.Trace/CIVisibilityProtocolWriterBenchmark.cs +++ b/tracer/test/benchmarks/Benchmarks.Trace/CIVisibilityProtocolWriterBenchmark.cs @@ -28,7 +28,7 @@ static CIVisibilityProtocolWriterBenchmark() { ConfigurationKeys.TraceEnabled, false.ToString() }, }); var sources = new CompositeConfigurationSource(new[] { overrides, GlobalConfigurationSource.Instance }); - var settings = new CIVisibilitySettings(sources, NullConfigurationTelemetry.Instance); + var settings = new TestOptimizationSettings(sources, NullConfigurationTelemetry.Instance); EventWriter = new CIVisibilityProtocolWriter(settings, new FakeCIVisibilityProtocolWriter());