File: Execution\BaseRunTests.cs
Web Access
Project: src\src\vstest\src\Microsoft.TestPlatform.CrossPlatEngine\Microsoft.TestPlatform.CrossPlatEngine.csproj (Microsoft.TestPlatform.CrossPlatEngine)
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

using Microsoft.VisualStudio.TestPlatform.Common.ExtensionDecorators;
using Microsoft.VisualStudio.TestPlatform.Common.ExtensionFramework;
using Microsoft.VisualStudio.TestPlatform.Common.ExtensionFramework.Utilities;
using Microsoft.VisualStudio.TestPlatform.Common.Interfaces;
using Microsoft.VisualStudio.TestPlatform.Common.Telemetry;
using Microsoft.VisualStudio.TestPlatform.Common.Utilities;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities;
using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities.Interfaces;
using Microsoft.VisualStudio.TestPlatform.CoreUtilities.Tracing.Interfaces;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Adapter;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.DataCollection.Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Adapter;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Engine.ClientProtocol;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;

using CrossPlatEngineResources = Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Resources.Resources;

namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Execution;

/// <summary>
/// The base run tests.
/// </summary>
internal abstract class BaseRunTests
{
    private readonly ITestCaseEventsHandler? _testCaseEventsHandler;
    private readonly ITestPlatformEventSource _testPlatformEventSource;

    /// <summary>
    /// To create thread in given apartment state.
    /// </summary>
    private readonly IThread _platformThread;

    /// <summary>
    /// The Run configuration. To determine framework and execution thread apartment state.
    /// </summary>
    private readonly RunConfiguration _runConfiguration;

    /// <summary>
    /// The Serializer to clone testcase object in case of user input test source is package. E.g UWP scenario(appx/build.appxrecipe).
    /// </summary>
    private readonly IDataSerializer _dataSerializer;

    private protected string? _package;
    private readonly IRequestData _requestData;

    /// <summary>
    /// Specifies that the test run cancellation is requested
    /// </summary>
    private volatile bool _isCancellationRequested;

    /// <summary>
    /// Active executor which is executing the tests currently
    /// </summary>
    private ITestExecutor? _activeExecutor;

