Expand OpenTelemetry support: conventions, resource, env-driven setup, MSTest spans - #10358
Conversation
…STest spans Draft implementation of a broader observability story for MTP. Platform abstractions: - IPlatformActivity gains TraceId/SpanId/IsRecording/SetStatus/AddEvent/RecordException - tagged counter/histogram measurements, IUpDownCounter, observable gauges, CurrentActivity and ActivityKind on IPlatformOpenTelemetryService - TestingPlatformSemanticConventions centralises every attribute/instrument name - PlatformOpenTelemetryOptions (output capture, attribute limits, legacy names) - EnvironmentTraceContext reads and validates W3C TRACEPARENT/TRACESTATE Result handling: - spans named after the test case, semconv attributes (test.*, code.*, error.*) - span status and a proper "exception" event on failures - test.case.duration (seconds, dimensioned), test.case.result.count, test.case.active, test.run.duration; legacy instruments still emitted Hosts: runs nest under the caller's trace context; host/framework/session/request and extension spans carry semconv attributes and the final exit code. Extension package: status/event/exception support in the wrappers, explicit histogram buckets tuned for test durations, AddTestingPlatformResource() with CI/VCS detection, and AddOpenTelemetryProviderFromEnvironment() for OTEL_*-driven turnkey configuration. MSTest: a dependency-free instrumentation seam in the engine, bridged to the platform service, producing spans for assembly/class/test fixtures and the test method itself. Retry: per-attempt spans and a test.case.retry.count counter. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
…nter, run telemetry lifetime - MSTest fixture/test spans are no longer ambient. MSTest captures the ExecutionContext inside fixtures and replays it for every later test, and Activity.Current is an async-local, so an ambient span leaked into that context and parented the rest of the run (and any user-created span) to an already-stopped fixture span. Spans are now timed without ever becoming Activity.Current and are parented explicitly to the test framework span. - test.case.active stayed at the started-test count whenever no tracer was listening, because the decrement lived on the has-a-span path only. In-flight bookkeeping no longer depends on a span existing. - Run-level metrics were recorded from TestApplicationResult.Dispose(), after the OpenTelemetry providers had already been disposed, so they were dropped. The host now reports them while the providers and root span are alive. - Fixture call sites no longer pay an AssemblyName allocation per invocation when tracing is disabled. - Assembly initialize/cleanup spans now record the failure that RunWithTimeoutAndCancellationAsync returns instead of throws. - Replaced the CurrentActivity property, which handed out a disposable the caller did not own, with HasCurrentActivity. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
Covers the ambient-context behaviour that mock-based tests cannot see: that setAsCurrent:false spans are timed and exported without ever becoming Activity.Current and without popping an existing ambient activity, plus status, exception events and creation tags. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
…ty, turnkey cost - IsValidTraceParent accepted uppercase hex and version 'ff'. System.Diagnostics rejects both internally and silently starts a new trace, which is the exact orphan-trace failure this validation exists to prevent. - Legacy attribute twins bypassed truncation. Since legacy names are on by default, the full untruncated stdout/stderr/stack trace was still attached to every span, defeating the documented size limit and its PII rationale. - test.run.duration carried raw test counts as dimensions, which creates a new histogram time series per distinct count. The counts moved to the root span and the histogram now uses the bounded test.run.result.status instead of reusing the test-case attribute. - AddOpenTelemetryProviderFromEnvironment instrumented the run even when nothing would be exported: AddSource installs a sampling listener, so every test still allocated a fully tagged span. Instrumentation is now only installed when an exporter or a configuration delegate is present. - OTEL_TRACES_EXPORTER/OTEL_METRICS_EXPORTER are comma-separated lists per the specification, so 'otlp,console' silently disabled export. - test.case.duration named both a seconds-valued metric and a milliseconds-valued span attribute; the span attribute is now test.case.duration_ms. The MSTest data-row count no longer collides with the test.case.result.count metric. - host.arch emitted .NET spellings (x64/arm) instead of the OpenTelemetry enum. - TRACESTATE was documented and parsed but never applied; it is now set on the root spans. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
… tracestate reach - The test host controller shares TestApplicationResult but consumes no test node updates, so reporting from there emitted a second run record with zero counts and a ZeroTests exit code, contradicting the child process's real one. Only the host that observes the results now reports. - The root span was tagged with a stale exit code on the cancellation and server-mode paths, because the cancellation verdict was applied after the finally block. It is normalized before the span and telemetry read it, and the host now passes its own exit code instead of having TestApplicationResult recompute a different one. - tracestate only reached the root span: Activity derives it from an in-process parent reference, and every span carrying test data is created with an explicit parent id string. It now lives on the service and is stamped onto those spans, so the caller's vendor sampling state reaches the test data. The unused IPlatformActivity.TraceState member is gone. - A malformed property bag could leak an already-dequeued span; disposal moved to a finally. Adds unit tests for the MSTest instrumentation seam being fully inert until a host installs a factory. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
…conv 1.43.0) Verified every emitted name against the OpenTelemetry semantic conventions rather than assuming; several claims did not hold. - test.case.result.status upstream defines only 'pass'/'fail', not 'passed'/'failed'. The semconv attribute now uses the upstream spelling while the legacy test.result attribute keeps its original values. - code.namespace, error.message and exception.escaped are all deprecated upstream and are no longer emitted. code.function.name now carries the fully qualified name as the convention requires, and the exception message travels on the exception event. - os.type has no defined catch-all value, so an unrecognised platform omits the attribute instead of inventing 'other'. - There are no upstream test.* metrics and no upstream test-case span conventions. The instruments, the extra result statuses and cicd.provider.name are now documented as platform extensions instead of being presented as standard, in both the constants file and PACKAGE.md. - Removed constants that were declared but never emitted, including test.run.id which does not exist upstream. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
A cancelled run skips NotifyTestSessionEndAsync, so the message bus is never drained or disabled before the host disposes services. A consumer can therefore still be publishing results while Dispose enumerates the in-flight dictionary, which would throw a collection-modified exception out of the telemetry path during shutdown. Telemetry must never fail a run. The bookkeeping is now guarded, Dispose drains into a local list before closing the spans, and a result that arrives after disposal closes its own span instead of being tracked. Adds tests for the post-dispose path and for concurrent completion during disposal. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
The namespace is empty for a type declared in the global namespace, so building the fully qualified name by interpolation produced '.MyClass.MyMethod'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
TestMethodIdentifierProperty.Namespace is a plain string filled in by whichever framework produced the node. MSTest derives it by splitting the managed type name, so a global-namespace type yields an empty string, but a framework built on Roslyn that calls INamespaceSymbol.ToDisplayString() without first checking IsGlobalNamespace hands us '<global namespace>' instead. Both now contribute no namespace segment. The segment is dropped rather than emitting Roslyn's 'global::' prefix: that keeps the attribute language-agnostic and matches both the convention's own examples and how the rest of this repository treats the global namespace (TestClassModelBuilder, ReflectionMetadataGenerator and DiscoveredTestsJsonSerializer all omit it). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
The guard added to OpenTelemetryResultHandler is a local mitigation for one consumer. The underlying platform behaviour - a cancelled run skips the message bus drain/disable, so any IDataConsumer can be disposed while still consuming - predates this work and is tracked separately. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Expands OpenTelemetry across MTP, MSTest, and retry execution with richer signals, environment configuration, resources, and trace correlation.
Changes:
- Adds semantic attributes, metrics, resources, context propagation, and OTLP setup.
- Instruments MSTest lifecycle operations and retry attempts.
- Adds tests, documentation, dependencies, and API tracking.
Show a summary per file
| File | Description |
|---|---|
test/UnitTests/MSTestAdapter.PlatformServices.UnitTests/MSTestInstrumentationTests.cs |
Tests the MSTest tracing seam. |
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Telemetry/OpenTelemetryResultHandlerTests.cs |
Expands result telemetry tests. |
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Telemetry/EnvironmentTraceContextTests.cs |
Tests environment trace context parsing. |
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/TestApplicationResultTests.cs |
Updates telemetry service mocks. |
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/OpenTelemetryPlatformServiceTests.cs |
Tests live activity behavior. |
test/UnitTests/Microsoft.Testing.Extensions.UnitTests/Microsoft.Testing.Extensions.UnitTests.csproj |
References the OpenTelemetry extension. |
src/Platform/Microsoft.Testing.Platform/Telemetry/TestingPlatformSemanticConventions.cs |
Centralizes telemetry names and values. |
src/Platform/Microsoft.Testing.Platform/Telemetry/PlatformOpenTelemetryOptions.cs |
Adds environment-driven telemetry options. |
src/Platform/Microsoft.Testing.Platform/Telemetry/PlatformActivityStatusCode.cs |
Defines activity status abstraction. |
src/Platform/Microsoft.Testing.Platform/Telemetry/PlatformActivityKind.cs |
Defines activity kind abstraction. |
src/Platform/Microsoft.Testing.Platform/Telemetry/OpenTelemetryResultHandler.cs |
Adds richer spans and metrics. |
src/Platform/Microsoft.Testing.Platform/Telemetry/Meters.cs |
Extends metric abstractions. |
src/Platform/Microsoft.Testing.Platform/Telemetry/IPlatformOpenTelemetryService.cs |
Expands telemetry service capabilities. |
src/Platform/Microsoft.Testing.Platform/Telemetry/IPlatformActivity.cs |
Adds status, events, and identifiers. |
src/Platform/Microsoft.Testing.Platform/Telemetry/EnvironmentTraceContext.cs |
Reads W3C context from environment. |
src/Platform/Microsoft.Testing.Platform/Services/TestApplicationResult.cs |
Reports run-level telemetry. |
src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs |
Adds conventional framework/session tags. |
src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt |
Tracks new platform internals. |
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.cs |
Parents runs to environment context. |
src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs |
Records root run verdicts. |
src/Platform/Microsoft.Testing.Platform/Helpers/ExtensionHelper.cs |
Adds conventional extension tags. |
src/Platform/Microsoft.Testing.Platform/Helpers/EnvironmentVariableConstants.cs |
Defines telemetry environment variables. |
src/Platform/Microsoft.Testing.Extensions.Retry/RetryOrchestrator.cs |
Instruments retry attempts. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/UpDownCounterWrapper.cs |
Implements UpDownCounter support. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/TestingPlatformResourceDetector.cs |
Detects process and CI resources. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/PublicAPI/PublicAPI.Unshipped.txt |
Tracks new public extension APIs. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/PACKAGE.md |
Documents signals and configuration. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/OpenTelemetryProviderExtensions.cs |
Adds resource and environment setup APIs. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/OpenTelemetryPlatformService.cs |
Implements expanded telemetry abstractions. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/OpenTelemetryEnvironmentVariables.cs |
Defines standard OTEL variables. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/Microsoft.Testing.Extensions.OpenTelemetry.csproj |
Adds the OTLP exporter dependency. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/MeasurementTags.cs |
Converts metric tag collections. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/InternalAPI/InternalAPI.Unshipped.txt |
Tracks extension internals. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/HistogramWrapper.cs |
Adds tagged histogram recording. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/CounterWrapper.cs |
Adds tagged counter recording. |
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/ActivityWrapper.cs |
Implements status and exception events. |
src/Adapter/MSTestAdapter.PlatformServices/Telemetry/MSTestInstrumentation.cs |
Introduces the MSTest tracing seam. |
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs |
Instruments test-method execution. |
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.Lifecycle.cs |
Instruments test fixtures. |
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.Initializer.cs |
Instruments class initialization. |
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.Cleanup.cs |
Instruments class cleanup. |
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs |
Instruments assembly fixtures. |
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestTestFramework.cs |
Enables MSTest instrumentation. |
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestPlatformActivity.cs |
Bridges MSTest spans to MTP. |
src/Adapter/MSTest.TestAdapter/InternalAPI/InternalAPI.Unshipped.txt |
Tracks the new bridge internals. |
Directory.Packages.props |
Pins the OTLP exporter version. |
Review details
Suppressed comments (3)
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs:321
- If
RunWithTimeoutAndCancellationAsyncthrows, this catch stores the failure only after theusingscope has already disposed the assembly-cleanup span, so that span is exported without Error status or an exception event. Keep the activity in scope for the catch and callRecordException(ex)before disposal.
catch (Exception ex)
{
AssemblyCleanupException = ex;
}
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs:137
Inconclusiveis not treated as skipped here, so an all-inconclusive invocation is taggedpass; the adapter normally converts inconclusive results to skipped (or failed when configured). Include this outcome and account forMapInconclusiveToFailedso the MSTest span agrees with the published test node.
if (testResult.Outcome is not (UnitTestOutcome.Ignored or UnitTestOutcome.NotRunnable))
{
allSkipped = false;
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs:143
Aborted,Unknown, andNotFoundare omitted from the failure set, so invocations with those outcomes fall through topasseven though the adapter publishes them as failures/errors. Include every non-success failure outcome in this branch.
if (testResult.Outcome is UnitTestOutcome.Failed or UnitTestOutcome.Error or UnitTestOutcome.Timeout)
{
anyFailed = true;
}
- Files reviewed: 46/46 changed files
- Comments generated: 14
- Review effort level: Balanced
- Mark the MSTest.TestMethod span failed when the failure is returned rather than thrown. An ordinary assertion failure comes back in the TestResult, so the span previously stayed Unset and looked green for a failed test. - Strip URL user info before emitting vcs.repository.url.full. GitLab's CI_REPOSITORY_URL embeds gitlab-ci-token credentials, so the job token was being exported to the telemetry backend; the sanitizer covers every provider, including Jenkins GIT_URL. - Return from AddOpenTelemetryProviderFromEnvironment before registering anything when no exporter and no delegate are configured, so the documented 'costs nothing when not opted in' actually holds and the MSTest seam stays off. - Honor TESTINGPLATFORM_OTEL_EMIT_LEGACY_ATTRIBUTES for the extension tags and the SessionUid/RequestType session tags, which were emitted unconditionally. - Balance test.case.active when Dispose drains in-flight entries. - Derive test.run.result.status from the exit code as well as the failed count, so cancellation, zero-tests and threshold failures no longer report pass. - Emit the exception event and status with truncated values so the attribute length limit is not bypassed for stack traces. - Add the MSTest instrumentation types to the internal API baseline, restore the UTF-8 BOM on MSTestInstrumentationTests.cs, and make the test-only tag lookup use LINQ. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
There was a problem hiding this comment.
Review details
Suppressed comments (5)
src/Platform/Microsoft.Testing.Platform/InternalAPI/InternalAPI.Unshipped.txt:448
- The shipped four-parameter
StartActivitysignature is still listed inInternalAPI.Shipped.txt, but this file only declares the new six-parameter signature. PublicApiAnalyzers will report RS0017 under warnings-as-errors unless the old signature is explicitly marked removed.
Microsoft.Testing.Platform.Telemetry.IPlatformOpenTelemetryService.StartActivity(string! name = "", System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, object?>>? tags = null, string? parentId = null, System.DateTimeOffset startTime = default(System.DateTimeOffset), Microsoft.Testing.Platform.Telemetry.PlatformActivityKind kind = Microsoft.Testing.Platform.Telemetry.PlatformActivityKind.Internal, bool setAsCurrent = true) -> Microsoft.Testing.Platform.Telemetry.IPlatformActivity?
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/InternalAPI/InternalAPI.Unshipped.txt:31
- Both the one-parameter
ActivityWrapperconstructor and the four-parameterOpenTelemetryPlatformService.StartActivityremain in this project'sInternalAPI.Shipped.txt. Their changed CLR signatures require*REMOVED*declarations; otherwise RS0017 fails the warnings-as-errors build.
Microsoft.Testing.Extensions.OpenTelemetry.ActivityWrapper.ActivityWrapper(System.Diagnostics.Activity! activity, bool isAmbient = true) -> void
Microsoft.Testing.Extensions.OpenTelemetry.OpenTelemetryPlatformService.HasCurrentActivity.get -> bool
Microsoft.Testing.Extensions.OpenTelemetry.OpenTelemetryPlatformService.StartActivity(string! name = "", System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string!, object?>>? tags = null, string? parentId = null, System.DateTimeOffset startTime = default(System.DateTimeOffset), Microsoft.Testing.Platform.Telemetry.PlatformActivityKind kind = Microsoft.Testing.Platform.Telemetry.PlatformActivityKind.Internal, bool setAsCurrent = true) -> Microsoft.Testing.Platform.Telemetry.IPlatformActivity?
src/Platform/Microsoft.Testing.Platform/Helpers/EnvironmentVariableConstants.cs:50
- This source file is linked into Retry, TrxReport, HangDump, and HotReload, whose existing constants are tracked in each project's
InternalAPI.Unshipped.txt. None of those four files declares these seven new constants, so RS0016 will fail those builds. Add the corresponding entries to every tracking consumer (the linked-project references are at Retry.csproj:26, TrxReport.csproj:27, HangDump.csproj:44, and HotReload.csproj:13).
// OpenTelemetry
// W3C trace context of the process that started this test run, so the run nests under it.
public const string TRACEPARENT = nameof(TRACEPARENT);
public const string TRACESTATE = nameof(TRACESTATE);
public const string TESTINGPLATFORM_TRACEPARENT = nameof(TESTINGPLATFORM_TRACEPARENT);
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/OpenTelemetryPlatformService.cs:37
RootTraceStateis applied only afterActivitySource.StartActivityhas already run its sampler. A sampler therefore cannot see the propagated tracestate, and if it drops the root activity this block is never reached at all. This breaks vendor sampling decisions carried in tracestate. Parse the traceparent and tracestate into anActivityContextand pass that context to theStartActivityoverload so sampling receives both values.
// Activity only derives tracestate from an in-process parent reference, which an explicit parent id string
// does not provide. Stamp it on so the caller's vendor sampling state survives down to the test spans.
if (RootTraceState is not null && activity.TraceStateString is null)
{
activity.TraceStateString = RootTraceState;
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestPlatformActivity.cs:21
- MSTest failures bypass
TESTINGPLATFORM_OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIThere.ActivityWrapper.RecordExceptionwrites the full exception message andexception.ToString()into event attributes, while onlyOpenTelemetryResultHandlerappliesPlatformOpenTelemetryOptions.Truncate. Large assertion messages or fixture exceptions can therefore produce unbounded payloads despite the configured limit. Pass the resolved limit into this bridge and truncate the MSTest exception event/status consistently.
public void RecordException(Exception exception)
=> activity.RecordException(exception);
- Files reviewed: 48/48 changed files
- Comments generated: 1
- Review effort level: Balanced
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
🤖 Automated content by GitHub Copilot. Generated by the Grade Tests on PR (on open / sync) workflow. · sonnet46 110.8 AIC · ⌖ 5.43 AIC · ⊞ 11.8K · ◷
Comments that could not be inline-anchored
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Telemetry/OpenTelemetryResultHandlerTests.cs:385
🧪 Grade B (80–89) — Asserts counter increments but does not verify both activities are independently tracked in FIFO order.
Assert that both activities are tracked by completing each and checking their tags are set separately.
Assert.AreEqual(2, _startedCounter.Value);
// Both activities must be independently completable without throwing.
_handler.NotifyPassed(testNode, PassedTestNodeStateProperty.CachedInstance);
_handler.NotifyPassed(testNo…
</details>
<details><summary>test/UnitTests/Microsoft.Testing.Platform.UnitTests/Telemetry/OpenTelemetryResultHandlerTests.cs:141</summary>
🧪 **Grade B (80–89)** — Only verifies no exception; a mutation returning a non-null dummy activity would pass silently.
After NotifyPassed, assert that no SetTag or Dispose was called on any mock activity.
```suggestion
// Should not throw when completing the test (no activity tracked).
_handler.NotifyPassed(testNode, PassedTestNodeStateProperty.CachedInstance);
// Verify no activity methods were invoked (null-activity path is inert).
Assert.AreEqual(1, _comp…
</details>
<details><summary>test/UnitTests/Microsoft.Testing.Platform.UnitTests/Telemetry/OpenTelemetryResultHandlerTests.cs:694</summary>
🧪 **Grade B (80–89)** — Purely a "does not throw" smoke test; the concurrent state transitions are not asserted on.
After publisher completes, assert `_activeTestCases.Value` equals 0 to confirm all in-flight tests resolved.
```suggestion
_handler.Dispose();
publisher.GetAwaiter().GetResult();
// All in-flight test nodes were either completed or orphaned; gauge must be back at zero.
Assert.AreEqual(0, _activeTestCases.Value);
test/UnitTests/Microsoft.Testing.Platform.UnitTests/Telemetry/EnvironmentTraceContextTests.cs:89
🧪 Grade B (80–89) — Single trivial null-return assertion; no positive case is paired and the test does not verify which environment variable was consulted.
Add a mock verification to confirm the method accessed the expected variable before returning null.
Assert.IsNull(EnvironmentTraceContext.TryGetTraceState(environment.Object));
environment.Verify(e => e.GetEnvironmentVariable("TRACESTATE"), Times.Once);
GetAggregateOutcomeName only treated Failed, Error and Timeout as failing and only Ignored/NotRunnable as skipped, so Inconclusive, Aborted and Unknown all reported 'pass' on the span even though the adapter maps Aborted and Unknown to failed, and Inconclusive to skipped (or failed under MapInconclusiveToFailed). The mapping now mirrors UnitTestOutcomeHelper.ToTestOutcome, including the MapInconclusiveToFailed and MapNotRunnableToFailed settings, and the failure lookup uses the same mapping so the span status agrees with the reported status. Also starts the result array empty rather than null so the finally block needs no null-forgiving operator, which is behavior-neutral on every reachable path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
The concurrency test now asserts test.case.active settles at zero, which is the invariant the locking fix exists to preserve, and the no-tracer test asserts the result is still counted rather than only that nothing throws. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
|
Thanks, the mutation notes were useful. I took the two that closed real assertion gaps:
I left the other two. |
There was a problem hiding this comment.
Review details
Suppressed comments (16)
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.Initializer.cs:292
- An ordinary class-initialize exception is rethrown by
RunWithTimeoutAndCancellationAsync(seeFixtureMethodRunner.cs:37and:145-148), so execution skips theRecordExceptionblock and disposes this new span withUnsetstatus. Catch, record, and rethrow around the await so thrown fixture failures are exported as failed spans as well as timeout/cancellation results.
TestFailedException? result = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync(
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.Lifecycle.cs:271
- A normal exception from
TestInitializeis rethrown by the fixture runner, so this await exits beforeRecordExceptionand the new initialize span remains green. Catch, record, and rethrow so both returned timeout/cancellation failures and thrown user-code failures mark the span.
TestFailedException? result = await InvokeFixtureMethodAsync(
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs:300
- Unlike assembly initialization, assembly cleanup has no catch inside the activity scope. When the fixture body throws, the outer catch at line 318 records the adapter failure only after this activity has already been disposed, so the cleanup span remains green. Record and rethrow inside this scope before the outer catch converts the result.
AssemblyCleanupException = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync(
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.Cleanup.cs:208
- Thrown class-cleanup failures bypass the code after this await because
RunWithTimeoutAndCancellationAsyncrethrows ordinary fixture exceptions. Theusingthen closes the span without an exception event or Error status; record the exception in a catch before rethrowing it to the existing caller.
TestFailedException? result = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync(
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.Lifecycle.cs:314
- A thrown
TestCleanupexception skips the post-awaitRecordExceptionblock, leaving the cleanup span withUnsetstatus even though the adapter reports a cleanup failure. Record and rethrow thrown exceptions inside the activity lifetime.
TestFailedException? result = await InvokeFixtureMethodAsync(
src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.CommonServices.cs:138
TRACESTATEis stamped even whenTRACEPARENTis absent or invalid, so a newly created root trace can inherit vendor state that belongs to no parent context. W3C tracestate is only valid alongside a valid traceparent; resolve the parent once and only carry tracestate when that parent was accepted.
platformOTelService.RootTraceState = EnvironmentTraceContext.TryGetTraceState(systemEnvironment);
context.BuilderActivity = platformOTelService.StartActivity(
TestingPlatformSemanticConventions.Activities.TestHostBuilder,
parentId: EnvironmentTraceContext.TryGetParentId(systemEnvironment),
startTime: buildBuilderStart);
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/TestingPlatformResourceDetector.cs:116
GITHUB_REPOSITORYcontainsowner/repository, but the OpenTelemetry definition ofvcs.repository.nameexplicitly excludes the organization/group (for example, it expectssemantic-conventions, notopen-telemetry/semantic-conventions). Split off the owner before emitting this attribute, and optionally emit it separately asvcs.owner.name.
("vcs.repository.name", "GITHUB_REPOSITORY")))
src/Platform/Microsoft.Testing.Extensions.Retry/RetryOrchestrator.cs:168
- The attempt span is never assigned a status from
attemptResult.ExitCode, including theExitedBeforeConnectearly return. Failed retry attempts therefore still appear as green/Unset spans, contrary to the PR's failure-status goal. Set Error/Ok after the attempt completes and mark the early-connect failure before returning.
using IPlatformActivity? attemptActivity = otelService?.StartActivity(
"RetryAttempt",
tags:
[
new(TestingPlatformSemanticConventions.Attributes.TestCaseRetryAttempt, attemptCount),
new("test.retry.max_attempts", userMaxRetryCount + 1),
new("test.retry.scheduled_count", lastListOfFailedId?.Length ?? 0),
]);
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestPlatformActivity.cs:21
- This bridge sends full exception messages and
exception.ToString()throughActivityWrapper.RecordException, bypassingTESTINGPLATFORM_OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT. As a result, the newly added MSTest fixture/method spans can still export arbitrarily large or sensitive stack traces even though the package documents that limit. Pass the resolved limit into this path or provide a truncating record operation.
public void RecordException(Exception exception)
=> activity.RecordException(exception);
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/OpenTelemetryProviderExtensions.cs:209
- The standard OpenTelemetry boolean grammar is case-insensitive, but this recognizes only three hand-picked casings. For example,
OTEL_SDK_DISABLED=TrUeincorrectly enables and exports telemetry. Use an ordinal case-insensitive comparison (while retaining1as the supported alias).
private static bool IsTrue(string? value)
=> value is "1" or "true" or "True" or "TRUE";
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestPlatformActivity.cs:46
- This enables the static MSTest factory whenever the telemetry service exists, not when tracing is enabled. A metrics-only
AddOpenTelemetryProviderFromEnvironmentregistration still creates the service but has no ActivitySource listener, so every test and fixture now allocates its tag array only forStartActivityto return null. Expose listener availability or make tag creation lazy so the advertised inert tracing path also holds for metrics-only configurations.
if (serviceProvider.GetService(typeof(IPlatformOpenTelemetryService)) is not IPlatformOpenTelemetryService otelService)
{
MSTestInstrumentation.SetActivityFactory(null);
return;
}
MSTestInstrumentation.SetActivityFactory((name, tags)
// setAsCurrent: false is required, not an optimization - see the remarks on MSTestInstrumentation.
// Because the spans are not ambient they cannot pick a parent up from the ambient context either, so
// they are parented explicitly to the same test-framework span the platform's test-case spans use,
// which keeps everything in one trace.
=> otelService.StartActivity(name, tags, parentId: otelService.TestFrameworkActivity?.Id, setAsCurrent: false) is { } platformActivity
src/Platform/Microsoft.Testing.Platform/Telemetry/PlatformOpenTelemetryOptions.cs:36
- The documented global attribute limit is not actually enforced for all string attributes. For example,
GetTestInitialInfoemits display names, paths, and metadata values unchanged, andSetResultDetailsemits metadata values and step descriptions unchanged. Those values are user-controlled and can exceed this limit, so the package'sTESTINGPLATFORM_OTEL_ATTRIBUTE_VALUE_LENGTH_LIMITcontract remains incomplete; apply truncation centrally to every string tag value or narrow the documented contract.
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/OpenTelemetryProviderExtensions.cs:129 - There are no tests for this new public environment-driven API. Its signal-specific branches (
none,otlp, comma-separated exporters, endpoint-only opt-in, SDK disablement, and delegates) determine whether providers and exporters are installed, and a prior bug already occurred in the no-configuration branch. Add unit tests that isolate the relevant environment variables and verify registration/configuration for each branch.
public static void AddOpenTelemetryProviderFromEnvironment(this ITestApplicationBuilder builder, Action<TracerProviderBuilder>? configureTracing = null, Action<MeterProviderBuilder>? configureMetrics = null)
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/TestingPlatformResourceDetector.cs:107
- The new CI/resource detector has no tests, despite provider-specific mapping, precedence, and credential stripping being security-sensitive. Add isolated environment-variable tests for each provider, repository-name normalization, URL user-info removal, and the no-CI path so future mapping changes cannot leak credentials or silently emit non-conventional values.
private static IEnumerable<KeyValuePair<string, object>> GetCiAttributes()
{
if (IsTrue(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")))
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/OpenTelemetryPlatformService.cs:37
RootTraceStateis applied only afterActivitySource.StartActivityreturns, but OpenTelemetry sampling runs insideStartActivity. A sampler therefore never sees the inherited tracestate and may drop a trace that the caller's vendor state intended to retain;ActivityStartedlisteners also observe it as null. Parse the parent ID and tracestate into anActivityContextand use the parent-context overload so tracestate participates in sampling as well as export.
// Activity only derives tracestate from an in-process parent reference, which an explicit parent id string
// does not provide. Stamp it on so the caller's vendor sampling state survives down to the test spans.
if (RootTraceState is not null && activity.TraceStateString is null)
{
activity.TraceStateString = RootTraceState;
src/Platform/Microsoft.Testing.Extensions.Retry/RetryOrchestrator.cs:127
- No tests exercise the new retry counter or per-attempt spans. Add coverage for first-attempt exclusion, scheduled-count values on later attempts, success/failure span status, and interrupted/early-exit attempts; otherwise the newly added retry telemetry can regress while the existing retry behavior tests still pass.
IPlatformOpenTelemetryService? otelService = _serviceProvider.GetPlatformOTelService();
ICounter<int>? retryCounter = otelService?.CreateCounter<int>(
TestingPlatformSemanticConventions.Metrics.TestRetryCount,
TestingPlatformSemanticConventions.Units.Count,
"Number of test cases scheduled for a retry attempt.");
- Files reviewed: 48/48 changed files
- Comments generated: 0 new
- Review effort level: Balanced
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Review details
Suppressed comments (11)
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.Lifecycle.cs:314
- A regular
TestCleanupfailure is rethrown byRunWithTimeoutAndCancellationAsync, so execution skips this assignment's followingRecordExceptionblock and disposes an Unset span. Catch, record, and rethrow around this await as is done for returned timeout/cancellation failures.
TestFailedException? result = await InvokeFixtureMethodAsync(
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.Lifecycle.cs:271
- Ordinary
TestInitializeexceptions never reach this post-awaitcheck:FixtureMethodRunner.RunWithTimeoutAndCancellationAsyncrethrows non-cancellation exceptions. Theusingthen closes the fixture span with an Unset status, so precisely the failing fixture this instrumentation is meant to expose still appears green. Record the exception before rethrowing it.
This issue also appears on line 314 of the same file.
TestFailedException? result = await InvokeFixtureMethodAsync(
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.Initializer.cs:292
RunWithTimeoutAndCancellationAsyncrethrows an exception thrown by the class initializer, so the code below that records a returned failure is bypassed. The enclosing class-initialize logic converts that exception to a failed result only after thisusinghas disposed the span, leaving the fixture span Unset. Record and rethrow here.
TestFailedException? result = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync(
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.Cleanup.cs:208
- A class-cleanup method that throws takes the exception path out of
RunWithTimeoutAndCancellationAsync; the laterresultcheck is therefore never reached, and the activity is disposed as successful/Unset before the outer cleanup code converts the exception. Catch, record, and rethrow around this await.
TestFailedException? result = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync(
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs:300
- Only timeout/cancellation failures returned by the helper are recorded here. If
AssemblyCleanupitself throws, theusingactivity is disposed while unwinding and the outer catch at line 318 stores the failure after the span is already closed, so that span remains Unset. Keep the activity alive through the catch and callRecordExceptionthere before disposing it.
AssemblyCleanupException = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync(
src/Adapter/MSTest.TestAdapter/TestingPlatformAdapter/MSTestPlatformActivity.cs:41
- A metrics-only setup (
OTEL_METRICS_EXPORTER=otlp,OTEL_TRACES_EXPORTER=none) still registersIPlatformOpenTelemetryService, so this enables the global MSTest factory even though no activity listener exists. Every test and fixture then allocates tag arrays and callsStartActivityonly to receive null, contradicting the intended single-field-read disabled path. Gate factory installation on tracing/listener enablement rather than service presence alone.
MSTestInstrumentation.SetActivityFactory((name, tags)
src/Platform/Microsoft.Testing.Platform/Requests/TestHostTestFrameworkInvoker.cs:123
- These conventional framework tags are only added on the base path.
HotReloadTestHostTestFrameworkInvoker.ExecuteRequestAsyncoverrides this method and creates its ownTestFrameworkactivity with onlytestFramework.ToOTelTags(), so hot-reload runs omittest.framework.nameandtest.framework.version. Share this tag construction with the override or update that override as part of this change.
src/Platform/Microsoft.Testing.Platform/Telemetry/OpenTelemetryResultHandler.cs:552 IPlatformActivity.IsRecordingwas added specifically to avoid building large payloads when a listener requested propagation only, but this path checks only for a non-null activity. It still truncates/copies stdout and stderr even thoughSetTagwill not record them. Include!activity.IsRecordingin this guard.
src/Platform/Microsoft.Testing.Platform/Telemetry/OpenTelemetryResultHandler.cs:503- The documented attribute-value limit is bypassed for metadata values here (and in the initial metadata tags), so a large trait can still export an unbounded string despite
TESTINGPLATFORM_OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT. Apply_options.Truncate(metadataProperty.Value)to both the semantic and legacy tags, and apply the same policy centrally to the other string attributes if the option is intended to cover every attribute as documented.
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodRunner.cs:223 UnitTestOutcome.NotFoundfalls into the skipped default here, butUnitTestOutcomeHelper.ToTestOutcomemaps it toTestOutcome.NotFound, and the MTP converter publishes that asErrorTestNodeStateProperty. The inner MSTest span therefore reportsskippedwhile the outer test-case span reportserror. MapNotFoundto the failing status (and add a regression test) so the two spans agree.
_ => SkippedOutcomeName,
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/TestingPlatformResourceDetector.cs:190
- The new CI resource detector and credential-stripping path have no tests: repository-wide test search finds no coverage for
TestingPlatformResourceDetector,CI_REPOSITORY_URL, orRemoveUrlUserInfo. Add tests for each provider mapping and URL forms with user info, query/fragment, and no scheme; otherwise a future mapping change can reintroduce credential export unnoticed.
private static string RemoveUrlUserInfo(string value)
- Files reviewed: 48/48 changed files
- Comments generated: 0 new
- Review effort level: Balanced
…Activity
Adding optional parameters to StartActivity removed its 4-argument signature.
The C# compiler bakes optional-parameter defaults into the call site, so the
already-shipped HotReload 2.2.1 binary references that exact signature and the
test host crashed with MissingMethodException, which is what failed the Debug
legs of the forward-compatibility acceptance test:
MissingMethodException: Method not found: 'IPlatformActivity
IPlatformOpenTelemetryService.StartActivity(String,
IEnumerable<KeyValuePair<String,Object>>, String, DateTimeOffset)'
at HotReloadTestHostTestFrameworkInvoker.ExecuteRequestAsync
StartActivity is restored to its original signature and the non-ambient
behavior moved to a separate StartNonAmbientActivity method. Verified by
reflection that the emitted signature matches the one the exception named, and
by running the acceptance test against packed packages.
Also removes PlatformActivityKind, which this PR added but never used with any
value other than Internal, and documents on StartActivity why its signature is
frozen: no analyzer catches this, only the forward-compatibility test does.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 1fd8d719-a2d1-41ea-923e-0bead40aa87b
🧪 Test quality grade — PR #10358No new or modified test methods were identified in the changed regions of this PR. Nothing to grade. Re-run with
|
There was a problem hiding this comment.
Review details
Suppressed comments (8)
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.Lifecycle.cs:275
- When the test-initialize method throws normally,
InvokeFixtureMethodAsyncpropagates the exception; only timeout/cancellation failures are returned inresult. Theusingthen closes this span without recording the thrown failure, so a failing initializer is exported as green. Record the exception and rethrow around this await, as well as handling the returned failure below.
This issue also appears on line 314 of the same file.
TestFailedException? result = await InvokeFixtureMethodAsync(
methodInfo,
classInstance,
arguments: null,
timeout,
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.Initializer.cs:296
RunWithTimeoutAndCancellationAsyncrethrows ordinary class-initialize exceptions; it returns aTestFailedExceptiononly for timeout/cancellation paths. Consequently the activity is disposed during unwinding beforeresultcan be recorded, and a throwing class initializer produces a green span. Record and rethrow exceptions around this await.
TestFailedException? result = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync(
() => methodInfo.InvokeAsFixtureMethodAsync(
testContext,
ec => ExecutionContext = ec),
testContext.CancellationTokenSource,
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestAssemblyInfo.cs:304
- A normally thrown assembly-cleanup exception jumps to the outer catch after this
usinghas already disposed the activity; only returned timeout/cancellation failures are recorded below. Record the exception inside the activity scope and rethrow it to the existing outer catch so throwing assembly cleanup methods do not produce green spans.
AssemblyCleanupException = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync(
() => AssemblyCleanupMethod.InvokeAsFixtureMethodAsync(
testContext,
ec => ExecutionContext = ec),
testContext.CancellationTokenSource,
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestMethodInfo.Lifecycle.cs:318
- A normally thrown test-cleanup exception propagates from
InvokeFixtureMethodAsync, bypassing theresultcheck and disposing this span with no failure status. Catch only to record the exception on the activity, then rethrow so the existing cleanup error handling remains unchanged.
TestFailedException? result = await InvokeFixtureMethodAsync(
methodInfo,
classInstance,
arguments: null,
timeout,
src/Adapter/MSTestAdapter.PlatformServices/Execution/TestClassInfo.Cleanup.cs:212
- Ordinary class-cleanup exceptions are thrown from this helper rather than returned. That skips the
resultcheck and disposes the new activity as successful while the caller reports a cleanup failure. Record the thrown exception on the activity before rethrowing it.
TestFailedException? result = await FixtureMethodRunner.RunWithTimeoutAndCancellationAsync(
() => methodInfo.InvokeAsFixtureMethodAsync(
testContext,
ec => ExecutionContext = ec),
testContext.CancellationTokenSource,
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/OpenTelemetryPlatformService.cs:52
RootTraceStateis stamped only afterActivitySource.StartActivityhas already run the sampler. For an explicit parent-id string, the sampler therefore receives a parent context withouttracestate; a vendor sampler can reject the span, in which caseStampis never reached at all. Parse a W3C parent id together withRootTraceStateinto anActivityContextand use the context overload before sampling (while retaining a fallback for non-W3C parent ids).
if (RootTraceState is not null && activity.TraceStateString is null)
{
activity.TraceStateString = RootTraceState;
src/Platform/Microsoft.Testing.Extensions.OpenTelemetry/OpenTelemetryProviderExtensions.cs:193
- A standard signal-specific endpoint does not opt this API in. With only
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTorOTEL_EXPORTER_OTLP_METRICS_ENDPOINTset and the exporter selector unset, this checks only the shared endpoint, both provider flags can remain false, and the method returns without exporting. Include the corresponding signal endpoint when deciding whether that signal has an OTLP configuration.
// Mirror the behavior of the OpenTelemetry auto-instrumentation: an endpoint alone is enough to opt in.
if (OpenTelemetryEnvironmentVariables.IsNullOrWhiteSpace(configured))
{
return !OpenTelemetryEnvironmentVariables.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(OpenTelemetryEnvironmentVariables.ExporterOtlpEndpoint));
}
src/Platform/Microsoft.Testing.Platform/Hosts/CommonTestHost.cs:145
ServerTestHostis also not a run-verdict owner at this point: test execution happens inServerTestHost.RequestExecution, while thisfinallyruns only when the server message loop exits. Reporting here makestest.run.durationcover server startup/idle time and emits one shutdown record instead of one record per run request (with cumulative shared result state). Exclude the server host here and report the per-requestTestApplicationResultwhen each run request completes.
if (OwnsRunVerdict
&& ServiceProvider.GetService<ITestApplicationProcessExitCode>() is TestApplicationResult testApplicationResult)
{
testApplicationResult.ReportRunTelemetry(activity, exitCode);
}
- Files reviewed: 47/47 changed files
- Comments generated: 3
- Review effort level: Balanced
Brings in #10358 (expanded OpenTelemetry support), which touches `TestApplicationResult` — the same `ConsumeAsync` that carries this branch's superseded-retry-attempt guard. One conflict, purely additive: `InternalAPI.Unshipped.txt` gained the new OpenTelemetry surface alongside this branch's `TestProgressState.Report*Test` overloads. Kept both. `TestApplicationResult.cs` auto-merged, and the result is semantically correct: the retry guard sits above the outcome switch, so a superseded attempt now also skips the new OpenTelemetry notifications. That is the desired behavior — an attempt a later one supersedes is not the test's outcome, so it must not emit a failed span or count toward the OTel metrics, exactly as it must not count toward the exit code. The activity accounting stays balanced across a retry: `NotifyInProgress` enqueues one activity per test (MSTest reports in-progress once per test, not once per attempt, and in-progress nodes carry no `RetryAttemptProperty`), and the guard ensures exactly one dequeue from the final attempt rather than one per attempt. Added `ConsumeAsync_SupersededRetryAttempt_DoesNotCloseTheTestActivity` to lock that interaction in. Verified by mutation: disabling the guard fails this test along with the two existing superseded-attempt tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: fa37ea7e-23a2-458b-bb8a-68ad56c832e6
Why
The OpenTelemetry extension worked, but it was minimal: traces and metrics only, custom (non-conventional) attribute names, no span status so failing tests produced green spans, eight untagged instruments with no units or buckets, no resource attributes, no
OTEL_*support, no trace context propagation, and no instrumentation at all inside MSTest or the other extensions. In practice a trace showed one span per test case and nothing that explained a slow or failing run.This makes the extension useful out of the box and extends the instrumentation down into the MSTest engine.
What changed
Conventions and signal quality. Where an OpenTelemetry semantic convention exists it is now used verbatim (
test.case.name,test.case.result.status,test.suite.name,code.function.name,code.file.path,code.line.number,code.stacktrace,error.type), failures set anErrorspan status and record a properexceptionevent, and durations are recorded in seconds with explicit histogram buckets tuned for test latencies. The pre-existing attribute and instrument names are still emitted by default so current dashboards keep working;TESTINGPLATFORM_OTEL_EMIT_LEGACY_ATTRIBUTES=0drops them.Zero-code enablement.
AddOpenTelemetryProviderFromEnvironment()configures instrumentation, resource and an OTLP exporter purely from the standardOTEL_*variables, and installs nothing when no exporter is configured, so it is free to leave inProgram.cs.AddTestingPlatformResource()describes where a run happened, including CI provider, pipeline run, branch and commit detected from GitHub Actions, Azure Pipelines, GitLab CI and Jenkins.Correlation. A run now nests under the
TRACEPARENTpublished by whoever started the process, so test spans join their CI step's trace instead of starting an orphan one.tracestateis carried across too.MSTest. A dependency-free instrumentation seam in the engine (also usable under VSTest, and a single static field read when off) produces spans for
AssemblyInitialize/Cleanup,ClassInitialize/Cleanup,TestInitialize/Cleanupand the test method itself. This is what tells "the test is slow" apart from "the assembly fixture is slow".Retry. Per-attempt spans and a
test.case.retry.countcounter.Worth a careful look
MSTest spans are deliberately non-ambient. MSTest captures the
ExecutionContextinsideAssemblyInitialize/ClassInitializeand replays it for every subsequent test.Activity.Currentis an async-local, so an ambient fixture span was captured into that context and parented the rest of the run, including any span the user's own code created, to an already-stopped fixture span. It also changed what test code observes fromActivity.Current. The spans are therefore timed and exported without ever becoming current, and are parented explicitly. The trade-off is a flatter shape: MSTest spans are siblings under the test-framework span rather than nested. Restoring nesting safely is follow-up work.Package dependency. The turnkey API pulls in
OpenTelemetry.Exporter.OpenTelemetryProtocol. That is what makes "configure by environment variable" possible, but splitting it into a separate exporters package is a reasonable alternative if the closure size matters.Platform extensions vs. conventions. Every emitted name was checked against the upstream registry (semantic conventions 1.43.0). There are no upstream
test.*metrics and no test-case span conventions, andcicd.provider.namedoes not exist. Those names are documented as Microsoft.Testing.Platform extensions rather than presented as standard, and deliberately sit in the namespace where an upstream definition would land.One local mitigation, one separate bug.
OpenTelemetryResultHandler's bookkeeping is guarded so telemetry cannot throw during shutdown. The underlying platform behaviour, that a cancelled run never drains or disables the message bus so anyIDataConsumercan be disposed while still consuming, predates this work and affects TRX, coverage and the report extensions equally. It is tracked separately in #10357 and should get its own PR.Testing
Five review rounds produced 20 findings, all fixed. Around 20 tests were added, including live
ActivityListenertests that cover the ambient-context behaviour mocks cannot see, and tests pinning the MSTest seam as inert when tracing is off. Build is clean and 7,116 tests pass across the platform, extensions, adapter and framework suites; the new concurrency test was run repeatedly to confirm it is not flaky.