Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Testing.Platform.Logging;
using Microsoft.Testing.Platform.Messages;
using Microsoft.Testing.Platform.Services;
using Microsoft.Testing.Platform.Telemetry;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Extensions;
using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Helpers;
Expand Down Expand Up @@ -41,7 +42,7 @@ protected override Task SynchronizedDiscoverTestsAsync(VSTestDiscoverTestExecuti
Debugger.Launch();
}

new MSTestDiscoverer().DiscoverTests(request.AssemblyPaths, request.DiscoveryContext, request.MessageLogger, request.DiscoverySink, _configuration);
new MSTestDiscoverer(new TestSourceHandler(), CreateTelemetrySender()).DiscoverTests(request.AssemblyPaths, request.DiscoveryContext, request.MessageLogger, request.DiscoverySink, _configuration);
return Task.CompletedTask;
}

Expand All @@ -55,7 +56,7 @@ protected override async Task SynchronizedRunTestsAsync(VSTestRunTestExecutionRe
Debugger.Launch();
}

MSTestExecutor testExecutor = new(cancellationToken);
MSTestExecutor testExecutor = new(cancellationToken, CreateTelemetrySender());
await testExecutor.RunTestsAsync(request.AssemblyPaths, request.RunContext, request.FrameworkHandle, _configuration).ConfigureAwait(false);
}

Expand Down Expand Up @@ -103,5 +104,19 @@ private static TestMethodIdentifierProperty GetMethodIdentifierPropertyFromManag
// Or alternatively, does VSTest object model expose the assembly full name somewhere?
return new TestMethodIdentifierProperty(assemblyFullName: string.Empty, @namespace, typeName, methodName, arity, parameterTypes, returnTypeFullName: string.Empty);
}

[SuppressMessage("ApiDesign", "RS0030:Do not use banned APIs", Justification = "We can use MTP from this folder")]
private Func<string, IDictionary<string, object>, Task>? CreateTelemetrySender()
{
ITelemetryInformation telemetryInformation = ServiceProvider.GetTelemetryInformation();
if (!telemetryInformation.IsEnabled)
{
return null;
}

ITelemetryCollector telemetryCollector = ServiceProvider.GetTelemetryCollector();

return (eventName, metrics) => telemetryCollector.LogEventAsync(eventName, metrics, CancellationToken.None);
}
}
#endif
26 changes: 22 additions & 4 deletions src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestDiscoverer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
internal sealed class MSTestDiscoverer : ITestDiscoverer
{
private readonly ITestSourceHandler _testSourceHandler;
private readonly Func<string, IDictionary<string, object>, Task>? _telemetrySender;

public MSTestDiscoverer()
Comment thread
Evangelink marked this conversation as resolved.
: this(new TestSourceHandler())
{
}

internal /* for testing purposes */ MSTestDiscoverer(ITestSourceHandler testSourceHandler)
=> _testSourceHandler = testSourceHandler;
internal /* for testing purposes */ MSTestDiscoverer(ITestSourceHandler testSourceHandler, Func<string, IDictionary<string, object>, Task>? telemetrySender = null)
{
_testSourceHandler = testSourceHandler;
_telemetrySender = telemetrySender;
}

/// <summary>
/// Discovers the tests available from the provided source. Not supported for .xap source.
Expand All @@ -47,9 +51,23 @@ internal void DiscoverTests(IEnumerable<string> sources, IDiscoveryContext disco
Ensure.NotNull(logger);
Ensure.NotNull(discoverySink);

if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext, logger, configuration, _testSourceHandler))
// Initialize telemetry collection if not already set (e.g. first call in the session)
if (!MSTestTelemetryDataCollector.IsTelemetryOptedOut())
{
_ = MSTestTelemetryDataCollector.EnsureInitialized();
}