    /// <summary>
    /// Initializes a new instance of the <see cref="BaseRunTests"/> class.
    /// </summary>
    /// <param name="requestData">The request data for providing common execution services and data</param>
    /// <param name="package">The user input test source(package) if it differs from actual test source otherwise null.</param>
    /// <param name="runSettings">The run settings.</param>
    /// <param name="testExecutionContext">The test execution context.</param>
    /// <param name="testCaseEventsHandler">The test case events handler.</param>
    /// <param name="testRunEventsHandler">The test run events handler.</param>
    /// <param name="testPlatformEventSource">Test platform event source.</param>
    protected BaseRunTests(
        IRequestData requestData,
        string? package,
        string? runSettings,
        TestExecutionContext testExecutionContext,
        ITestCaseEventsHandler? testCaseEventsHandler,
        IInternalTestRunEventsHandler testRunEventsHandler,
        ITestPlatformEventSource testPlatformEventSource)
        : this(
            requestData,
            package,
            runSettings,
            testExecutionContext,
            testCaseEventsHandler,
            testRunEventsHandler,
            testPlatformEventSource,
            testCaseEventsHandler as ITestEventsPublisher,
            new PlatformThread(),
            JsonDataSerializer.Instance)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="BaseRunTests"/> class.
    /// </summary>
    /// <param name="requestData">Provides services and data for execution</param>
    /// <param name="package">The user input test source(package) list if it differs from actual test source otherwise null.</param>
    /// <param name="runSettings">The run settings.</param>
    /// <param name="testExecutionContext">The test execution context.</param>
    /// <param name="testCaseEventsHandler">The test case events handler.</param>
    /// <param name="testRunEventsHandler">The test run events handler.</param>
    /// <param name="testPlatformEventSource">Test platform event source.</param>
    /// <param name="testEventsPublisher">Publisher for test events.</param>
    /// <param name="platformThread">Platform Thread.</param>
    /// <param name="dataSerializer">Data Serializer for cloning TestCase and test results object.</param>
    [SuppressMessage("Style", "IDE0060:Remove unused parameter", Justification = "Part of the public API")]
    protected BaseRunTests(
        IRequestData requestData,
        string? package,
        string? runSettings,
        TestExecutionContext testExecutionContext,
        ITestCaseEventsHandler? testCaseEventsHandler,
        IInternalTestRunEventsHandler testRunEventsHandler,
        ITestPlatformEventSource testPlatformEventSource,
        ITestEventsPublisher? testEventsPublisher,
        IThread platformThread,
        IDataSerializer dataSerializer)
    {
        _package = package;
        RunSettings = runSettings;
        TestExecutionContext = testExecutionContext;
        _testCaseEventsHandler = testCaseEventsHandler;
        TestRunEventsHandler = testRunEventsHandler;
        _requestData = requestData;

        _isCancellationRequested = false;
        _testPlatformEventSource = testPlatformEventSource;
        _platformThread = platformThread;
        _dataSerializer = dataSerializer;

        TestRunCache = new TestRunCache(TestExecutionContext.FrequencyOfRunStatsChangeEvent, TestExecutionContext.RunStatsChangeEventTimeout, OnCacheHit);

        RunContext = new RunContext();
        RunContext.RunSettings = RunSettingsUtilities.CreateAndInitializeRunSettings(RunSettings);
        RunContext.KeepAlive = TestExecutionContext.KeepAlive;
        RunContext.InIsolation = TestExecutionContext.InIsolation;
        RunContext.IsDataCollectionEnabled = TestExecutionContext.IsDataCollectionEnabled;
        RunContext.IsBeingDebugged = TestExecutionContext.IsDebug;

        var runConfig = XmlRunSettingsUtilities.GetRunConfigurationNode(RunSettings);
        RunContext.TestRunDirectory = RunSettingsUtilities.GetTestResultsDirectory(runConfig);
        RunContext.SolutionDirectory = RunSettingsUtilities.GetSolutionDirectory(runConfig);
        _runConfiguration = runConfig;

        FrameworkHandle = new FrameworkHandle(
            _testCaseEventsHandler,
            TestRunCache,
            TestExecutionContext,
            TestRunEventsHandler);
        FrameworkHandle.TestRunMessage += OnTestRunMessage;

        ExecutorUrisThatRanTests = new List<string>();
    }

    /// <summary>
    /// Gets the run settings.
    /// </summary>
    protected string? RunSettings { get; }

    /// <summary>
    /// Gets the test execution context.
    /// </summary>
    protected TestExecutionContext TestExecutionContext { get; }

    /// <summary>
    /// Gets the test run events handler.
    /// </summary>
    protected IInternalTestRunEventsHandler TestRunEventsHandler { get; }

    /// <summary>
    /// Gets the test run cache.
    /// </summary>
    protected ITestRunCache TestRunCache { get; }

    protected bool IsCancellationRequested => _isCancellationRequested;

    protected RunContext RunContext { get; }

    protected FrameworkHandle FrameworkHandle { get; }

    protected ICollection<string> ExecutorUrisThatRanTests { get; }

    public void RunTests()
    {
        using (TestRunCache)
        {
            TimeSpan? elapsedTime = null;
            Exception? exception = null;
            bool isAborted = false;

            try
            {
                // Call Session-Start event on in-proc datacollectors
                SendSessionStart();

                elapsedTime = RunTestsInternal();
                if (elapsedTime is null)
                {
                    EqtTrace.Error("BaseRunTests.RunTests: Failed to run the tests. Reason: GetExecutorUriExtensionMap returned null.");
                    isAborted = true;
                }
            }
            catch (Exception ex)
            {
                EqtTrace.Error("BaseRunTests.RunTests: Failed to run the tests. Reason: {0}.", ex);

                exception = new Exception(ex.Message, ex.InnerException);
                isAborted = true;
            }
            finally
            {
                // Trigger Session End on in-proc datacollectors
                SendSessionEnd();

                try
                {
                    // Send the test run complete event.
                    RaiseTestRunComplete(exception, _isCancellationRequested, isAborted, elapsedTime ?? TimeSpan.Zero);
                }
                catch (Exception ex2)
                {
                    EqtTrace.Error("BaseRunTests.RunTests: Failed to raise runCompletion error. Reason: {0}.", ex2);

                    // TODO : this does not crash the process currently because of the job queue.
                    // Let the process crash
                    throw;
                }
            }
        }

        EqtTrace.Verbose("BaseRunTests.RunTests: Run is complete.");
    }

