File: TrxLogger.cs
Web Access
Project: src\src\vstest\src\Microsoft.TestPlatform.Extensions.TrxLogger\Microsoft.TestPlatform.Extensions.TrxLogger.csproj (Microsoft.VisualStudio.TestPlatform.Extensions.Trx.TestLogger)
// 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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Xml;

using Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel;
using Microsoft.TestPlatform.Extensions.TrxLogger.Utility;
using Microsoft.TestPlatform.Extensions.TrxLogger.XML;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
using Microsoft.VisualStudio.TestPlatform.Utilities;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;

using ObjectModelConstants = Microsoft.VisualStudio.TestPlatform.ObjectModel.Constants;
using TrxLoggerConstants = Microsoft.TestPlatform.Extensions.TrxLogger.Utility.Constants;
using TrxLoggerObjectModel = Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel;
using TrxLoggerResources = Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger.Resources.TrxResource;

namespace Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger;

/// <summary>
/// Logger for Generating TRX
/// </summary>
[FriendlyName(TrxLoggerConstants.FriendlyName)]
[ExtensionUri(TrxLoggerConstants.ExtensionUri)]
public class TrxLogger : ITestLoggerWithParameters
{
    /// <summary>
    /// Initializes a new instance of the <see cref="TrxLogger"/> class.
    /// </summary>
    public TrxLogger() : this(new Utilities.Helpers.FileHelper()) { }

    /// <summary>
    /// Initializes a new instance of the <see cref="TrxLogger"/> class.
    /// Constructor with Dependency injection. Used for unit testing.
    /// </summary>
    /// <param name="fileHelper">The file helper interface.</param>
    protected TrxLogger(IFileHelper fileHelper) : this(fileHelper, new TrxFileHelper()) { }

    internal TrxLogger(IFileHelper fileHelper, TrxFileHelper trxFileHelper)
    {
        _converter = new Converter(fileHelper, trxFileHelper);
        _trxFileHelper = trxFileHelper;
    }

    /// <summary>
    /// Cache the TRX file path
    /// </summary>
    private string? _trxFilePath;

    // The converter class
    private readonly Converter _converter;
    private ConcurrentDictionary<Guid, ITestResult>? _results;
    private ConcurrentDictionary<Guid, ITestElement>? _testElements;
    private ConcurrentDictionary<Guid, TestEntry>? _entries;

    // Caching results and inner test entries for constant time lookup for inner parents.
    private ConcurrentDictionary<Guid, ITestResult>? _innerResults;
    private ConcurrentDictionary<Guid, TestEntry>? _innerTestEntries;

    private readonly TrxFileHelper _trxFileHelper;

    /// <summary>
    /// Specifies the run level "out" messages
    /// </summary>
    private StringBuilder? _runLevelStdOut;

    // List of run level errors and warnings generated. These are logged in the Trx in the Results Summary.
    private List<RunInfo>? _runLevelErrorsAndWarnings;
    private readonly string _trxFileExtension = ".trx";

    /// <summary>
    /// Parameters dictionary for logger. Ex: {"LogFileName":"TestResults.trx"}.
    /// </summary>
    private Dictionary<string, string?>? _parametersDictionary;

    /// <summary>
    /// Gets the directory under which default trx file and test results attachments should be saved.
    /// </summary>
    private string? _testResultsDirPath;
    private bool _warnOnFileOverwrite;


    #region ITestLogger

    [MemberNotNullWhen(true, nameof(_testResultsDirPath), nameof(_results), nameof(_innerResults), nameof(_testElements), nameof(_entries), nameof(_innerTestEntries), nameof(_runLevelErrorsAndWarnings), nameof(_runLevelStdOut))]
    private bool IsInitialized { get; set; }

