// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Extensions.TestFramework; using Microsoft.Testing.Platform.Messages; using Microsoft.Testing.Platform.TestHost; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Helpers; using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestPlatform.ObjectModel; using FrameworkTestResult = Microsoft.VisualStudio.TestTools.UnitTesting.TestResult; namespace Microsoft.VisualStudio.TestTools.UnitTesting; /// /// A Microsoft.Testing.Platform-native that reports test lifecycle and results /// directly as s on the platform message bus, without materializing a VSTest /// TestResult or going through the VSTest bridge. /// [SuppressMessage("ApiDesign", "RS0030:Do not use banned APIs", Justification = "We can use MTP from this folder")] internal sealed class MtpTestResultRecorder : ITestResultRecorder { private readonly IMessageBus _messageBus; private readonly ExecuteRequestContext? _executeRequestContext; private readonly IDataProducer _dataProducer; private readonly SessionUid _sessionUid; private readonly bool _isTrxEnabled; private readonly MSTestSettings _settings; private readonly Action _stageResultFiles; private readonly Dictionary _occurrences = []; #if NET9_0_OR_GREATER private readonly Lock _syncRoot = new(); #else private readonly object _syncRoot = new(); #endif public MtpTestResultRecorder( IMessageBus messageBus, IDataProducer dataProducer, SessionUid sessionUid, bool isTrxEnabled, MSTestSettings settings) : this(messageBus, executeRequestContext: null, dataProducer, sessionUid, isTrxEnabled, settings, static _ => { }) { } public MtpTestResultRecorder( IMessageBus messageBus, IDataProducer dataProducer, SessionUid sessionUid, bool isTrxEnabled, MSTestSettings settings, Action stageResultFiles) : this(messageBus, executeRequestContext: null, dataProducer, sessionUid, isTrxEnabled, settings, stageResultFiles) { } internal MtpTestResultRecorder( ExecuteRequestContext executeRequestContext, IDataProducer dataProducer, SessionUid sessionUid, bool isTrxEnabled, MSTestSettings settings) : this(executeRequestContext.MessageBus, executeRequestContext, dataProducer, sessionUid, isTrxEnabled, settings, static _ => { }) { } internal MtpTestResultRecorder( ExecuteRequestContext executeRequestContext, IDataProducer dataProducer, SessionUid sessionUid, bool isTrxEnabled, MSTestSettings settings, Action stageResultFiles) : this(executeRequestContext.MessageBus, executeRequestContext, dataProducer, sessionUid, isTrxEnabled, settings, stageResultFiles) { } private MtpTestResultRecorder( IMessageBus messageBus, ExecuteRequestContext? executeRequestContext, IDataProducer dataProducer, SessionUid sessionUid, bool isTrxEnabled, MSTestSettings settings, Action stageResultFiles) { _messageBus = messageBus; _executeRequestContext = executeRequestContext; _dataProducer = dataProducer; _sessionUid = sessionUid; _isTrxEnabled = isTrxEnabled; _settings = settings; _stageResultFiles = stageResultFiles; } void ITestResultRecorder.PrepareResults(UnitTestElement testElement, FrameworkTestResult[] results) { lock (_syncRoot) { if (_occurrences.TryGetValue(testElement, out Occurrence? occurrence)) { occurrence.RemainingResults = results.Length; } } } public async Task RecordStartAsync(UnitTestElement testElement) { TestNode testNode = MSTestTestNodeConverter.ToInProgressTestNode(testElement, _isTrxEnabled); if (testElement.SupportsExecutionActivityLease && _executeRequestContext is not null) { Occurrence occurrence; lock (_syncRoot) { if (!_occurrences.TryGetValue(testElement, out occurrence!)) { occurrence = new Occurrence(_executeRequestContext.StartTestExecutionAsync( _dataProducer, new TestNodeUpdateMessage(_sessionUid, testNode))); _occurrences.Add(testElement, occurrence); } } try { TestExecution execution = await occurrence.ExecutionTask.ConfigureAwait(false); testElement.ExecutionActivityLease = occurrence.GetOrCreateLease(execution); return; } catch { lock (_syncRoot) { if (_occurrences.TryGetValue(testElement, out Occurrence? current) && ReferenceEquals(current, occurrence)) { _occurrences.Remove(testElement); } } throw; } } await PublishAsync(testNode).ConfigureAwait(false); } public async Task RecordEmptyResultAsync(UnitTestElement testElement) { TestNode testNode = MSTestTestNodeConverter.ToEmptyResultTestNode(testElement, _isTrxEnabled); Occurrence? occurrence = TakeOccurrence(testElement, isResult: true); if (occurrence is not null) { TestExecution execution = await occurrence.ExecutionTask.ConfigureAwait(false); await execution.CompleteAsync( [new TestNodeUpdateMessage(_sessionUid, testNode)], DateTimeOffset.UtcNow).ConfigureAwait(false); return; } await PublishAsync(testNode).ConfigureAwait(false); } public async Task RecordResultAsync(UnitTestElement testElement, FrameworkTestResult unitTestResult, DateTimeOffset startTime, DateTimeOffset endTime) { var outcome = UnitTestOutcomeHelper.ToTestOutcome(unitTestResult.Outcome, _settings); bool isFailed = outcome == TestOutcome.Failed; Occurrence? occurrence = TakeOccurrence(testElement, isResult: true); TestNodeUpdateMessage? resultMessage = null; // Mirror TestResultRecorderExtensions: a NotFound result is not reported while hot reload is enabled. if (outcome != TestOutcome.NotFound || !RuntimeContext.IsHotReloadEnabled) { _stageResultFiles(unitTestResult); TestNode testNode = MSTestTestNodeConverter.ToResultTestNode(testElement, unitTestResult, startTime, endTime, _isTrxEnabled, _settings); resultMessage = new TestNodeUpdateMessage(_sessionUid, testNode); } if (occurrence is null) { if (resultMessage is not null) { await _messageBus.PublishAsync(_dataProducer, resultMessage).ConfigureAwait(false); } return isFailed; } TestNodeUpdateMessage[]? messagesToComplete = null; lock (_syncRoot) { if (resultMessage is not null) { occurrence.ResultMessages.Add(resultMessage); } if (occurrence.IsFinalResult) { messagesToComplete = [.. occurrence.ResultMessages]; } } if (messagesToComplete is not null) { TestExecution execution = await occurrence.ExecutionTask.ConfigureAwait(false); await execution.CompleteAsync(messagesToComplete, endTime).ConfigureAwait(false); } return isFailed; } private Task PublishAsync(TestNode testNode) => _messageBus.PublishAsync(_dataProducer, new TestNodeUpdateMessage(_sessionUid, testNode)); private Occurrence? TakeOccurrence(UnitTestElement testElement, bool isResult) { lock (_syncRoot) { if (!_occurrences.TryGetValue(testElement, out Occurrence? occurrence)) { return null; } if (isResult) { occurrence.RemainingResults = Math.Max(0, occurrence.RemainingResults - 1); occurrence.IsFinalResult = occurrence.RemainingResults == 0; } if (occurrence.IsFinalResult) { _occurrences.Remove(testElement); testElement.ExecutionActivityLease = null; } return occurrence; } } private sealed class Occurrence(Task executionTask) { private ITestExecutionActivityLease? _lease; public Task ExecutionTask { get; } = executionTask; public List ResultMessages { get; } = []; public int RemainingResults { get; set; } = 1; public bool IsFinalResult { get; set; } public ITestExecutionActivityLease GetOrCreateLease(TestExecution execution) => _lease ??= new MSTestExecutionActivityLease(execution); } private sealed class MSTestExecutionActivityLease(TestExecution execution) : ITestExecutionActivityLease { public Task RunAsync(Func> callback) => execution.RunAsync(callback); } }