try
{
if (MSTestDiscovererHelpers.InitializeDiscovery(sources, discoveryContext, logger, configuration, _testSourceHandler))
{
new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, logger, discoverySink, discoveryContext);
}
}
finally
{
new UnitTestDiscoverer(_testSourceHandler).DiscoverTests(sources, logger, discoverySink, discoveryContext);
// Use Task.Run to avoid capturing any SynchronizationContext that could cause deadlocks
Comment thread
Evangelink marked this conversation as resolved.
Outdated
Task.Run(() => MSTestTelemetryDataCollector.SendTelemetryAndResetAsync(_telemetrySender)).GetAwaiter().GetResult();
Comment thread
Evangelink marked this conversation as resolved.
Outdated
}
}
}
37 changes: 34 additions & 3 deletions src/Adapter/MSTest.TestAdapter/VSTestAdapter/MSTestExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter;
internal sealed class MSTestExecutor : ITestExecutor
{
private readonly CancellationToken _cancellationToken;
private readonly Func<string, IDictionary<string, object>, Task>? _telemetrySender;

/// <summary>
/// Token for canceling the test run.
Expand All @@ -35,10 +36,11 @@ public MSTestExecutor()
_cancellationToken = CancellationToken.None;
}

internal MSTestExecutor(CancellationToken cancellationToken)
internal MSTestExecutor(CancellationToken cancellationToken, Func<string, IDictionary<string, object>, Task>? telemetrySender = null)
{
TestExecutionManager = new TestExecutionManager();
_cancellationToken = cancellationToken;
_telemetrySender = telemetrySender;
}

/// <summary>
Expand Down Expand Up @@ -105,12 +107,25 @@ internal async Task RunTestsAsync(IEnumerable<TestCase>? tests, IRunContext? run
Ensure.NotNull(frameworkHandle);
Ensure.NotNullOrEmpty(tests);

// Initialize telemetry collection if not already set
if (!MSTestTelemetryDataCollector.IsTelemetryOptedOut())
{
_ = MSTestTelemetryDataCollector.EnsureInitialized();
}

Comment thread
Evangelink marked this conversation as resolved.
if (!MSTestDiscovererHelpers.InitializeDiscovery(from test in tests select test.Source, runContext, frameworkHandle, configuration, new TestSourceHandler()))
{
return;
}
Comment on lines +126 to 137
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MSTestTelemetryDataCollector is initialized before InitializeDiscovery, but if MSTestDiscovererHelpers.InitializeDiscovery(...) returns false the method returns early and SendTelemetryAsync() is never called. This leaves the static Current collector (and assertion counters) alive across sessions and can cause cross-run contamination/memory growth. Wrap the InitializeDiscovery + execution block in a try/finally (or move initialization after a successful InitializeDiscovery) so telemetry is always reset/drained on every exit path.

This issue also appears on line 169 of the same file.

Copilot uses AI. Check for mistakes.

await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(tests, runContext, frameworkHandle, testRunToken).ConfigureAwait(false)).ConfigureAwait(false);
try
{
await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(tests, runContext, frameworkHandle, testRunToken).ConfigureAwait(false)).ConfigureAwait(false);
}
finally
{
await SendTelemetryAsync().ConfigureAwait(false);
}
}

internal async Task RunTestsAsync(IEnumerable<string>? sources, IRunContext? runContext, IFrameworkHandle? frameworkHandle, IConfiguration? configuration)
Expand All @@ -123,14 +138,27 @@ internal async Task RunTestsAsync(IEnumerable<string>? sources, IRunContext? run
Ensure.NotNull(frameworkHandle);
Ensure.NotNullOrEmpty(sources);

// Initialize telemetry collection if not already set
if (!MSTestTelemetryDataCollector.IsTelemetryOptedOut())
{
_ = MSTestTelemetryDataCollector.EnsureInitialized();
}

TestSourceHandler testSourceHandler = new();
if (!MSTestDiscovererHelpers.InitializeDiscovery(sources, runContext, frameworkHandle, configuration, testSourceHandler))
{
return;
}

sources = testSourceHandler.GetTestSources(sources);
await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, runContext, frameworkHandle, testSourceHandler, testRunToken).ConfigureAwait(false)).ConfigureAwait(false);
try
{
await RunTestsFromRightContextAsync(frameworkHandle, async testRunToken => await TestExecutionManager.RunTestsAsync(sources, runContext, frameworkHandle, testSourceHandler, testRunToken).ConfigureAwait(false)).ConfigureAwait(false);
}
finally
{
await SendTelemetryAsync().ConfigureAwait(false);
}
}