    /// <inheritdoc/>
    [MemberNotNull(nameof(_testResultsDirPath), nameof(_results), nameof(_innerResults), nameof(_testElements), nameof(_entries), nameof(_innerTestEntries), nameof(_runLevelErrorsAndWarnings), nameof(_runLevelStdOut))]
    public void Initialize(TestLoggerEvents events, string testResultsDirPath)
    {
        ValidateArg.NotNull(events, nameof(events));
        ValidateArg.NotNullOrEmpty(testResultsDirPath, nameof(testResultsDirPath));

        // Register for the events.
        events.TestRunMessage += TestMessageHandler;
        events.TestResult += TestResultHandler;
        events.TestRunComplete += TestRunCompleteHandler;

        _testResultsDirPath = testResultsDirPath;
        _results = new ConcurrentDictionary<Guid, ITestResult>();
        _innerResults = new ConcurrentDictionary<Guid, ITestResult>();
        _testElements = new ConcurrentDictionary<Guid, ITestElement>();
        _entries = new ConcurrentDictionary<Guid, TestEntry>();
        _innerTestEntries = new ConcurrentDictionary<Guid, TestEntry>();
        _runLevelErrorsAndWarnings = new List<RunInfo>();
        LoggerTestRun = null;
        TotalTestCount = 0;
        PassedTestCount = 0;
        FailedTestCount = 0;
        _runLevelStdOut = new StringBuilder();
        TestRunStartTime = DateTime.UtcNow;

        IsInitialized = true;
    }

    /// <inheritdoc/>
    [MemberNotNull(nameof(_parametersDictionary))]
    public void Initialize(TestLoggerEvents events, Dictionary<string, string?> parameters)
    {
        ValidateArg.NotNull(parameters, nameof(parameters));
        if (parameters.Count == 0)
        {
            throw new ArgumentException("No default parameters added", nameof(parameters));
        }

        var isLogFilePrefixParameterExists = parameters.TryGetValue(TrxLoggerConstants.LogFilePrefixKey, out _);
        var isLogFileNameParameterExists = parameters.TryGetValue(TrxLoggerConstants.LogFileNameKey, out _);
        _warnOnFileOverwrite = parameters.TryGetValue(TrxLoggerConstants.WarnOnFileOverwrite, out string? warnOnOverwriteString)
            ? bool.TryParse(warnOnOverwriteString, out bool providedValue)
                ? providedValue
                // We found the option but could not parse the value.
                : true
            // We did not find the option and want to fallback to warning on write, because that was the default before.
            : true;

        if (isLogFilePrefixParameterExists && isLogFileNameParameterExists)
        {
            var trxParameterErrorMsg = TrxLoggerResources.PrefixAndNameProvidedError;

            EqtTrace.Error(trxParameterErrorMsg);
            throw new ArgumentException(trxParameterErrorMsg);
        }

        _parametersDictionary = parameters;
        Initialize(events, _parametersDictionary[DefaultLoggerParameterNames.TestRunDirectory]!);
    }
    #endregion

    #region ForTesting

