File: ObjectModel\TestResult.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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

using Microsoft.TestPlatform.Extensions.TrxLogger.Utility;
using Microsoft.TestPlatform.Extensions.TrxLogger.XML;
using Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;

using TrxLoggerResources = Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger.Resources.TrxResource;

namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel;

/// <summary>
/// Class to uniquely identify test results
/// </summary>
internal sealed class TestResultId : IXmlTestStore
{
    private readonly Guid _runId;

    /// <summary>
    /// Initializes a new instance of the <see cref="TestResultId"/> class.
    /// </summary>
    /// <param name="runId">
    /// The run id.
    /// </param>
    /// <param name="executionId">
    /// The execution id.
    /// </param>
    /// <param name="parentExecutionId">
    /// The parent execution id.
    /// </param>
    /// <param name="testId">
    /// The test id.
    /// </param>
    public TestResultId(Guid runId, Guid executionId, Guid parentExecutionId, Guid testId)
    {
        _runId = runId;
        ExecutionId = executionId;
        ParentExecutionId = parentExecutionId;
        TestId = testId;
    }

    /// <summary>
    /// Gets the execution id.
    /// </summary>
    public Guid ExecutionId { get; }

    /// <summary>
    /// Gets the parent execution id.
    /// </summary>
    public Guid ParentExecutionId { get; }

    /// <summary>
    /// Gets the test id.
    /// </summary>
    public Guid TestId { get; }


    #region Overrides

    /// <summary>
    /// Override function for Equals
    /// </summary>
    /// <param name="obj">
    /// The object to compare
    /// </param>
    /// <returns>
    /// The <see cref="bool"/>.
    /// </returns>
    public override bool Equals(object? obj)
    {
        return obj is TestResultId tmpId && _runId.Equals(tmpId._runId) && ExecutionId.Equals((object)tmpId.ExecutionId);
    }

    /// <summary>
    /// Override function for GetHashCode.
    /// </summary>
    /// <returns>
    /// The <see cref="int"/>.
    /// </returns>
    public override int GetHashCode()
    {
        return _runId.GetHashCode() ^ ExecutionId.GetHashCode();
    }

    /// <summary>
    /// Override function for ToString.
    /// </summary>
    /// <returns>
    /// The <see cref="string"/>.
    /// </returns>
    public override string ToString()
    {
        return ExecutionId.ToString("B");
    }
    #endregion

    #region IXmlTestStore Members

    /// <summary>
    /// Saves the class under the XmlElement..
    /// </summary>
    /// <param name="element">
    /// The parent xml.
    /// </param>
    /// <param name="parameters">
    /// The parameter
    /// </param>
    public void Save(System.Xml.XmlElement element, XmlTestStoreParameters? parameters)
    {
        XmlPersistence helper = new();

        if (ExecutionId != Guid.Empty)
            helper.SaveGuid(element, "@executionId", ExecutionId);
        if (ParentExecutionId != Guid.Empty)
            helper.SaveGuid(element, "@parentExecutionId", ParentExecutionId);

        helper.SaveGuid(element, "@testId", TestId);
    }

    #endregion
}

/// <summary>
/// The test result error info class.
/// </summary>
internal sealed class TestResultErrorInfo : IXmlTestStore
{
    [StoreXmlSimpleField("Message", "")]
    private string? _message;

    [StoreXmlSimpleField("StackTrace", "")]
    private string? _stackTrace;


    /// <summary>
    /// Gets or sets the message.
    /// </summary>
    public string? Message
    {
        get { return _message; }
        set { _message = value; }
    }

    /// <summary>
    /// Gets or sets the stack trace.
    /// </summary>
    public string? StackTrace
    {
        get { return _stackTrace; }
        set { _stackTrace = value; }
    }

    #region IXmlTestStore Members