/// <summary>
Expand All @@ -139,6 +167,9 @@ internal async Task RunTestsAsync(IEnumerable<string>? sources, IRunContext? run
public void Cancel()
=> _testRunCancellationToken?.Cancel();

private async Task SendTelemetryAsync()
=> await MSTestTelemetryDataCollector.SendTelemetryAndResetAsync(_telemetrySender).ConfigureAwait(false);

private async Task RunTestsFromRightContextAsync(IFrameworkHandle frameworkHandle, Func<TestRunCancellationToken, Task> runTestsAction)
{
ApartmentState? requestedApartmentState = MSTestSettings.RunConfigurationSettings.ExecutionApartmentState;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ internal virtual TypeEnumerator GetTypeEnumerator(Type type, string assemblyFile
var typeValidator = new TypeValidator(ReflectHelper, discoverInternals);
var testMethodValidator = new TestMethodValidator(ReflectHelper, discoverInternals);

return new TypeEnumerator(type, assemblyFileName, ReflectHelper, typeValidator, testMethodValidator);
return new TypeEnumerator(type, assemblyFileName, ReflectHelper, typeValidator, testMethodValidator, MSTestTelemetryDataCollector.Current);
}

private List<UnitTestElement> DiscoverTestsInType(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ internal class TypeEnumerator
private readonly TypeValidator _typeValidator;
private readonly TestMethodValidator _testMethodValidator;
private readonly ReflectHelper _reflectHelper;
private readonly MSTestTelemetryDataCollector? _telemetryDataCollector;

/// <summary>
/// Initializes a new instance of the <see cref="TypeEnumerator"/> class.
Expand All @@ -28,13 +29,15 @@ internal class TypeEnumerator
/// <param name="reflectHelper"> An instance to reflection helper for type information. </param>
/// <param name="typeValidator"> The validator for test classes. </param>
/// <param name="testMethodValidator"> The validator for test methods. </param>
internal TypeEnumerator(Type type, string assemblyFilePath, ReflectHelper reflectHelper, TypeValidator typeValidator, TestMethodValidator testMethodValidator)
/// <param name="telemetryDataCollector"> Optional telemetry data collector for tracking API usage. </param>
internal TypeEnumerator(Type type, string assemblyFilePath, ReflectHelper reflectHelper, TypeValidator typeValidator, TestMethodValidator testMethodValidator, MSTestTelemetryDataCollector? telemetryDataCollector = null)
{
_type = type;
_assemblyFilePath = assemblyFilePath;
_reflectHelper = reflectHelper;
_typeValidator = typeValidator;
_testMethodValidator = testMethodValidator;
_telemetryDataCollector = telemetryDataCollector;
}

/// <summary>
Expand All @@ -49,6 +52,13 @@ internal TypeEnumerator(Type type, string assemblyFilePath, ReflectHelper reflec
return null;
}

// Track class-level attributes for telemetry
if (_telemetryDataCollector is not null)
{
Attribute[] classAttributes = _reflectHelper.GetCustomAttributesCached(_type);
_telemetryDataCollector.TrackDiscoveredClass(_type, classAttributes);
}

// If test class is valid, then get the tests
return GetTests(warnings);
}
Expand Down Expand Up @@ -143,6 +153,7 @@ internal UnitTestElement GetTestFromMethod(MethodInfo method, bool classDisables
};

Attribute[] attributes = _reflectHelper.GetCustomAttributesCached(method);
_telemetryDataCollector?.TrackDiscoveredMethod(attributes);
TestMethodAttribute? testMethodAttribute = null;

// Backward looping for backcompat. This used to be calls to _reflectHelper.GetFirstAttributeOrDefault
Expand Down
12 changes: 12 additions & 0 deletions src/Adapter/MSTestAdapter.PlatformServices/MSTestSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,18 @@ internal static void PopulateSettings(IDiscoveryContext? context, IMessageLogger

CurrentSettings = settings;
RunConfigurationSettings = runConfigurationSettings;

// Track configuration source for telemetry
#if !WINDOWS_UWP
if (MSTestTelemetryDataCollector.Current is { } telemetry)
{
telemetry.ConfigurationSource = configuration?["mstest"] is not null
? "testconfig.json"
: !StringEx.IsNullOrEmpty(context?.RunSettings?.SettingsXml)
? "runsettings"
: "none";
}
#endif
}

/// <summary>
Expand Down
Loading
Loading