    internal string GetRunLevelInformationalMessage()
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        return _runLevelStdOut.ToString();
    }

    internal List<RunInfo> GetRunLevelErrorsAndWarnings()
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        return _runLevelErrorsAndWarnings;
    }

    internal DateTime TestRunStartTime { get; private set; }

    internal TestRun? LoggerTestRun { get; private set; }

    private int _totalTestCount;
    private int _passedTestCount;
    private int _failedTestCount;

    internal int TotalTestCount { get => _totalTestCount; private set => _totalTestCount = value; }

    internal int PassedTestCount { get => _passedTestCount; private set => _passedTestCount = value; }

    internal int FailedTestCount { get => _failedTestCount; private set => _failedTestCount = value; }

    internal int TestResultCount
    {
        get
        {
            TPDebug.Assert(IsInitialized, "Logger is not initialized");
            return _results.Count;
        }
    }

    internal int UnitTestElementCount
    {
        get
        {
            TPDebug.Assert(IsInitialized, "Logger is not initialized");
            return _testElements.Count;
        }
    }

    internal int TestEntryCount
    {
        get
        {
            TPDebug.Assert(IsInitialized, "Logger is not initialized");
            return _entries.Count;
        }
    }

    internal TrxLoggerObjectModel.TestOutcome TestResultOutcome { get; private set; } = TrxLoggerObjectModel.TestOutcome.Passed;

    #endregion

    #region Event Handlers

    /// <summary>
    /// Called when a test message is received.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="e">
    /// Event args
    /// </param>
    internal void TestMessageHandler(object? sender, TestRunMessageEventArgs e)
    {
        ValidateArg.NotNull(sender, nameof(sender));
        ValidateArg.NotNull(e, nameof(e));
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        RunInfo runMessage;

        switch (e.Level)
        {
            case TestMessageLevel.Informational:
                AddRunLevelInformationalMessage(e.Message);
                break;
            case TestMessageLevel.Warning:
                runMessage = new RunInfo(e.Message, null, Environment.MachineName, TrxLoggerObjectModel.TestOutcome.Warning);
                _runLevelErrorsAndWarnings.Add(runMessage);
                break;
            case TestMessageLevel.Error:
                TestResultOutcome = TrxLoggerObjectModel.TestOutcome.Failed;
                runMessage = new RunInfo(e.Message, null, Environment.MachineName, TrxLoggerObjectModel.TestOutcome.Error);
                _runLevelErrorsAndWarnings.Add(runMessage);
                break;
            default:
                Debug.Fail("TrxLogger.TestMessageHandler: The test message level is unrecognized: {0}", e.Level.ToString());
                break;
        }
    }

    /// <summary>
    /// Called when a test result is received.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="e">
    /// The eventArgs.
    /// </param>
    internal void TestResultHandler(object? sender, TestResultEventArgs e)
    {
        // Create test run
        if (LoggerTestRun == null)
            CreateTestRun();

        // Convert skipped test to a log entry as that is the behavior of mstest.
        if (e.Result.Outcome == ObjectModel.TestOutcome.Skipped)
            HandleSkippedTest(e.Result);

        var testType = Converter.GetTestType(e.Result);
        var executionId = Converter.GetExecutionId(e.Result);

        // Setting parent properties like parent result, parent test element, parent execution id.
        var parentExecutionId = _converter.GetParentExecutionId(e.Result);
        var parentTestResult = GetTestResult(parentExecutionId);
        var parentTestElement = parentTestResult != null ? GetTestElement(parentTestResult.Id.TestId) : null;

        // Switch to flat test results in case any parent related information is missing.
        if (parentTestResult == null || parentTestElement == null || parentExecutionId == Guid.Empty)
        {
            parentTestResult = null;
            parentTestElement = null;
            parentExecutionId = Guid.Empty;
        }

        // Create trx test element from rocksteady test case
        var testElement = GetOrCreateTestElement(executionId, parentExecutionId, testType, parentTestElement, e.Result);

        // Update test links. Test Links are updated in case of Ordered test.
        UpdateTestLinks(testElement, parentTestElement);

        // Convert the rocksteady result to trx test result
        var testResult = CreateTestResult(executionId, parentExecutionId, testType, testElement, parentTestElement, parentTestResult, e.Result);

        // Update test entries
        UpdateTestEntries(executionId, parentExecutionId, testElement, parentTestElement);

        // Set various counts (passed tests, failed tests, total tests)
        Interlocked.Increment(ref _totalTestCount);
        if (testResult.Outcome == TrxLoggerObjectModel.TestOutcome.Failed)
        {
            TestResultOutcome = TrxLoggerObjectModel.TestOutcome.Failed;
            Interlocked.Increment(ref _failedTestCount);
        }
        else if (testResult.Outcome == TrxLoggerObjectModel.TestOutcome.Passed)
        {
            Interlocked.Increment(ref _passedTestCount);
        }
    }

    /// <summary>
    /// Called when a test run is completed.
    /// </summary>
    /// <param name="sender">
    /// The sender.
    /// </param>
    /// <param name="e">
    /// Test run complete events arguments.
    /// </param>
    internal void TestRunCompleteHandler(object? sender, TestRunCompleteEventArgs e)
    {
        // Create test run
        // If abort occurs there is no call to TestResultHandler which results in testRun not created.
        // This happens when some test aborts in the first batch of execution.
        if (LoggerTestRun == null)
            CreateTestRun();

        TPDebug.Assert(IsInitialized, "Logger is not initialized");

        XmlPersistence helper = new();
        XmlTestStoreParameters parameters = XmlTestStoreParameters.GetParameters();
        XmlElement rootElement = helper.CreateRootElement("TestRun");

        // Save runId/username/creation time etc.
        LoggerTestRun.Finished = DateTime.UtcNow;
        helper.SaveSingleFields(rootElement, LoggerTestRun, parameters);

        // Save test settings
        helper.SaveObject(LoggerTestRun.RunConfiguration, rootElement, "TestSettings", parameters);

        // Save test results
        helper.SaveIEnumerable(_results.Values, rootElement, "Results", ".", null, parameters);

        // Save test definitions
        helper.SaveIEnumerable(_testElements.Values, rootElement, "TestDefinitions", ".", null, parameters);

        // Save test entries
        helper.SaveIEnumerable(_entries.Values, rootElement, "TestEntries", ".", "TestEntry", parameters);

        // Save default categories
        List<TestListCategory> categories =
        [
            TestListCategory.UncategorizedResults,
            TestListCategory.AllResults
        ];
        helper.SaveList(categories, rootElement, "TestLists", ".", "TestList", parameters);

        // Save summary
        if (TestResultOutcome == TrxLoggerObjectModel.TestOutcome.Passed)
        {
            TestResultOutcome = TrxLoggerObjectModel.TestOutcome.Completed;
        }

        TestResultOutcome = ChangeTestOutcomeIfNecessary(TestResultOutcome);

        List<string> errorMessages = [];
        List<CollectorDataEntry> collectorEntries = _converter.ToCollectionEntries(e.AttachmentSets, LoggerTestRun, _testResultsDirPath);
        IList<string> resultFiles = _converter.ToResultFiles(e.AttachmentSets, LoggerTestRun, _testResultsDirPath, errorMessages);

        if (errorMessages.Count > 0)
        {
            // Got some errors while attaching files, report them and set the outcome of testrun to be Error...
            TestResultOutcome = TrxLoggerObjectModel.TestOutcome.Error;
            foreach (string msg in errorMessages)
            {
                RunInfo runMessage = new(msg, null, Environment.MachineName, TrxLoggerObjectModel.TestOutcome.Error);
                _runLevelErrorsAndWarnings.Add(runMessage);
            }
        }

        TestRunSummary runSummary = new(
            TotalTestCount,
            PassedTestCount + FailedTestCount,
            PassedTestCount,
            FailedTestCount,
            TestResultOutcome,
            _runLevelErrorsAndWarnings,
            _runLevelStdOut.ToString(),
            resultFiles,
            collectorEntries);

        helper.SaveObject(runSummary, rootElement, "ResultSummary", parameters);

        ReserveTrxFilePath();
        PopulateTrxFile(_trxFilePath!, rootElement);
    }

    /// <summary>
    /// populate trx file from the xml element
    /// </summary>
    /// <param name="trxFileName">
    /// Trx full path
    /// </param>
    /// <param name="rootElement">
    /// XmlElement.
    /// </param>
    internal virtual void PopulateTrxFile(string trxFileName, XmlElement rootElement)
    {
        try
        {
            using (var fs = File.Open(trxFileName, FileMode.Truncate))
            {
                using XmlWriter writer = XmlWriter.Create(fs, new XmlWriterSettings { NewLineHandling = NewLineHandling.Entitize, Indent = true });
                rootElement.OwnerDocument.Save(writer);
                writer.Flush();
            }

            string resultsFileMessage = string.Format(CultureInfo.CurrentCulture, TrxLoggerResources.TrxLoggerResultsFile, trxFileName);
            ConsoleOutput.Instance.Information(false, resultsFileMessage);
            EqtTrace.Info(resultsFileMessage);
        }
        catch (UnauthorizedAccessException fileWriteException)
        {
            ConsoleOutput.Instance.Error(false, fileWriteException.Message);
        }
    }

    /// <summary>
    /// Add run level informational message
    /// </summary>
    /// <param name="message">
    /// The message.
    /// </param>
    private void AddRunLevelInformationalMessage(string message)
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        _runLevelStdOut.AppendLine(message);
    }

    // Handle the skipped test result
    private void HandleSkippedTest(ObjectModel.TestResult rsTestResult)
    {
        TPDebug.Assert(rsTestResult.Outcome == ObjectModel.TestOutcome.Skipped, "Test Result should be skipped but it is " + rsTestResult.Outcome);

        TestCase testCase = rsTestResult.TestCase;
        string testCaseName = !string.IsNullOrEmpty(testCase.DisplayName) ? testCase.DisplayName : testCase.FullyQualifiedName;
        string message = string.Format(CultureInfo.CurrentCulture, TrxLoggerResources.MessageForSkippedTests, testCaseName);
        AddRunLevelInformationalMessage(message);
    }

    private void ReserveTrxFilePath()
    {
        for (short retries = 0; retries != short.MaxValue; retries++)
        {
            var filePath = AcquireTrxFileNamePath(out var shouldOverwrite);

            if (shouldOverwrite && File.Exists(filePath))
            {
                if (_warnOnFileOverwrite)
                {
                    var overwriteWarningMsg = string.Format(CultureInfo.CurrentCulture, TrxLoggerResources.TrxLoggerResultsFileOverwriteWarning, filePath);
                    ConsoleOutput.Instance.Warning(false, overwriteWarningMsg);
                    EqtTrace.Warning(overwriteWarningMsg);
                }
            }
            else
            {
                try
                {
                    using var fs = File.Open(filePath, FileMode.CreateNew);
                }
                catch (IOException)
                {
                    // File already exists, try again!
                    continue;
                }
            }

            _trxFilePath = filePath;
            return;
        }
    }

    private string AcquireTrxFileNamePath(out bool shouldOverwrite)
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");

        shouldOverwrite = false;
        string? filePath = null;

        if (_parametersDictionary is not null)
        {
            var isLogFileNameParameterExists = _parametersDictionary.TryGetValue(TrxLoggerConstants.LogFileNameKey, out string? logFileNameValue) && !logFileNameValue.IsNullOrWhiteSpace();
            var isLogFilePrefixParameterExists = _parametersDictionary.TryGetValue(TrxLoggerConstants.LogFilePrefixKey, out string? logFilePrefixValue) && !logFilePrefixValue.IsNullOrWhiteSpace();
            if (isLogFilePrefixParameterExists)
            {
                if (_parametersDictionary.TryGetValue(DefaultLoggerParameterNames.TargetFramework, out var framework) && framework != null)
                {
                    framework = Framework.FromString(framework)?.ShortName ?? framework;
                    logFilePrefixValue = logFilePrefixValue + "_" + framework;
                }

                filePath = _trxFileHelper.GetNextTimestampFileName(_testResultsDirPath, logFilePrefixValue + _trxFileExtension, "_yyyyMMddHHmmss");
            }
            else if (isLogFileNameParameterExists)
            {
                filePath = Path.Combine(_testResultsDirPath, logFileNameValue!);
                shouldOverwrite = true;
            }
        }

        filePath ??= SetDefaultTrxFilePath();

        var trxFileDirPath = Path.GetDirectoryName(filePath);

        if (!Directory.Exists(trxFileDirPath))
        {
            Directory.CreateDirectory(trxFileDirPath!);
        }

        return filePath;
    }

    /// <summary>
    /// Returns an auto generated Trx file name under test results directory.
    /// </summary>
    private string SetDefaultTrxFilePath()
    {
        TPDebug.Assert(LoggerTestRun != null, "LoggerTestRun is null");
        TPDebug.Assert(LoggerTestRun.RunConfiguration != null, "LoggerTestRun.RunConfiguration is null");
        TPDebug.Assert(IsInitialized, "Logger is not initialized");

        var baseName = LoggerTestRun.RunConfiguration.RunDeploymentRootDirectory;

        if (_parametersDictionary is not null
            && _parametersDictionary.TryGetValue(DefaultLoggerParameterNames.TargetFramework, out var framework)
            && !framework.IsNullOrWhiteSpace())
        {
            var shortName = Framework.FromString(framework)?.ShortName ?? framework;
            baseName = baseName + "_" + shortName;
        }

        var defaultTrxFileName = baseName + ".trx";

        return TrxFileHelper.GetNextIterationFileName(_testResultsDirPath, defaultTrxFileName, false);
    }

    /// <summary>
    /// Creates test run.
    /// </summary>
    [MemberNotNull(nameof(LoggerTestRun))]
    private void CreateTestRun()
    {
        // Skip run creation if already exists.
        if (LoggerTestRun != null)
            return;

        Guid runId = Guid.NewGuid();
        LoggerTestRun = new TestRun(runId);

        // We cannot rely on the StartTime for the first test result
        // In case of parallel, first test result is the fastest test and not the one which started first.
        // Setting Started to DateTime.Now in Initialize will make sure we include the startup cost, which was being ignored earlier.
        // This is in parity with the way we set this.testRun.Finished
        LoggerTestRun.Started = TestRunStartTime;

        // Save default test settings
        string runDeploymentRoot = TrxFileHelper.ReplaceInvalidFileNameChars(LoggerTestRun.Name);
        TestRunConfiguration testrunConfig = new("default", _trxFileHelper);
        testrunConfig.RunDeploymentRootDirectory = runDeploymentRoot;
        LoggerTestRun.RunConfiguration = testrunConfig;
    }

    /// <summary>
    /// Gets test result from stored test results.
    /// </summary>
    /// <param name="executionId"></param>
    /// <returns>Test result</returns>
    private ITestResult? GetTestResult(Guid executionId)
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        ITestResult? testResult = null;

        if (executionId != Guid.Empty)
        {
            _results.TryGetValue(executionId, out testResult);

            if (testResult == null)
                _innerResults.TryGetValue(executionId, out testResult);
        }

        return testResult;
    }

    /// <summary>
    /// Gets test element from stored test elements.
    /// </summary>
    /// <param name="testId"></param>
    /// <returns></returns>
    private ITestElement? GetTestElement(Guid testId)
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        _testElements.TryGetValue(testId, out var testElement);
        return testElement;
    }

    /// <summary>
    /// Gets or creates test element.
    /// </summary>
    /// <param name="executionId"></param>
    /// <param name="parentExecutionId"></param>
    /// <param name="testType"></param>
    /// <param name="parentTestElement"></param>
    /// <param name="rockSteadyTestResult"></param>
    /// <returns>Trx test element</returns>
    private ITestElement GetOrCreateTestElement(Guid executionId, Guid parentExecutionId, TestType testType, ITestElement? parentTestElement, ObjectModel.TestResult rockSteadyTestResult)
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        ITestElement? testElement = parentTestElement;

        // For scenarios like data driven tests, test element is same as parent test element.
        if (parentTestElement != null && !parentTestElement.TestType.Equals(TrxLoggerConstants.OrderedTestType))
        {
            return testElement!;
        }

        TestCase testCase = rockSteadyTestResult.TestCase;
        Guid testId = Converter.GetTestId(testCase);

        // Get test element
        testElement = GetTestElement(testId);

        // Create test element
        if (testElement == null)
        {
            testElement = Converter.ToTestElement(testId, executionId, parentExecutionId, testCase.DisplayName, testType, testCase);
            _testElements.TryAdd(testId, testElement);
        }

        return testElement;
    }

    /// <summary>
    /// Update test links
    /// </summary>
    /// <param name="testElement"></param>
    /// <param name="parentTestElement"></param>
    private static void UpdateTestLinks(ITestElement testElement, ITestElement? parentTestElement)
    {
        if (parentTestElement == null
            || !parentTestElement.TestType.Equals(TrxLoggerConstants.OrderedTestType))
        {
            return;
        }

        var orderedTest = (OrderedTestElement)parentTestElement;
        if (!orderedTest.TestLinks.ContainsKey(testElement.Id.Id))
        {
            orderedTest.TestLinks.Add(testElement.Id.Id, new TestLink(testElement.Id.Id, testElement.Name, testElement.Storage));
        }
    }

    /// <summary>
    /// Creates test result
    /// </summary>
    /// <param name="executionId"></param>
    /// <param name="parentExecutionId"></param>
    /// <param name="testType"></param>
    /// <param name="testElement"></param>
    /// <param name="parentTestElement"></param>
    /// <param name="parentTestResult"></param>
    /// <param name="rocksteadyTestResult"></param>
    /// <returns>Trx test result</returns>
    private ITestResult CreateTestResult(Guid executionId, Guid parentExecutionId, TestType testType,
        ITestElement testElement, ITestElement? parentTestElement, ITestResult? parentTestResult, ObjectModel.TestResult rocksteadyTestResult)
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        // Create test result
        TrxLoggerObjectModel.TestOutcome testOutcome = Converter.ToOutcome(rocksteadyTestResult.Outcome);
        TPDebug.Assert(LoggerTestRun != null, "LoggerTestRun is null");
        var testResult = _converter.ToTestResult(testElement.Id.Id, executionId, parentExecutionId, testElement.Name,
            _testResultsDirPath, testType, testElement.CategoryId, testOutcome, LoggerTestRun, rocksteadyTestResult);

        // Normal result scenario
        if (parentTestResult == null)
        {
            _results.TryAdd(executionId, testResult);
            return testResult;
        }

        // Ordered test inner result scenario
        if (parentTestElement != null && parentTestElement.TestType.Equals(TrxLoggerConstants.OrderedTestType))
        {
            TPDebug.Assert(parentTestResult is TestResultAggregation, "parentTestResult is not of type TestResultAggregation");
            ((TestResultAggregation)parentTestResult).InnerResults.Add(testResult);
            _innerResults.TryAdd(executionId, testResult);
            return testResult;
        }

        // Data driven inner result scenario
        if (parentTestElement != null && parentTestElement.TestType.Equals(TrxLoggerConstants.UnitTestType))
        {
            TPDebug.Assert(parentTestResult is TestResultAggregation, "parentTestResult is not of type TestResultAggregation");
            var testResultAggregation = (TestResultAggregation)parentTestResult;
            testResultAggregation.InnerResults.Add(testResult);
            testResult.DataRowInfo = testResultAggregation.InnerResults.Count;
            testResult.ResultType = TrxLoggerConstants.InnerDataDrivenResultType;
            parentTestResult.ResultType = TrxLoggerConstants.ParentDataDrivenResultType;
            return testResult;
        }

        return testResult;
    }

    /// <summary>
    /// Update test entries
    /// </summary>
    /// <param name="executionId"></param>
    /// <param name="parentExecutionId"></param>
    /// <param name="testElement"></param>
    /// <param name="parentTestElement"></param>
    private void UpdateTestEntries(Guid executionId, Guid parentExecutionId, ITestElement testElement, ITestElement? parentTestElement)
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        TestEntry te = new(testElement.Id, TestListCategory.UncategorizedResults.Id);
        te.ExecutionId = executionId;

        if (parentTestElement == null)
        {
            _entries.TryAdd(executionId, te);
        }
        else if (parentTestElement.TestType.Equals(TrxLoggerConstants.OrderedTestType))
        {
            te.ParentExecutionId = parentExecutionId;

            var parentTestEntry = GetTestEntry(parentExecutionId);
            if (parentTestEntry != null)
                parentTestEntry.TestEntries.Add(te);

            _innerTestEntries.TryAdd(executionId, te);
        }
    }

    /// <summary>
    /// Gets test entry from stored test entries.
    /// </summary>
    /// <param name="executionId"></param>
    /// <returns>Test entry</returns>
    private TestEntry? GetTestEntry(Guid executionId)
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");
        TestEntry? testEntry = null;

        if (executionId != Guid.Empty)
        {
            _entries.TryGetValue(executionId, out testEntry);

            if (testEntry == null)
                _innerTestEntries.TryGetValue(executionId, out testEntry);
        }

        return testEntry;
    }

    private TrxLoggerObjectModel.TestOutcome ChangeTestOutcomeIfNecessary(TrxLoggerObjectModel.TestOutcome outcome)
    {
        TPDebug.Assert(IsInitialized, "Logger is not initialized");

        // If no tests discovered/executed and TreatNoTestsAsError was set to True
        // We will return ResultSummary as Failed
        // Note : we only send the value of TreatNoTestsAsError if it is "True"
        if (TotalTestCount == 0 && _parametersDictionary?.ContainsKey(ObjectModelConstants.TreatNoTestsAsError) == true)
        {
            outcome = TrxLoggerObjectModel.TestOutcome.Failed;
        }

        return outcome;
    }

    #endregion
}