    /// <summary>
    /// Saves the class under the XmlElement..
    /// </summary>
    /// <param name="element">
    /// The parent xml.
    /// </param>
    /// <param name="parameters">
    /// The parameter
    /// </param>
    public void Save(System.Xml.XmlElement element, XmlTestStoreParameters? parameters)
    {
        XmlPersistence.SaveUsingReflection(element, this, typeof(TestResultErrorInfo), parameters);
    }

    #endregion
}

/// <summary>
/// Class for test result.
/// </summary>
internal class TestResult : ITestResult, IXmlTestStore
{
    private readonly string _resultName;
    private string? _stdOut;
    private string? _stdErr;
    private string? _debugTrace;
    private TimeSpan _duration;
    private readonly TestType _testType;
    private TestRun? _testRun;
    private TestResultErrorInfo? _errorInfo;
    private readonly TestListCategoryId _categoryId;
    private ArrayList _textMessages;
    private readonly TrxFileHelper _trxFileHelper;

    /// <summary>
    /// Paths to test result files, relative to the test results folder, sorted in increasing order
    /// </summary>
    private readonly SortedList<string, object?> _resultFiles = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>
    /// Information provided by data collectors for the test case
    /// </summary>
    private readonly List<CollectorDataEntry> _collectorDataEntries = new();

    /// <summary>
    /// Initializes a new instance of the <see cref="TestResult"/> class.
    /// </summary>
    /// <param name="computerName">
    /// The computer name.
    /// </param>
    /// <param name="runId">
    /// The run id.
    /// </param>
    /// <param name="executionId">Execution id.</param>
    /// <param name="parentExecutionId">Parent execution id.</param>
    /// <param name="resultName">Result name.</param>
    /// <param name="testId">
    /// The test id.
    /// </param>
    /// <param name="outcome">
    /// The outcome.
    /// </param>
    /// <param name="testType"></param>
    /// <param name="testCategoryId"></param>
    /// <param name="trxFileHelper"></param>
    public TestResult(
        Guid runId,
        Guid testId,
        Guid executionId,
        Guid parentExecutionId,
        string resultName,
        string computerName,
        TestOutcome outcome,
        TestType testType,
        TestListCategoryId testCategoryId,
        TrxFileHelper trxFileHelper)
    {
        TPDebug.Assert(computerName != null, "computername is null");
        TPDebug.Assert(!Guid.Empty.Equals(executionId), "ExecutionId is empty");
        TPDebug.Assert(!Guid.Empty.Equals(testId), "TestId is empty");

        _textMessages = new ArrayList();
        DataRowInfo = -1;

        Id = new TestResultId(runId, executionId, parentExecutionId, testId);
        _resultName = resultName;
        _testType = testType;
        ComputerName = computerName;
        Outcome = outcome;
        _categoryId = testCategoryId;
        RelativeTestResultsDirectory = TestRunDirectories.GetRelativeTestResultsDirectory(executionId);
        _trxFileHelper = trxFileHelper;
    }

    /// <summary>
    /// Gets or sets the end time.
    /// </summary>
    public DateTime EndTime { get; set; }

    /// <summary>
    /// Gets or sets the start time.
    /// </summary>
    public DateTime StartTime { get; set; }

    /// <summary>
    /// Gets or sets the duration.
    /// </summary>
    public TimeSpan Duration
    {
        get { return _duration; }

        set
        {
            // On some hardware the Stopwatch.Elapsed can return a negative number.  This tends
            // to happen when the duration of the test is very short and it is hardware dependent
            // (seems to happen most on virtual machines or machines with AMD processors).  To prevent
            // reporting a negative duration, use TimeSpan.Zero when the elapsed time is less than zero.
            EqtTrace.WarningIf(value < TimeSpan.Zero, "TestResult.Duration: The duration is being set to {0}.  Since the duration is negative the duration will be updated to zero.", value);
            _duration = value > TimeSpan.Zero ? value : TimeSpan.Zero;
        }
    }

    /// <summary>
    /// Gets the computer name.
    /// </summary>
    public string ComputerName { get; }

    /// <summary>
    /// Gets or sets the outcome.
    /// </summary>
    public TestOutcome Outcome { get; set; }


