| File: TrxReader.cs | Web Access |
| Project: src\tools\Aspire.TestTools\Aspire.TestTools.csproj (Aspire.TestTools) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; using System.Xml.Serialization; namespace Aspire.TestTools; public class TrxReader { public static IList<TestResult> GetTestResultsFromTrx(string filepath, Func<string, string, bool>? testFilter = null) { XmlSerializer serializer = new(typeof(TestRun)); using FileStream fileStream = new(filepath, FileMode.Open); if (serializer.Deserialize(fileStream) is not TestRun testRun || testRun.Results?.UnitTestResults is null) { return Array.Empty<TestResult>(); } var testResults = new List<TestResult>(); foreach (var unitTestResult in testRun.Results.UnitTestResults) { if (string.IsNullOrEmpty(unitTestResult.TestName) || string.IsNullOrEmpty(unitTestResult.Outcome)) { continue; } if (testFilter is not null && !testFilter(unitTestResult.TestName, unitTestResult.Outcome)) { continue; } var startTime = unitTestResult.StartTime; var endTime = unitTestResult.EndTime; testResults.Add(new TestResult( Name: unitTestResult.TestName, Outcome: unitTestResult.Outcome, StartTime: startTime is null ? TimeSpan.MinValue : TimeSpan.Parse(startTime, CultureInfo.InvariantCulture), EndTime: endTime is null ? TimeSpan.MinValue : TimeSpan.Parse(endTime, CultureInfo.InvariantCulture), ErrorMessage: unitTestResult.Output?.ErrorInfoString, Stdout: unitTestResult.Output?.StdOut )); } return testResults; } public static IList<DetailedTestResult> GetDetailedTestResultsFromTrx(string filepath, Func<DetailedTestResult, bool>? testFilter = null) { var testRun = DeserializeTrxFile(filepath); if (testRun?.Results?.UnitTestResults is null) { return Array.Empty<DetailedTestResult>(); } var testDefinitions = testRun.TestDefinitions?.UnitTests? .Where(static testDefinition => !string.IsNullOrWhiteSpace(testDefinition.Id)) .ToDictionary(static testDefinition => testDefinition.Id!, StringComparer.OrdinalIgnoreCase); var testResults = new List<DetailedTestResult>(); foreach (var unitTestResult in testRun.Results.UnitTestResults) { if (string.IsNullOrWhiteSpace(unitTestResult.TestName) || string.IsNullOrWhiteSpace(unitTestResult.Outcome)) { continue; } var canonicalName = GetCanonicalTestName(unitTestResult, testDefinitions); var detailedTestResult = new DetailedTestResult( CanonicalName: canonicalName, DisplayName: unitTestResult.TestName, Outcome: unitTestResult.Outcome, ErrorMessage: unitTestResult.Output?.ErrorMessage, StackTrace: unitTestResult.Output?.StackTrace, Stdout: unitTestResult.Output?.StdOut); if (testFilter is not null && !testFilter(detailedTestResult)) { continue; } testResults.Add(detailedTestResult); } return testResults; } public static TestRun? DeserializeTrxFile(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) { throw new ArgumentException($"{nameof(filePath)} cannot be null or empty.", nameof(filePath)); } XmlSerializer serializer = new(typeof(TestRun)); using FileStream fileStream = new(filePath, FileMode.Open, FileAccess.Read); return serializer.Deserialize(fileStream) as TestRun; } public static double GetTestRunDurationInMinutes(TestRun testRun) { // First try to use the Times element if available if (testRun?.Times is not null && !string.IsNullOrEmpty(testRun.Times.Start) && !string.IsNullOrEmpty(testRun.Times.Finish)) { if (DateTime.TryParse(testRun.Times.Start, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var start) && DateTime.TryParse(testRun.Times.Finish, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var finish)) { return (finish - start).TotalMinutes; } } // Fall back to computing from individual test results if Times element is not available if (testRun?.Results?.UnitTestResults is null || testRun.Results.UnitTestResults.Count == 0) { return 0.0; } DateTime? earliestStartTime = null; DateTime? latestEndTime = null; foreach (var unitTestResult in testRun.Results.UnitTestResults) { if (DateTime.TryParse(unitTestResult.StartTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var startTime)) { if (earliestStartTime is null || startTime < earliestStartTime) { earliestStartTime = startTime; } } if (DateTime.TryParse(unitTestResult.EndTime, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var endTime)) { if (latestEndTime is null || endTime > latestEndTime) { latestEndTime = endTime; } } } if (earliestStartTime is null || latestEndTime is null) { return 0.0; } return (latestEndTime.Value - earliestStartTime.Value).TotalMinutes; } private static string GetCanonicalTestName(UnitTestResult unitTestResult, IReadOnlyDictionary<string, UnitTestDefinition>? testDefinitions) { if (!string.IsNullOrWhiteSpace(unitTestResult.TestId) && testDefinitions is not null && testDefinitions.TryGetValue(unitTestResult.TestId, out var testDefinition) && testDefinition.TestMethod is { ClassName: { Length: > 0 } className, Name: { Length: > 0 } methodName }) { if (methodName.StartsWith($"{className}.", StringComparison.Ordinal)) { return methodName; } return $"{className}.{methodName}"; } return unitTestResult.TestName ?? string.Empty; } } [XmlRoot("TestRun", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")] public class TestRun { public Results? Results { get; set; } public ResultSummary? ResultSummary { get; set; } public Times? Times { get; set; } public TestDefinitions? TestDefinitions { get; set; } } public class Results { [XmlElement("UnitTestResult")] public List<UnitTestResult>? UnitTestResults { get; set; } } public class Times { [XmlAttribute("start")] public string? Start { get; set; } [XmlAttribute("finish")] public string? Finish { get; set; } [XmlAttribute("creation")] public string? Creation { get; set; } [XmlAttribute("queuing")] public string? Queuing { get; set; } } public class UnitTestResult { [XmlAttribute("testId")] public string? TestId { get; set; } [XmlAttribute("testName")] public string? TestName { get; set; } [XmlAttribute("outcome")] public string? Outcome { get; set; } [XmlAttribute("startTime")] public string? StartTime { get; set; } [XmlAttribute("endTime")] public string? EndTime { get; set; } [XmlAttribute("duration")] public string? Duration { get; set; } public Output? Output { get; set; } } public class Output { public ErrorInfo? ErrorInfo { get; set; } public string? StdOut { get; set; } [XmlIgnore] public string ErrorInfoString => string.Join( Environment.NewLine, new[] { ErrorMessage, StackTrace }.Where(static value => !string.IsNullOrWhiteSpace(value))); [XmlIgnore] public string? ErrorMessage => ErrorInfo?.Message; [XmlIgnore] public string? StackTrace => ErrorInfo?.StackTrace; } public class ErrorInfo { public string? Message { get; set; } public string? StackTrace { get; set; } } public class ResultSummary { public string? Outcome { get; set; } public Counters? Counters { get; set; } } public class Counters { [XmlAttribute("total")] public int Total { get; set; } [XmlAttribute("executed")] public int Executed { get; set; } [XmlAttribute("passed")] public int Passed { get; set; } [XmlAttribute("failed")] public int Failed { get; set; } [XmlAttribute("error")] public int Error { get; set; } [XmlAttribute("timeout")] public int Timeout { get; set; } [XmlAttribute("aborted")] public int Aborted { get; set; } [XmlAttribute("inconclusive")] public int Inconclusive { get; set; } [XmlAttribute("passedButRunAborted")] public int PassedButRunAborted { get; set; } [XmlAttribute("notRunnable")] public int NotRunnable { get; set; } [XmlAttribute("notExecuted")] public int NotExecuted { get; set; } } public record TestResult(string Name, string Outcome, TimeSpan StartTime, TimeSpan EndTime, string? ErrorMessage = null, string? Stdout = null); public record DetailedTestResult(string CanonicalName, string DisplayName, string Outcome, string? ErrorMessage = null, string? StackTrace = null, string? Stdout = null); public class TestDefinitions { [XmlElement("UnitTest")] public List<UnitTestDefinition>? UnitTests { get; set; } } public class UnitTestDefinition { [XmlAttribute("id")] public string? Id { get; set; } [XmlAttribute("name")] public string? Name { get; set; } public TestMethodDefinition? TestMethod { get; set; } } public class TestMethodDefinition { [XmlAttribute("className")] public string? ClassName { get; set; } [XmlAttribute("name")] public string? Name { get; set; } }