    internal void Abort()
    {
        EqtTrace.Verbose("BaseRunTests.Abort: Calling RaiseTestRunComplete");
        RaiseTestRunComplete(exception: null, canceled: _isCancellationRequested, aborted: true, elapsedTime: TimeSpan.Zero);
    }

    /// <summary>
    /// Cancel the current run by setting cancellation token for active executor
    /// </summary>
    internal void Cancel()
    {
        // Note: Test host delegates the cancellation to active executor and doesn't call HandleTestRunComplete in cancel request.
        // Its expected from active executor to respect the cancel request and thus return from RunTests quickly (canceling the tests).
        _isCancellationRequested = true;

        if (_activeExecutor == null)
        {
            return;
        }

        if (NotRequiredStaThread() || !TryToRunInStaThread(() => CancelTestRunInternal(_activeExecutor), false))
        {
            Task.Run(() => CancelTestRunInternal(_activeExecutor));
        }
    }

    protected abstract void BeforeRaisingTestRunComplete(bool exceptionsHitDuringRunTests);

    protected abstract IEnumerable<Tuple<Uri, string>>? GetExecutorUriExtensionMap(
        IFrameworkHandle testExecutorFrameworkHandle,
        RunContext runContext);

    protected abstract void InvokeExecutor(
        LazyExtension<ITestExecutor, ITestExecutorCapabilities> executor,
        Tuple<Uri, string> executorUriExtensionTuple,
        RunContext runContext,
        IFrameworkHandle frameworkHandle);

    /// <summary>
    /// Asks the adapter about attaching the debugger to the default test host.
    /// </summary>
    /// <param name="executor">The executor used to run the tests.</param>
    /// <param name="executorUriExtensionTuple">The executor URI.</param>
    /// <param name="runContext">The run context.</param>
    /// <returns>
    /// <see langword="true"/> if must attach the debugger to the default test host,
    /// <see langword="false"/> otherwise.
    /// </returns>
    protected abstract bool ShouldAttachDebuggerToTestHost(
        LazyExtension<ITestExecutor, ITestExecutorCapabilities> executor,
        Tuple<Uri, string> executorUriExtensionTuple,
        RunContext runContext);

    protected abstract void SendSessionStart();

    protected abstract void SendSessionEnd();

    private static void CancelTestRunInternal(ITestExecutor executor)
    {
        try
        {
            executor.Cancel();
        }
        catch (Exception e)
        {
            EqtTrace.Info("{0}.Cancel threw an exception: {1} ", executor.GetType().FullName, e);
        }
    }
    private void OnTestRunMessage(object? sender, TestRunMessageEventArgs e)
    {
        TestRunEventsHandler.HandleLogMessage(e.Level, e.Message);
    }

    private TimeSpan? RunTestsInternal()
    {
        long totalTests = 0;

        // Set on the logger the TreatAdapterErrorAsWarning setting from runsettings.
        SetAdapterLoggingSettings();

        var executorUriExtensionMap = GetExecutorUriExtensionMap(FrameworkHandle, RunContext);

        var stopwatch = new Stopwatch();
        stopwatch.Start();

        _testPlatformEventSource.ExecutionStart();

        if (executorUriExtensionMap is null)
        {
            return null;
        }

        var exceptionsHitDuringRunTests = RunTestInternalWithExecutors(
            executorUriExtensionMap,
            totalTests);

        stopwatch.Stop();
        _testPlatformEventSource.ExecutionStop(TestRunCache.TotalExecutedTests);
        BeforeRaisingTestRunComplete(exceptionsHitDuringRunTests);
        return stopwatch.Elapsed;
    }