    /// <summary>
    /// Gets or sets the id.
    /// </summary>
    public TestResultId Id { get; internal set; }

    /// <summary>
    /// Gets or sets the error message.
    /// </summary>
    public string ErrorMessage
    {
        get { return _errorInfo?.Message ?? string.Empty; }
        set
        {
            _errorInfo ??= new TestResultErrorInfo();

            _errorInfo.Message = value;
        }
    }

    /// <summary>
    /// Gets or sets the error stack trace.
    /// </summary>
    public string ErrorStackTrace
    {
        get { return _errorInfo?.StackTrace ?? string.Empty; }

        set
        {
            _errorInfo ??= new TestResultErrorInfo();

            _errorInfo.StackTrace = value;
        }
    }

    /// <summary>
    /// Gets the text messages.
    /// </summary>
    /// <remarks>
    /// Additional information messages from TestTextResultMessage, e.g. generated by TestOutcome.WriteLine.
    /// Avoid using this property in the following way: for (int i=0; i&lt;prop.Length; i++) { ... prop[i] ...}
    /// </remarks>
    [NotNull]
    public string[]? TextMessages
    {
        get { return (string[])_textMessages.ToArray(typeof(string)); }

        set
        {
            if (value != null)
                _textMessages = new ArrayList(value);
            else
                _textMessages.Clear();
        }
    }

    /// <summary>
    /// Gets or sets the standard out.
    /// </summary>
    public string StdOut
    {
        get { return _stdOut ?? string.Empty; }
        set { _stdOut = value; }
    }

    /// <summary>
    /// Gets or sets the standard err.
    /// </summary>
    public string StdErr
    {
        get { return _stdErr ?? string.Empty; }
        set { _stdErr = value; }
    }

    /// <summary>
    /// Gets or sets the debug trace.
    /// </summary>
    public string DebugTrace
    {
        get { return _debugTrace ?? string.Empty; }
        set { _debugTrace = value; }
    }

    /// <summary>
    /// Gets the path to the test results directory
    /// </summary>
    public string TestResultsDirectory
    {
        get
        {
            if (_testRun == null)
            {
                Debug.Fail("'m_testRun' is null");
                throw new InvalidOperationException(TrxLoggerResources.Common_MissingRunInResult);
            }

            return _testRun.GetResultFilesDirectory(this);
        }
    }

    /// <summary>
    /// Gets the directory containing the test result files, relative to the root results directory
    /// </summary>
    public string RelativeTestResultsDirectory { get; }

    /// <summary>
    /// Gets or sets the data row info.
    /// </summary>
    public int DataRowInfo { get; set; }

    /// <summary>
    /// Gets or sets the result type.
    /// </summary>
    public string? ResultType { get; set; }


    #region Overrides
    public override bool Equals(object? obj)
    {
        if (obj is not TestResult trm)
        {
            return false;
        }
        TPDebug.Assert(Id != null, "id is null");
        TPDebug.Assert(trm.Id != null, "test result message id is null");
        return Id.Equals(trm.Id);
    }

    public override int GetHashCode()
    {
        TPDebug.Assert(Id != null, "id is null");
        return Id.GetHashCode();
    }

    #endregion

    /// <summary>
    /// Helper function to add a text message info to the test result
    /// </summary>
    /// <param name="text">Message to be added</param>
    public void AddTextMessage(string text)
    {
        EqtAssert.ParameterNotNull(text, nameof(text));
        _textMessages.Add(text);
    }

    /// <summary>
    /// Sets the test run the test was executed in
    /// </summary>
    /// <param name="testRun">The test run the test was executed in</param>
    internal virtual void SetTestRun(TestRun testRun)
    {
        TPDebug.Assert(testRun != null, "'testRun' is null");
        _testRun = testRun;
    }

