diff --git a/tracer/src/Datadog.Trace/Ci/Tagging/TestSpanTags.cs b/tracer/src/Datadog.Trace/Ci/Tagging/TestSpanTags.cs
index ebcdb501158e..103924400e15 100644
--- a/tracer/src/Datadog.Trace/Ci/Tagging/TestSpanTags.cs
+++ b/tracer/src/Datadog.Trace/Ci/Tagging/TestSpanTags.cs
@@ -127,6 +127,9 @@ public TestSpanTags(TestSuiteSpanTags suiteTags, string testName)
[Tag(TestTags.TestAttemptToFixPassed)]
public string? AttemptToFixPassed { get; set; }
+ [Tag(TestTags.TestFinalStatus)]
+ public string? FinalStatus { get; set; }
+
[Tag(CapabilitiesTags.LibraryCapabilitiesTestImpactAnalysis)]
public string? CapabilitiesTestImpactAnalysis { get; set; }
diff --git a/tracer/src/Datadog.Trace/Ci/Tags/TestTags.cs b/tracer/src/Datadog.Trace/Ci/Tags/TestTags.cs
index 1075b2ebb518..73217bf7de65 100644
--- a/tracer/src/Datadog.Trace/Ci/Tags/TestTags.cs
+++ b/tracer/src/Datadog.Trace/Ci/Tags/TestTags.cs
@@ -156,6 +156,21 @@ internal static class TestTags
///
public const string TestRetryReason = "test.retry_reason";
+ ///
+ /// Retry reason value for Early Flake Detection
+ ///
+ public const string TestRetryReasonEfd = "efd";
+
+ ///
+ /// Retry reason value for Auto Test Retries
+ ///
+ public const string TestRetryReasonAtr = "atr";
+
+ ///
+ /// Retry reason value for Attempt to Fix (Test Management)
+ ///
+ public const string TestRetryReasonAttemptToFix = "attempt_to_fix";
+
///
/// Test is quarantined flag
///
@@ -185,4 +200,9 @@ internal static class TestTags
/// Test management enabled flag
///
public const string TestManagementEnabled = "test.test_management.enabled";
+
+ ///
+ /// Test final status - the adjusted test outcome for CI pipelines
+ ///
+ public const string TestFinalStatus = "test.final_status";
}
diff --git a/tracer/src/Datadog.Trace/Ci/Test.cs b/tracer/src/Datadog.Trace/Ci/Test.cs
index 63ac18ec312e..b4d80f11cd51 100644
--- a/tracer/src/Datadog.Trace/Ci/Test.cs
+++ b/tracer/src/Datadog.Trace/Ci/Test.cs
@@ -555,8 +555,8 @@ public void Close(TestStatus status, TimeSpan? duration, string? skipReason)
{
var retryReasonTag = tags.TestRetryReason switch
{
- "efd" => MetricTags.CIVisibilityTestingEventTypeRetryReason.EarlyFlakeDetection,
- "atr" => MetricTags.CIVisibilityTestingEventTypeRetryReason.AutomaticTestRetry,
+ TestTags.TestRetryReasonEfd => MetricTags.CIVisibilityTestingEventTypeRetryReason.EarlyFlakeDetection,
+ TestTags.TestRetryReasonAtr => MetricTags.CIVisibilityTestingEventTypeRetryReason.AutomaticTestRetry,
_ => MetricTags.CIVisibilityTestingEventTypeRetryReason.None
};
diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Common.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Common.cs
index 7a245f887954..ab65e3fe1e7a 100644
--- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Common.cs
+++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/Common.cs
@@ -10,6 +10,7 @@
using System.Threading;
using Datadog.Trace.Ci;
using Datadog.Trace.Ci.Net;
+using Datadog.Trace.Ci.Tagging;
using Datadog.Trace.Ci.Tags;
using Datadog.Trace.Logging;
using Datadog.Trace.Util;
@@ -188,7 +189,7 @@ internal static bool SetEarlyFlakeDetectionTestTagsAndAbortReason(Test test, boo
if (isRetry)
{
testTags.TestIsRetry = "true";
- testTags.TestRetryReason = "efd";
+ testTags.TestRetryReason = TestTags.TestRetryReasonEfd;
}
else
{
@@ -211,7 +212,7 @@ internal static bool SetFlakyRetryTags(Test test, bool isRetry)
{
var testTags = test.GetTags();
testTags.TestIsRetry = "true";
- testTags.TestRetryReason = "atr";
+ testTags.TestRetryReason = TestTags.TestRetryReasonAtr;
}
return flakyRetryFeature;
@@ -244,7 +245,7 @@ internal static TestOptimizationClient.TestManagementResponseTestPropertiesAttri
if (isRetry)
{
testTags.TestIsRetry = "true";
- testTags.TestRetryReason = "attempt_to_fix";
+ testTags.TestRetryReason = TestTags.TestRetryReasonAttemptToFix;
}
}
@@ -273,4 +274,49 @@ internal static void CheckFaultyThreshold(Test test, long nTestCases, long tTest
}
}
}
+
+ ///
+ /// Calculates the final status for a test based on execution results and test management tags.
+ /// Priority order (first match wins):
+ /// 1. Quarantined/disabled -> skip (always mask to skip)
+ /// 2. For ATF tests: any execution failed -> fail (flaky test = fix didn't work)
+ /// 3. Any execution passed -> pass
+ /// 4. Skip/inconclusive AND no pass -> skip
+ /// 5. All executions failed -> fail
+ ///
+ /// True if any execution (initial or retry) passed.
+ /// True if any execution (initial or retry) failed.
+ /// True if the current/last execution was skip or inconclusive.
+ /// The test tags to check for quarantine/disabled/ATF status.
+ /// The final status string: "pass", "fail", or "skip".
+ internal static string CalculateFinalStatus(bool anyExecutionPassed, bool anyExecutionFailed, bool isSkippedOrInconclusive, TestSpanTags? testTags)
+ {
+ // Priority 1: Quarantined/disabled tests always mask to skip
+ if (testTags?.IsQuarantined == "true" || testTags?.IsDisabled == "true")
+ {
+ return TestTags.StatusSkip;
+ }
+
+ // Priority 2: For ATF tests, any failure means fix didn't work (test is still flaky)
+ // This must be checked BEFORE anyPassed for ATF tests
+ if (testTags?.IsAttemptToFix == "true" && anyExecutionFailed)
+ {
+ return TestTags.StatusFail;
+ }
+
+ // Priority 3: Any execution passed -> pass (pass takes precedence over skip)
+ if (anyExecutionPassed)
+ {
+ return TestTags.StatusPass;
+ }
+
+ // Priority 4: Skip/inconclusive AND no pass -> skip
+ if (isSkippedOrInconclusive)
+ {
+ return TestTags.StatusSkip;
+ }
+
+ // Priority 5: All executions failed -> fail
+ return TestTags.StatusFail;
+ }
}
diff --git a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/SkipTestMethodExecutor.cs b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/SkipTestMethodExecutor.cs
index 696caeb8324f..3c8f3d5aa4cf 100644
--- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/SkipTestMethodExecutor.cs
+++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/SkipTestMethodExecutor.cs
@@ -8,6 +8,7 @@
using System.Reflection;
using System.Threading.Tasks;
using Datadog.Trace.Ci;
+using Datadog.Trace.Ci.Tags;
using Datadog.Trace.DuckTyping;
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Testing.MsTestV2;
@@ -37,8 +38,13 @@ protected void ProcessTestMethod(object testMethod)
if (testMethod.TryDuckCast(out var testMethodInfo))
{
// Create the skip span
- MsTestIntegration.OnMethodBegin(testMethodInfo, testMethod.GetType(), isRetry: false)?
- .Close(TestStatus.Skip, TimeSpan.Zero, _skipReason);
+ var test = MsTestIntegration.OnMethodBegin(testMethodInfo, testMethod.GetType(), isRetry: false);
+ if (test is not null)
+ {
+ // Set final_status = skip for pre-execution skipped tests (ITR/attribute-based skips)
+ test.GetTags().FinalStatus = TestTags.StatusSkip;
+ test.Close(TestStatus.Skip, TimeSpan.Zero, _skipReason);
+ }
}
}
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 2283520d730c..2c45047960d2 100644
--- a/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestMethodAttributeExecuteIntegration.cs
+++ b/tracer/src/Datadog.Trace/ClrProfiler/AutoInstrumentation/Testing/MsTestV2/TestMethodAttributeExecuteIntegration.cs
@@ -7,14 +7,18 @@
using System;
using System.Collections;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Datadog.Trace.Ci;
+using Datadog.Trace.Ci.Tags;
using Datadog.Trace.ClrProfiler.CallTarget;
using Datadog.Trace.ClrProfiler.CallTarget.Handlers;
using Datadog.Trace.DuckTyping;
+using Datadog.Trace.Util;
namespace Datadog.Trace.ClrProfiler.AutoInstrumentation.Testing.MsTestV2;
@@ -102,6 +106,15 @@ internal static CallTargetState OnMethodBegin(TTarget inst
public sealed class TestMethodAttributeExecuteAsyncIntegration
#pragma warning restore SA1402
{
+ // Per-row cache for parameterized test execution results, keyed by test identifier (DisplayName)
+ // This survives across initial and retry executions for the same test method
+ // Use ConcurrentDictionary for thread safety - MSTest can run parameterized rows in parallel
+ // ConditionalWeakTable allows garbage collection of testMethod without manual cleanup
+ private static readonly ConditionalWeakTable