    private bool RunTestInternalWithExecutors(IEnumerable<Tuple<Uri, string>> executorUriExtensionMap, long totalTests)
    {
        // Collecting Total Number of Adapters Discovered in Machine.
        var executorUriExtensionMapList = executorUriExtensionMap as IList<Tuple<Uri, string>> ?? executorUriExtensionMap.ToList();
        _requestData.MetricsCollection.Add(TelemetryDataConstants.NumberOfAdapterDiscoveredDuringExecution, executorUriExtensionMapList.Count);

        var attachedToTestHost = false;
        var executorCache = new Dictionary<string, LazyExtension<ITestExecutor, ITestExecutorCapabilities>>();
        foreach (var executorUriExtensionTuple in executorUriExtensionMapList)
        {
            // Avoid processing the same executor twice.
            if (executorCache.ContainsKey(executorUriExtensionTuple.Item1.AbsoluteUri))
            {
                continue;
            }

            // Get the extension manager.
            var extensionManager = GetExecutorExtensionManager(executorUriExtensionTuple.Item2);

            // Look up the executor.
            var executor = extensionManager?.TryGetTestExtension(executorUriExtensionTuple.Item1);
            if (executor == null)
            {
                // Commenting this out because of a compatibility issue with Microsoft.Dotnet.ProjectModel released on nuGet.org.
                // this.activeExecutor = null;
                // var runtimeVersion = string.Concat(PlatformServices.Default.Runtime.RuntimeType, " ",
                // PlatformServices.Default.Runtime.RuntimeVersion);
                var runtimeVersion = " ";
                TestRunEventsHandler?.HandleLogMessage(
                    TestMessageLevel.Warning,
                    string.Format(
                        CultureInfo.CurrentCulture,
                        CrossPlatEngineResources.NoMatchingExecutor,
                        executorUriExtensionTuple.Item1.AbsoluteUri,
                        runtimeVersion));

                continue;
            }

            // Cache the executor.
            executorCache.Add(executorUriExtensionTuple.Item1.AbsoluteUri, executor);

            // Check if we actually have to attach to the default test host.
            if (!RunContext.IsBeingDebugged || attachedToTestHost)
            {
                // We already know we should attach to the default test host, simply continue.
                continue;
            }

            // If there's at least one adapter in the filtered adapters list that doesn't
            // implement the new test executor interface, we should attach to the default test
            // host by default.
            // Same goes if all adapters implement the new test executor interface but at
            // least one of them needs the test platform to attach to the default test host.
            if (executor.Value is not ITestExecutor2
                || ShouldAttachDebuggerToTestHost(executor, executorUriExtensionTuple, RunContext))
            {
                EqtTrace.Verbose("Attaching to default test host.");

                attachedToTestHost = true;
#if NET
                var pid = Environment.ProcessId;
#else
                var pid = Process.GetCurrentProcess().Id;
#endif
                if (!FrameworkHandle.AttachDebuggerToProcess(pid))
                {
                    EqtTrace.Warning(string.Format(CultureInfo.CurrentCulture, CrossPlatEngineResources.AttachDebuggerToDefaultTestHostFailure, pid));
                }
            }
        }


        // Call the executor for each group of tests.
        var exceptionsHitDuringRunTests = false;
        var executorsFromDeprecatedLocations = false;
        double totalTimeTakenByAdapters = 0;

        foreach (var executorUriExtensionTuple in executorUriExtensionMapList)
        {
            var executorUri = executorUriExtensionTuple.Item1.AbsoluteUri;
            // Get the executor from the cache.
            if (!executorCache.TryGetValue(executorUri, out var executor))
            {
                continue;
            }

            try
            {
                EqtTrace.Verbose(
                    "BaseRunTests.RunTestInternalWithExecutors: Running tests for {0}",
                    executor.Metadata.ExtensionUri);

                // set the active executor
                _activeExecutor = executor.Value;

                // If test run cancellation is requested, skip the next executor
                if (_isCancellationRequested)
                {
                    break;
                }

                var timeStartNow = DateTime.UtcNow;

                var currentTotalTests = TestRunCache.TotalExecutedTests;
                _testPlatformEventSource.AdapterExecutionStart(executorUri);

                // Run the tests.
                if (NotRequiredStaThread() || !TryToRunInStaThread(() => InvokeExecutor(executor, executorUriExtensionTuple, RunContext, FrameworkHandle), true))
                {
                    InvokeExecutor(executor, executorUriExtensionTuple, RunContext, FrameworkHandle);
                }

                _testPlatformEventSource.AdapterExecutionStop(TestRunCache.TotalExecutedTests - currentTotalTests);

                var totalTimeTaken = DateTime.UtcNow - timeStartNow;

                // Identify whether the executor did run any tests at all
                if (TestRunCache.TotalExecutedTests > totalTests)
                {
                    ExecutorUrisThatRanTests.Add(executorUri);

                    // Collecting Total Tests Ran by each Adapter
                    var totalTestRun = TestRunCache.TotalExecutedTests - totalTests;
                    _requestData.MetricsCollection.Add($"{TelemetryDataConstants.TotalTestsRanByAdapter}.{executorUri}", totalTestRun);

                    // Only enable this for MSTestV1 telemetry for now, this might become more generic later.
                    if (MsTestV1TelemetryHelper.IsMsTestV1Adapter(executorUri))
                    {
                        foreach (var adapterMetrics in TestRunCache.AdapterTelemetry.Keys.Where(k => k.StartsWith(executorUri)))
                        {
                            var value = TestRunCache.AdapterTelemetry[adapterMetrics];

                            _requestData.MetricsCollection.Add($"{TelemetryDataConstants.TotalTestsRunByMSTestv1}.{adapterMetrics}", value);
                        }
                    }

                    if (!CrossPlatEngine.Constants.DefaultAdapters.Contains(executor.Metadata.ExtensionUri, StringComparer.OrdinalIgnoreCase))
                    {
                        // If real executor is wrapped by a decorator we get the real decorated type
                        Type executorType =
                            (executor.Value is SerialTestRunDecorator serialTestRunDecorator)
                                ? serialTestRunDecorator.OriginalTestExecutor.GetType()
                                : executor.Value.GetType();

                        var executorLocation = executorType.Assembly.GetAssemblyLocation();
                        executorsFromDeprecatedLocations |= Path.GetDirectoryName(executorLocation)!.Equals(CrossPlatEngine.Constants.DefaultAdapterLocation);
                    }

                    totalTests = TestRunCache.TotalExecutedTests;
                }

                EqtTrace.Verbose(
                    "BaseRunTests.RunTestInternalWithExecutors: Completed running tests for {0}",
                    executor.Metadata.ExtensionUri);

                // Collecting Time Taken by each executor Uri
                _requestData.MetricsCollection.Add($"{TelemetryDataConstants.TimeTakenToRunTestsByAnAdapter}.{executorUri}", totalTimeTaken.TotalSeconds);
                totalTimeTakenByAdapters += totalTimeTaken.TotalSeconds;
            }
            catch (Exception e)
            {
                string exceptionMessage = (e is UnauthorizedAccessException)
                    ? string.Format(CultureInfo.CurrentCulture, CrossPlatEngineResources.AccessDenied, e.Message)
                    : ExceptionUtilities.GetExceptionMessage(e);

                exceptionsHitDuringRunTests = true;

                EqtTrace.Error(
                    "BaseRunTests.RunTestInternalWithExecutors: An exception occurred while invoking executor {0}. {1}.",
                    executorUriExtensionTuple.Item1,
                    e);

                TestRunEventsHandler?.HandleLogMessage(
                    TestMessageLevel.Error,
                    string.Format(
                        CultureInfo.CurrentCulture,
                        CrossPlatEngineResources.ExceptionFromRunTests,
                        executorUriExtensionTuple.Item1,
                        exceptionMessage));
            }
            finally
            {
                _activeExecutor = null;
            }
        }

        // Collecting Total Time Taken by Adapters
        _requestData.MetricsCollection.Add(TelemetryDataConstants.TimeTakenByAllAdaptersInSec, totalTimeTakenByAdapters);

        if (executorsFromDeprecatedLocations)
        {
            TestRunEventsHandler?.HandleLogMessage(TestMessageLevel.Warning, string.Format(CultureInfo.CurrentCulture, CrossPlatEngineResources.DeprecatedAdapterPath));
        }

        return exceptionsHitDuringRunTests;
    }