    /// <summary>
    /// Adds result files to the <see cref="_resultFiles"/> collection
    /// </summary>
    /// <param name="resultFileList">Paths to the result files</param>
    internal void AddResultFiles(IEnumerable<string> resultFileList)
    {
        TPDebug.Assert(resultFileList != null, "'resultFileList' is null");

        string testResultsDirectory = TestResultsDirectory;
        foreach (string resultFile in resultFileList)
        {
            TPDebug.Assert(!string.IsNullOrEmpty(resultFile), "'resultFile' is null or empty");
            TPDebug.Assert(resultFile.Trim() == resultFile, "'resultFile' has whitespace at the ends");

            _resultFiles[TrxFileHelper.MakePathRelative(resultFile, testResultsDirectory)] = null;
        }
    }

    /// <summary>
    /// Adds collector data entries to the <see cref="_collectorDataEntries"/> collection
    /// </summary>
    /// <param name="collectorDataEntryList">The collector data entry to add</param>
    internal void AddCollectorDataEntries(IEnumerable<CollectorDataEntry> collectorDataEntryList)
    {
        TPDebug.Assert(collectorDataEntryList != null, "'collectorDataEntryList' is null");

        string testResultsDirectory = TestResultsDirectory;
        foreach (CollectorDataEntry collectorDataEntry in collectorDataEntryList)
        {
            TPDebug.Assert(collectorDataEntry != null, "'collectorDataEntry' is null");
            TPDebug.Assert(!_collectorDataEntries.Contains(collectorDataEntry), "The collector data entry already exists in the collection");

            _collectorDataEntries.Add(collectorDataEntry.CloneWithRelativePath(testResultsDirectory));
        }
    }


    #region IXmlTestStore Members

    /// <summary>
    /// Saves the class under the XmlElement..
    /// </summary>
    /// <param name="element">
    /// The parent xml.
    /// </param>
    /// <param name="parameters">
    /// The parameter
    /// </param>
    public virtual void Save(System.Xml.XmlElement element, XmlTestStoreParameters? parameters)
    {
        XmlPersistence helper = new();

        helper.SaveObject(Id, element, ".", parameters);
        helper.SaveSimpleField(element, "@testName", _resultName, string.Empty);
        helper.SaveSimpleField(element, "@computerName", ComputerName, string.Empty);
        helper.SaveSimpleField(element, "@duration", _duration, default(TimeSpan));
        helper.SaveSimpleField(element, "@startTime", StartTime, default(DateTime));
        helper.SaveSimpleField(element, "@endTime", EndTime, default(DateTime));
        helper.SaveGuid(element, "@testType", _testType.Id);

        if (_stdOut != null)
            _stdOut = _stdOut.Trim();

        if (_stdErr != null)
            _stdErr = _stdErr.Trim();

        helper.SaveSimpleField(element, "@outcome", Outcome, default(TestOutcome));
        helper.SaveSimpleField(element, "Output/StdOut", _stdOut, string.Empty);
        helper.SaveSimpleField(element, "Output/StdErr", _stdErr, string.Empty);
        helper.SaveSimpleField(element, "Output/DebugTrace", _debugTrace, string.Empty);
        helper.SaveObject(_errorInfo, element, "Output/ErrorInfo", parameters);
        helper.SaveGuid(element, "@testListId", _categoryId.Id);
        helper.SaveIEnumerable(_textMessages, element, "Output/TextMessages", ".", "Message", parameters);
        helper.SaveSimpleField(element, "@relativeResultsDirectory", RelativeTestResultsDirectory, null);
        helper.SaveIEnumerable(_resultFiles.Keys, element, "ResultFiles", "@path", "ResultFile", parameters);
        helper.SaveIEnumerable(_collectorDataEntries, element, "CollectorDataEntries", ".", "Collector", parameters);

        if (DataRowInfo >= 0)
            helper.SaveSimpleField(element, "@dataRowInfo", DataRowInfo, -1);

        if (!string.IsNullOrEmpty(ResultType))
            helper.SaveSimpleField(element, "@resultType", ResultType, string.Empty);
    }

    #endregion
}