File: Client\MTP\MtpTestNodeConverter.cs
Web Access
Project: 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 Jsonite;

using Microsoft.VisualStudio.TestPlatform.ObjectModel;

namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Client.MTP;

/// <summary>
/// Converts Microsoft.Testing.Platform (MTP) test nodes into vstest ObjectModel
/// <see cref="TestCase"/> and <see cref="TestResult"/> instances.
///
/// The converter works purely off the MTP node shape (<c>uid</c>, <c>display-name</c>,
/// <c>execution-state</c>, <c>time.duration-ms</c>, <c>error.*</c>, <c>location.*</c>, <c>traits</c>)
/// so that an MTP application with no dependency on vstest at all still converts correctly. When the
/// optional <c>vstest.*</c> bridge properties are present they are used purely as enrichment.
/// </summary>
internal static class MtpTestNodeConverter
{
    internal static readonly TestProperty MtpUidProperty = TestProperty.Register(
        MtpConstants.MtpUidPropertyId,
        "MTP Uid",
        typeof(string),
        typeof(TestCase));

    /// <summary>
    /// Returns true when the node represents a runnable test (a leaf "action" node) rather than a
    /// grouping node (namespace/class/suite).
    /// </summary>
    public static bool IsActionNode(JsonObject node)
        => MtpJson.GetString(node, MtpConstants.NodeType) is "action";

    public static string? GetExecutionState(JsonObject node)
        => MtpJson.GetString(node, MtpConstants.ExecutionState);

    public static TestCase ToTestCase(JsonObject node, string source)
    {
        string uid = MtpJson.GetString(node, MtpConstants.Uid) ?? Guid.NewGuid().ToString();
        string fullyQualifiedName = MtpJson.GetString(node, MtpConstants.VsTestFullyQualifiedName) ?? uid;
        string executorUri = MtpJson.GetString(node, MtpConstants.VsTestExecutorUri) ?? MtpConstants.DefaultExecutorUri;

        var testCase = new TestCase(fullyQualifiedName, new Uri(executorUri), source)
        {
            DisplayName = MtpJson.GetString(node, MtpConstants.DisplayName) ?? fullyQualifiedName,
        };

        testCase.SetPropertyValue(MtpUidProperty, uid);

        string? file = MtpJson.GetString(node, MtpConstants.LocationFile);
        if (!string.IsNullOrEmpty(file))
        {
            testCase.CodeFilePath = file;
            if (MtpJson.TryGetInt(node, MtpConstants.LocationLineStart, out int line))
            {
                testCase.LineNumber = line;
            }
        }

        AddTraits(node, testCase);
        return testCase;
    }

    public static TestResult ToTestResult(JsonObject node, string source)
    {
        var testCase = ToTestCase(node, source);
        string? state = GetExecutionState(node);

        var result = new TestResult(testCase)
        {
            Outcome = ToOutcome(state),
            DisplayName = testCase.DisplayName,
            ErrorMessage = MtpJson.GetString(node, MtpConstants.ErrorMessage),
            ErrorStackTrace = MtpJson.GetString(node, MtpConstants.ErrorStackTrace),
        };

        if (MtpJson.TryGetDouble(node, MtpConstants.TimeDurationMs, out double durationMs))
        {
            result.Duration = TimeSpan.FromMilliseconds(durationMs);
        }

        return result;
    }

    public static bool IsTerminalState(string? state)
        => state is MtpConstants.StatePassed
            or MtpConstants.StateFailed
            or MtpConstants.StateSkipped
            or MtpConstants.StateError
            or MtpConstants.StateTimedOut;

    private static TestOutcome ToOutcome(string? state)
        => state switch
        {
            MtpConstants.StatePassed => TestOutcome.Passed,
            MtpConstants.StateFailed => TestOutcome.Failed,
            MtpConstants.StateError => TestOutcome.Failed,
            MtpConstants.StateTimedOut => TestOutcome.Failed,
            MtpConstants.StateSkipped => TestOutcome.Skipped,
            _ => TestOutcome.None,
        };

    private static void AddTraits(JsonObject node, TestCase testCase)
    {
        if (MtpJson.GetValue(node, MtpConstants.Traits) is not JsonArray traits)
        {
            return;
        }

        foreach (object? traitObject in traits)
        {
            if (traitObject is not JsonObject trait)
            {
                continue;
            }

            foreach (KeyValuePair<string, object> property in trait)
            {
                string value = property.Value as string ?? string.Empty;
                testCase.Traits.Add(new Trait(property.Key, value));
            }
        }
    }
}