    private bool NotRequiredStaThread()
    {
        return _runConfiguration.ExecutionThreadApartmentState != PlatformApartmentState.STA;
    }

    private static TestExecutorExtensionManager? GetExecutorExtensionManager(string extensionAssembly)
    {
        try
        {
            if (StringUtils.IsNullOrEmpty(extensionAssembly)
                || string.Equals(extensionAssembly, ObjectModel.Constants.UnspecifiedAdapterPath))
            {
                // full execution. Since the extension manager is cached this can be created multiple times without harming performance.
                return TestExecutorExtensionManager.Create();
            }
            else
            {
                return TestExecutorExtensionManager.GetExecutionExtensionManager(extensionAssembly);
            }
        }
        catch (Exception ex)
        {
            EqtTrace.Error(
                "BaseRunTests: GetExecutorExtensionManager: Exception occurred while loading extensions {0}",
                ex);

            return null;
        }
    }

    private static void SetAdapterLoggingSettings()
    {
        // TODO: enable the below once runsettings is in.
        // var sessionMessageLogger = testExecutorFrameworkHandle as TestSessionMessageLogger;
        // if (sessionMessageLogger != null
        //        && testExecutionContext != null
        //        && testExecutionContext.TestRunConfiguration != null)
        // {
        //    sessionMessageLogger.TreatTestAdapterErrorsAsWarnings
        //        = testExecutionContext.TestRunConfiguration.TreatTestAdapterErrorsAsWarnings;
        // }
    }

    private void RaiseTestRunComplete(
        Exception? exception,
        bool canceled,
        bool aborted,
        TimeSpan elapsedTime)
    {
        var runStats = TestRunCache?.TestRunStatistics ?? new TestRunStatistics(new Dictionary<TestOutcome, long>());
        var lastChunkTestResults = TestRunCache?.GetLastChunk() ?? new List<TestResult>();

        if (TestRunEventsHandler != null)
        {
            // Collecting Total Tests Run
            _requestData.MetricsCollection.Add(TelemetryDataConstants.TotalTestsRun, runStats.ExecutedTests);

            // Collecting Test Run State
            _requestData.MetricsCollection.Add(TelemetryDataConstants.RunState, canceled ? "Canceled" : (aborted ? "Aborted" : "Completed"));

            // Collecting Number of Adapters Used to run tests.
            _requestData.MetricsCollection.Add(TelemetryDataConstants.NumberOfAdapterUsedToRunTests, ExecutorUrisThatRanTests.Count);

            if (lastChunkTestResults.Count != 0 && IsTestSourceIsPackage())
            {
                UpdateTestCaseSourceToPackage(lastChunkTestResults, null, out lastChunkTestResults, out _);
            }

            var testRunChangedEventArgs = new TestRunChangedEventArgs(runStats, lastChunkTestResults, []);

            // Adding Metrics along with Test Run Complete Event Args
            Collection<AttachmentSet>? attachments = FrameworkHandle?.Attachments;
            var testRunCompleteEventArgs = new TestRunCompleteEventArgs(
                runStats,
                canceled,
                aborted,
                exception,
                attachments,
                // Today we don't offer an extension to run collectors for test adapters.
                new Collection<InvokedDataCollector>(),
                elapsedTime);

            testRunCompleteEventArgs.DiscoveredExtensions = TestPluginCache.Instance.TestExtensions?.GetCachedExtensions();
            testRunCompleteEventArgs.Metrics = _requestData.MetricsCollection.Metrics;

            TestRunEventsHandler.HandleTestRunComplete(
                testRunCompleteEventArgs,
                testRunChangedEventArgs,
                attachments,
                ExecutorUrisThatRanTests);
        }
        else
        {
            EqtTrace.Warning("Could not pass run completion as the callback is null. Aborted :{0}", aborted);
        }
    }

    private bool IsTestSourceIsPackage()
    {
        return !StringUtils.IsNullOrEmpty(_package);
    }

    private void OnCacheHit(TestRunStatistics testRunStats, ICollection<TestResult> results, ICollection<TestCase>? inProgressTestCases)
    {
        if (TestRunEventsHandler != null)
        {
            if (IsTestSourceIsPackage())
            {
                UpdateTestCaseSourceToPackage(results, inProgressTestCases, out results, out inProgressTestCases);
            }

            var testRunChangedEventArgs = new TestRunChangedEventArgs(testRunStats, results, inProgressTestCases);
            TestRunEventsHandler.HandleTestRunStatsChange(testRunChangedEventArgs);
        }
        else
        {
            EqtTrace.Error("BaseRunTests.OnCacheHit: Unable to send TestRunStatsChange Event as TestRunEventsHandler is NULL");
        }
    }

    private bool TryToRunInStaThread(Action action, bool waitForCompletion)
    {
        bool success = true;
        try
        {
            EqtTrace.Verbose("BaseRunTests.TryToRunInSTAThread: Using STA thread to call adapter API.");
            _platformThread.Run(action, PlatformApartmentState.STA, waitForCompletion);
        }
        catch (ThreadApartmentStateNotSupportedException ex)
        {
            success = false;
            EqtTrace.Warning("BaseRunTests.TryToRunInSTAThread: Failed to run in STA thread: {0}", ex);
            TestRunEventsHandler.HandleLogMessage(
                TestMessageLevel.Warning,
                string.Format(CultureInfo.CurrentCulture, CrossPlatEngineResources.ExecutionThreadApartmentStateNotSupportedForFramework, _runConfiguration.TargetFramework!.ToString()));
        }

        return success;
    }

    private void UpdateTestCaseSourceToPackage(
        ICollection<TestResult> testResults,
        ICollection<TestCase>? inProgressTestCases,
        out ICollection<TestResult> updatedTestResults,
        out ICollection<TestCase>? updatedInProgressTestCases)
    {
        EqtTrace.Verbose("BaseRunTests.UpdateTestCaseSourceToPackage: Update source details for testResults and testCases.");

        updatedTestResults = UpdateTestResults(testResults, _package);
        updatedInProgressTestCases = UpdateInProgressTests(inProgressTestCases, _package);
    }

    private ICollection<TestResult> UpdateTestResults(ICollection<TestResult> testResults, string? package)
    {
        ICollection<TestResult> updatedTestResults = new List<TestResult>();

        foreach (var testResult in testResults)
        {
            var updatedTestResult = _dataSerializer.Clone(testResult);
            TPDebug.Assert(updatedTestResult is not null, "updatedTestResult is null");
            updatedTestResult.TestCase.Source = package!;
            updatedTestResults.Add(updatedTestResult);
        }

        return updatedTestResults;
    }

    private ICollection<TestCase>? UpdateInProgressTests(ICollection<TestCase>? inProgressTestCases, string? package)
    {
        if (inProgressTestCases == null)
        {
            return null;
        }

        ICollection<TestCase> updatedTestCases = new List<TestCase>();
        foreach (var inProgressTestCase in inProgressTestCases)
        {
            var updatedTestCase = _dataSerializer.Clone(inProgressTestCase);
            TPDebug.Assert(updatedTestCase is not null, "updatedTestCase is null");
            updatedTestCase.Source = package!;
            updatedTestCases.Add(updatedTestCase);
        }

        return updatedTestCases;
    }

}