From 4d0a78f56cf5cc953be76a0af28aaea6b7126339 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 22:07:08 -0700 Subject: [PATCH 01/11] feat: AOT compliance with multi-targeting (netstandard2.1 + net8.0 + net10.0) (#41) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Makes TrxLib fully AOT-compatible by replacing `XmlSerializer` with `XDocument`/`XElement` manual parsing, adding multi-targeting, and validating with a native AOT smoke-test project in CI. ## Changes ### Library (`TrxLib`) - **Multi-target**: `netstandard2.1;net8.0;net10.0` — keeps backward compatibility while enabling `IsAotCompatible=true` on net8.0+ - **Parser rewrite**: `XmlSerializer` → `XDocument.Load()` + `XElement` navigation (fully AOT-safe, available on all targets, no `#if` branching) - **New model classes**: `Execution`, `Counters`, `ResultSummary`, `TestList`, `TestLists`, `TestEntry`, `TestEntries` - **Complete parsing**: all previously-unset fields now populated — `RunUser`, `ResultSummary`/`Counters`, `TestLists`, `TestEntries`, `UnitTest.Storage`/`Execution`, `UnitTestResult.ExecutionId`/`TestListId`/`TestType`/`RelativeResultsDirectory` - **`TestResultSet` properties**: `TestRunId`, `DeploymentRoot`, `TestSettingsName` now populated from parsed data - **Theory/parameterized test names**: FQTN now correctly appends parameter suffixes (e.g. `MethodName(arg: "value")`) - **Exception handling**: `catch (Exception)` narrowed to `catch (XmlException)` in the XML loading path - **Cleanup**: removed inert `[XmlRoot]`/`[XmlElement]`/`[XmlAttribute]` attributes and `using System.Xml.Serialization` from all model classes ### AOT Smoke Test (`TrxLib.AotSample`) - New `net10.0` console app with `PublishAot=true` that references TrxLib - Writes a minimal TRX to a temp file, parses it with `TrxParser.Parse`, and asserts the result — exercising the full `XDocument` parse path in the native binary ### CI (`.github/workflows/build-and-test.yml`) - Added `AOT Publish Validation` step to the existing matrix job (ubuntu + windows) - Runs `dotnet publish -r --self-contained` then **executes the native binary** to catch both link-time and runtime AOT regressions ## Verification - ✅ 38/38 tests pass - ✅ `dotnet build` — 0 AOT analyzer warnings on net8.0/net10.0 - ✅ `dotnet publish -r win-x64` — 0 ILLink/AOT warnings - ✅ Native binary runs and prints `TrxLib AOT validation passed.` --- .github/workflows/build-and-test.yml | 9 +- TrxLib.AotSample/Program.cs | 20 ++ TrxLib.AotSample/TrxLib.AotSample.csproj | 22 ++ TrxLib.Tests/SampleTrxFiles/theory-tests.trx | 46 ++++ TrxLib.Tests/TrxParserTests.cs | 30 ++- TrxLib.slnx | 1 + TrxLib/Counters.cs | 33 +-- TrxLib/Deployment.cs | 3 - TrxLib/ErrorInfo.cs | 4 - TrxLib/Execution.cs | 10 +- TrxLib/Output.cs | 4 - TrxLib/ResultSummary.cs | 15 +- TrxLib/Results.cs | 3 - TrxLib/TestDefinitions.cs | 3 - TrxLib/TestEntries.cs | 8 +- TrxLib/TestEntry.cs | 10 +- TrxLib/TestList.cs | 9 +- TrxLib/TestLists.cs | 8 +- TrxLib/TestMethod.cs | 6 - TrxLib/TestRun.cs | 15 +- TrxLib/TestSettings.cs | 5 - TrxLib/Times.cs | 6 - TrxLib/TrxLib.csproj | 3 +- TrxLib/TrxParser.cs | 233 +++++++++++++++++-- TrxLib/UnitTest.cs | 7 - TrxLib/UnitTestResult.cs | 14 -- 26 files changed, 371 insertions(+), 156 deletions(-) create mode 100644 TrxLib.AotSample/Program.cs create mode 100644 TrxLib.AotSample/TrxLib.AotSample.csproj create mode 100644 TrxLib.Tests/SampleTrxFiles/theory-tests.trx diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index c45f3ac..397afc2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -54,4 +54,11 @@ jobs: run: dotnet build -p:ContinuousIntegrationBuild=True --no-restore --configuration Release - name: Test - run: dotnet test --no-build --configuration Release --verbosity normal \ No newline at end of file + run: dotnet test --no-build --configuration Release --verbosity normal + + - name: AOT Publish Validation + run: | + $rid = if ($env:RUNNER_OS -eq 'Windows') { 'win-x64' } else { 'linux-x64' } + dotnet publish TrxLib.AotSample/TrxLib.AotSample.csproj -r $rid -c Release --self-contained + $ext = if ($env:RUNNER_OS -eq 'Windows') { '.exe' } else { '' } + & "TrxLib.AotSample/bin/Release/net10.0/$rid/publish/TrxLib.AotSample$ext" \ No newline at end of file diff --git a/TrxLib.AotSample/Program.cs b/TrxLib.AotSample/Program.cs new file mode 100644 index 0000000..a1ec8e5 --- /dev/null +++ b/TrxLib.AotSample/Program.cs @@ -0,0 +1,20 @@ +using TrxLib; + +var sampleRoot = Path.Combine(AppContext.BaseDirectory, "SampleTrxFiles"); +if (!Directory.Exists(sampleRoot)) + throw new DirectoryNotFoundException($"Sample TRX directory not found: {sampleRoot}"); + +var sampleFiles = Directory + .EnumerateFiles(sampleRoot, "*.trx", SearchOption.AllDirectories) + .OrderBy(path => path, StringComparer.Ordinal) + .ToArray(); + +if (sampleFiles.Length == 0) + throw new InvalidOperationException($"No TRX sample files found under: {sampleRoot}"); + +foreach (var file in sampleFiles) +{ + _ = TrxParser.Parse(new FileInfo(file)); +} + +Console.WriteLine($"TrxLib AOT validation passed. Parsed {sampleFiles.Length} sample files."); diff --git a/TrxLib.AotSample/TrxLib.AotSample.csproj b/TrxLib.AotSample/TrxLib.AotSample.csproj new file mode 100644 index 0000000..ef22c20 --- /dev/null +++ b/TrxLib.AotSample/TrxLib.AotSample.csproj @@ -0,0 +1,22 @@ + + + + Exe + net10.0 + true + true + false + + + + + + + + + + + diff --git a/TrxLib.Tests/SampleTrxFiles/theory-tests.trx b/TrxLib.Tests/SampleTrxFiles/theory-tests.trx new file mode 100644 index 0000000..3d49985 --- /dev/null +++ b/TrxLib.Tests/SampleTrxFiles/theory-tests.trx @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v3.1.5+1b188a7b0a (64-bit .NET 10.0.8) +[xUnit.net 00:00:00.08] Discovering: TrxLib.Tests +[xUnit.net 00:00:00.13] Discovered: TrxLib.Tests +[xUnit.net 00:00:00.15] Starting: TrxLib.Tests +[xUnit.net 00:00:00.20] Finished: TrxLib.Tests + + + + \ No newline at end of file diff --git a/TrxLib.Tests/TrxParserTests.cs b/TrxLib.Tests/TrxParserTests.cs index 9b56fd8..eb83079 100644 --- a/TrxLib.Tests/TrxParserTests.cs +++ b/TrxLib.Tests/TrxParserTests.cs @@ -168,6 +168,34 @@ private static bool NotWindows() return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows); } + [Fact] + public void Parse_TheoryTestsTrx_AppendsSuffixToFqtnForParameterizedTests() + { + var results = TrxParser.Parse(new FileInfo(GetSampleFilePath("theory-tests.trx"))); + results.Select(r => r.FullyQualifiedTestName) + .Should() + .Contain("Acme.Tests.MathTests.AddNumbers(left: 1, right: 2)"); + results.Select(r => r.FullyQualifiedTestName) + .Should() + .Contain("Acme.Tests.MathTests.AddNumbers(left: 0, right: 0)"); + } + + [Fact] + public void Parse_TheoryTestsTrx_DoesNotAppendSuffixForNonParameterizedTest() + { + var results = TrxParser.Parse(new FileInfo(GetSampleFilePath("theory-tests.trx"))); + results.Single(r => r.FullyQualifiedTestName == "Acme.Tests.MathTests.PlainTest") + .Should().NotBeNull(); + } + + [Fact] + public void Parse_TheoryTestsTrx_ParsesAllThreeTestResults() + { + var results = TrxParser.Parse(new FileInfo(GetSampleFilePath("theory-tests.trx"))); + results.Should().HaveCount(3); + results.Count(r => r.Outcome == TestOutcome.Passed).Should().Be(3); + } + [ConditionalFact(nameof(NotWindows))] public void Parse_Example1OSXTrx_ParsesCodebaseCorrectly() { @@ -355,4 +383,4 @@ public void Parse_Example1OSXTrx_ParsesFinishTimeCorrectly() var results = TrxParser.Parse(new FileInfo(GetSampleFilePath(Path.Combine("1", "example1_OSX.trx")))); results.CompletedTime.Should().Be(DateTimeOffset.Parse("2017-01-17T10:39:57.1294340-08:00")); } -} \ No newline at end of file +} diff --git a/TrxLib.slnx b/TrxLib.slnx index 0523140..18f741c 100644 --- a/TrxLib.slnx +++ b/TrxLib.slnx @@ -8,6 +8,7 @@ + diff --git a/TrxLib/Counters.cs b/TrxLib/Counters.cs index fe023da..e84ee0d 100644 --- a/TrxLib/Counters.cs +++ b/TrxLib/Counters.cs @@ -1,74 +1,55 @@ -using System.Xml.Serialization; - namespace TrxLib; /// -/// Represents the test result counters from the ResultSummary element of a TRX file. -/// Contains the authoritative vstest-computed counts for each outcome category. +/// Represents the vstest-authoritative test run counters from the ResultSummary element. /// public class Counters { /// Gets or sets the total number of tests. - [XmlAttribute("total")] public int Total { get; set; } /// Gets or sets the number of tests that were executed. - [XmlAttribute("executed")] public int Executed { get; set; } /// Gets or sets the number of tests that passed. - [XmlAttribute("passed")] public int Passed { get; set; } /// Gets or sets the number of tests that failed. - [XmlAttribute("failed")] public int Failed { get; set; } - /// Gets or sets the number of tests that encountered a system error. - [XmlAttribute("error")] + /// Gets or sets the number of tests that produced an error. public int Error { get; set; } /// Gets or sets the number of tests that timed out. - [XmlAttribute("timeout")] public int Timeout { get; set; } /// Gets or sets the number of tests that were aborted. - [XmlAttribute("aborted")] public int Aborted { get; set; } - /// Gets or sets the number of tests with inconclusive results. - [XmlAttribute("inconclusive")] + /// Gets or sets the number of tests that were inconclusive. public int Inconclusive { get; set; } /// Gets or sets the number of tests that passed but the run was aborted. - [XmlAttribute("passedButRunAborted")] public int PassedButRunAborted { get; set; } /// Gets or sets the number of tests that were not runnable. - [XmlAttribute("notRunnable")] public int NotRunnable { get; set; } /// Gets or sets the number of tests that were not executed. - [XmlAttribute("notExecuted")] public int NotExecuted { get; set; } - /// Gets or sets the number of tests that were disconnected. - [XmlAttribute("disconnected")] + /// Gets or sets the number of disconnected tests. public int Disconnected { get; set; } - /// Gets or sets the number of tests with a warning outcome. - [XmlAttribute("warning")] + /// Gets or sets the number of tests that produced warnings. public int Warning { get; set; } /// Gets or sets the number of completed tests. - [XmlAttribute("completed")] public int Completed { get; set; } - /// Gets or sets the number of tests currently in progress. - [XmlAttribute("inProgress")] + /// Gets or sets the number of tests in progress. public int InProgress { get; set; } - /// Gets or sets the number of tests that are pending. - [XmlAttribute("pending")] + /// Gets or sets the number of pending tests. public int Pending { get; set; } } diff --git a/TrxLib/Deployment.cs b/TrxLib/Deployment.cs index ba14d07..20b4836 100644 --- a/TrxLib/Deployment.cs +++ b/TrxLib/Deployment.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -11,6 +9,5 @@ public class Deployment /// /// Gets or sets the root directory path where test run files are deployed. /// - [XmlAttribute("runDeploymentRoot")] public string? RunDeploymentRoot { get; set; } } \ No newline at end of file diff --git a/TrxLib/ErrorInfo.cs b/TrxLib/ErrorInfo.cs index 24bda98..1262300 100644 --- a/TrxLib/ErrorInfo.cs +++ b/TrxLib/ErrorInfo.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -11,13 +9,11 @@ public class ErrorInfo /// /// Gets or sets the error message describing why the test failed. /// - [XmlElement("Message")] public string? Message { get; set; } /// /// Gets or sets the stack trace information from the test failure. /// Contains the call stack at the point of the exception that caused the test to fail. /// - [XmlElement("StackTrace")] public string? StackTrace { get; set; } } diff --git a/TrxLib/Execution.cs b/TrxLib/Execution.cs index c1bdd4b..c5e172e 100644 --- a/TrxLib/Execution.cs +++ b/TrxLib/Execution.cs @@ -1,17 +1,13 @@ -using System.Xml.Serialization; - namespace TrxLib; /// -/// Represents the execution element of a unit test definition in a TRX file. -/// Contains the execution ID that links a test definition to its result. +/// Represents the Execution element inside a UnitTest definition. +/// Links the test definition to its execution record and result. /// public class Execution { /// - /// Gets or sets the execution identifier. Links this test definition to the - /// corresponding UnitTestResult via its executionId attribute. + /// Gets or sets the execution identifier. /// - [XmlAttribute("id")] public string? Id { get; set; } } diff --git a/TrxLib/Output.cs b/TrxLib/Output.cs index 79a2a2f..cb0b004 100644 --- a/TrxLib/Output.cs +++ b/TrxLib/Output.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -12,12 +10,10 @@ public class Output /// Gets or sets the error information if the test failed. /// Contains the error message and stack trace details. /// - [XmlElement("ErrorInfo")] public ErrorInfo? ErrorInfo { get; set; } /// /// Gets or sets the standard output text captured during test execution. /// - [XmlElement("StdOut")] public string? StdOut { get; set; } } diff --git a/TrxLib/ResultSummary.cs b/TrxLib/ResultSummary.cs index 91f1259..7b700db 100644 --- a/TrxLib/ResultSummary.cs +++ b/TrxLib/ResultSummary.cs @@ -1,28 +1,23 @@ -using System.Xml.Serialization; - namespace TrxLib; /// -/// Represents the ResultSummary element of a TRX file. -/// Contains the overall run outcome, authoritative vstest-computed counters, and run-level output. +/// Represents the ResultSummary element of a TRX file, containing the overall +/// outcome and vstest-authoritative test counters for the run. /// public class ResultSummary { /// - /// Gets or sets the overall outcome of the test run (e.g. "Passed", "Failed", "Completed"). + /// Gets or sets the overall outcome of the test run (e.g., "Passed", "Failed"). /// - [XmlAttribute("outcome")] public string? Outcome { get; set; } /// - /// Gets or sets the test result counters for the run. + /// Gets or sets the authoritative test counters computed by vstest. /// - [XmlElement("Counters")] public Counters? Counters { get; set; } /// - /// Gets or sets the run-level output (e.g. run-level stdout written by the test host). + /// Gets or sets the run-level output (e.g., stdout from the test host). /// - [XmlElement("Output")] public Output? Output { get; set; } } diff --git a/TrxLib/Results.cs b/TrxLib/Results.cs index aaec770..1172e0f 100644 --- a/TrxLib/Results.cs +++ b/TrxLib/Results.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -11,6 +9,5 @@ public class Results /// /// Gets or sets the list of unit test results from the test run. /// - [XmlElement("UnitTestResult")] public List? UnitTestResults { get; set; } } diff --git a/TrxLib/TestDefinitions.cs b/TrxLib/TestDefinitions.cs index a3df25f..65ec1b7 100644 --- a/TrxLib/TestDefinitions.cs +++ b/TrxLib/TestDefinitions.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -11,6 +9,5 @@ public class TestDefinitions /// /// Gets or sets the list of unit test definitions. /// - [XmlElement("UnitTest")] public List? UnitTests { get; set; } } diff --git a/TrxLib/TestEntries.cs b/TrxLib/TestEntries.cs index b3cda4e..e203843 100644 --- a/TrxLib/TestEntries.cs +++ b/TrxLib/TestEntries.cs @@ -1,14 +1,10 @@ -using System.Xml.Serialization; - namespace TrxLib; /// -/// Represents the TestEntries element of a TRX file. -/// Contains the execution index linking test IDs to execution IDs and test list categories. +/// Represents the TestEntries element, which indexes test definitions to their execution records. /// public class TestEntries { - /// Gets or sets the individual test entry records. - [XmlElement("TestEntry")] + /// Gets or sets the collection of test entry records. public List? Items { get; set; } } diff --git a/TrxLib/TestEntry.cs b/TrxLib/TestEntry.cs index 584f8ec..6818313 100644 --- a/TrxLib/TestEntry.cs +++ b/TrxLib/TestEntry.cs @@ -1,22 +1,16 @@ -using System.Xml.Serialization; - namespace TrxLib; /// -/// Represents a single TestEntry in the TestEntries section of a TRX file. -/// Links a test definition (testId) to its execution record (executionId) and list category (testListId). +/// Represents a single TestEntry that links a test definition to its execution record. /// public class TestEntry { /// Gets or sets the test definition identifier. - [XmlAttribute("testId")] public string? TestId { get; set; } /// Gets or sets the execution identifier. - [XmlAttribute("executionId")] public string? ExecutionId { get; set; } - /// Gets or sets the test list identifier. - [XmlAttribute("testListId")] + /// Gets or sets the test list identifier this entry belongs to. public string? TestListId { get; set; } } diff --git a/TrxLib/TestList.cs b/TrxLib/TestList.cs index 0e2c2b1..10f7b13 100644 --- a/TrxLib/TestList.cs +++ b/TrxLib/TestList.cs @@ -1,18 +1,13 @@ -using System.Xml.Serialization; - namespace TrxLib; /// -/// Represents a single TestList entry in the TestLists section of a TRX file. -/// vstest always writes two default lists: "Results Not in a List" and "All Loaded Results". +/// Represents a single TestList entry used to categorize test results. /// public class TestList { - /// Gets or sets the display name of the test list. - [XmlAttribute("name")] + /// Gets or sets the name of the test list. public string? Name { get; set; } /// Gets or sets the unique identifier of the test list. - [XmlAttribute("id")] public string? Id { get; set; } } diff --git a/TrxLib/TestLists.cs b/TrxLib/TestLists.cs index 7363b2f..388778b 100644 --- a/TrxLib/TestLists.cs +++ b/TrxLib/TestLists.cs @@ -1,14 +1,10 @@ -using System.Xml.Serialization; - namespace TrxLib; /// -/// Represents the TestLists element of a TRX file. -/// Contains the list categories used to group test results. +/// Represents the TestLists element containing the list categories for a test run. /// public class TestLists { - /// Gets or sets the individual test list entries. - [XmlElement("TestList")] + /// Gets or sets the collection of test lists. public List? Items { get; set; } } diff --git a/TrxLib/TestMethod.cs b/TrxLib/TestMethod.cs index 0466b65..ba7cd31 100644 --- a/TrxLib/TestMethod.cs +++ b/TrxLib/TestMethod.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -11,24 +9,20 @@ public class TestMethod /// /// Gets or sets the path to the assembly containing the test method. /// - [XmlAttribute("codeBase")] public string? CodeBase { get; set; } /// /// Gets or sets the fully qualified name of the class containing the test method. /// - [XmlAttribute("className")] public string? ClassName { get; set; } /// /// Gets or sets the name of the test method. /// - [XmlAttribute("name")] public string? Name { get; set; } /// /// Gets or sets the fully qualified name of the test adapter used to run the test. /// - [XmlAttribute("adapterTypeName")] public string? AdapterTypeName { get; set; } } diff --git a/TrxLib/TestRun.cs b/TrxLib/TestRun.cs index a86241d..2dd9971 100644 --- a/TrxLib/TestRun.cs +++ b/TrxLib/TestRun.cs @@ -1,72 +1,59 @@ -using System.Xml.Serialization; - namespace TrxLib; /// /// Represents the root element of a TRX (Test Results XML) file. /// Contains all test definitions, results, and metadata about the test run. /// -[XmlRoot("TestRun", Namespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010")] public class TestRun { /// /// Gets or sets the collection of test results from the test run. /// - [XmlElement("Results")] public Results? Results { get; set; } /// /// Gets or sets the collection of test definitions used in the test run. /// - [XmlElement("TestDefinitions")] public TestDefinitions? TestDefinitions { get; set; } /// /// Gets or sets the name of the test run. /// - [XmlAttribute("name")] public string? Name { get; set; } /// /// Gets or sets the unique identifier of the test run. /// - [XmlAttribute("id")] public string? Id { get; set; } /// /// Gets or sets the timing information for the test run. /// - [XmlElement("Times")] public Times? Times { get; set; } /// /// Gets or sets the configuration settings used for the test run. /// - [XmlElement("TestSettings")] public TestSettings? TestSettings { get; set; } /// - /// Gets or sets the user account that initiated the test run. + /// Gets or sets the user account that initiated the test run (the runUser attribute). /// - [XmlAttribute("runUser")] public string? RunUser { get; set; } /// /// Gets or sets the result summary for the test run, including the overall outcome /// and authoritative vstest-computed counters. /// - [XmlElement("ResultSummary")] public ResultSummary? ResultSummary { get; set; } /// /// Gets or sets the test list categories used to group results. /// - [XmlElement("TestLists")] public TestLists? TestLists { get; set; } /// /// Gets or sets the test entries index linking test IDs to execution IDs. /// - [XmlElement("TestEntries")] public TestEntries? TestEntries { get; set; } } diff --git a/TrxLib/TestSettings.cs b/TrxLib/TestSettings.cs index 716e41d..bfd38fa 100644 --- a/TrxLib/TestSettings.cs +++ b/TrxLib/TestSettings.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -11,19 +9,16 @@ public class TestSettings /// /// Gets or sets the name of the test settings configuration. /// - [XmlAttribute("name")] public string? Name { get; set; } /// /// Gets or sets the unique identifier of the test settings. /// - [XmlAttribute("id")] public string? Id { get; set; } /// /// Gets or sets the deployment information for the test run. /// Contains details about where test files are deployed. /// - [XmlElement("Deployment")] public Deployment? Deployment { get; set; } } \ No newline at end of file diff --git a/TrxLib/Times.cs b/TrxLib/Times.cs index 873d868..da41144 100644 --- a/TrxLib/Times.cs +++ b/TrxLib/Times.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -11,24 +9,20 @@ public class Times /// /// Gets or sets the timestamp when the test run was created. /// - [XmlAttribute("creation")] public string? Creation { get; set; } /// /// Gets or sets the timestamp when the test run was queued for execution. /// - [XmlAttribute("queuing")] public string? Queuing { get; set; } /// /// Gets or sets the timestamp when the test run started execution. /// - [XmlAttribute("start")] public string? Start { get; set; } /// /// Gets or sets the timestamp when the test run finished execution. /// - [XmlAttribute("finish")] public string? Finish { get; set; } } \ No newline at end of file diff --git a/TrxLib/TrxLib.csproj b/TrxLib/TrxLib.csproj index 7ae2424..ed740ea 100644 --- a/TrxLib/TrxLib.csproj +++ b/TrxLib/TrxLib.csproj @@ -1,7 +1,8 @@  - netstandard2.1 + netstandard2.1;net8.0;net10.0 + true true true true diff --git a/TrxLib/TrxParser.cs b/TrxLib/TrxParser.cs index 4a66e9d..1bba8bb 100644 --- a/TrxLib/TrxParser.cs +++ b/TrxLib/TrxParser.cs @@ -1,4 +1,4 @@ -using System.Xml.Serialization; +using System.Xml.Linq; namespace TrxLib; @@ -7,6 +7,8 @@ namespace TrxLib; /// public class TrxParser { + private static readonly XNamespace TrxNs = XNamespace.Get("http://microsoft.com/schemas/VisualStudio/TeamTest/2010"); + /// /// Parses a TRX file and converts it into a TestResultSet containing structured test results. /// @@ -15,8 +17,7 @@ public class TrxParser public static TestResultSet Parse(FileInfo trxFile) { using var stream = trxFile.OpenRead(); - var serializer = new XmlSerializer(typeof(TestRun)); - TestRun? testRun = serializer.Deserialize(stream) as TestRun; + TestRun? testRun = DeserializeTestRun(stream); if (testRun == null) return new TestResultSet(); @@ -66,19 +67,29 @@ public static TestResultSet Parse(FileInfo trxFile) { if (!string.IsNullOrEmpty(testMethodDomain.ClassName) && !string.IsNullOrEmpty(testMethodDomain.Name)) { - // If Name already contains the full FQTN (starts with ClassName), use Name as-is - // as the base; otherwise build it from ClassName.Name. - var baseFqtn = testMethodDomain.Name.StartsWith(testMethodDomain.ClassName + ".", StringComparison.Ordinal) + string baseFqtn = testMethodDomain.Name.StartsWith(testMethodDomain.ClassName + ".", StringComparison.Ordinal) ? testMethodDomain.Name : $"{testMethodDomain.ClassName}.{testMethodDomain.Name}"; - // Preserve theory-test parameter suffixes: if testName starts with the base - // FQTN and has additional content (e.g. "(param: value)"), use testName - // directly so each parameterized invocation has a unique FQTN. - if (result.TestName != null && result.TestName.StartsWith(baseFqtn, StringComparison.Ordinal)) - fullyQualifiedTestName = result.TestName; - else - fullyQualifiedTestName = baseFqtn; + // For parameterized/theory tests, testName carries the suffix (e.g. "(arg1, arg2)"). + // Extract the short method name and append the suffix from testName if present. + var methodShortName = testMethodDomain.Name.Contains('.') + ? testMethodDomain.Name.Substring(testMethodDomain.Name.LastIndexOf('.') + 1) + : testMethodDomain.Name; + var paramSuffix = string.Empty; + if (!string.IsNullOrEmpty(result.TestName)) + { + string? candidate = null; + + if (result.TestName.StartsWith(baseFqtn, StringComparison.Ordinal)) + candidate = result.TestName.Substring(baseFqtn.Length); + else if (result.TestName.StartsWith(methodShortName, StringComparison.Ordinal)) + candidate = result.TestName.Substring(methodShortName.Length); + + if (candidate?.StartsWith("(", StringComparison.Ordinal) == true) + paramSuffix = candidate; + } + fullyQualifiedTestName = baseFqtn + paramSuffix; } else { @@ -142,8 +153,10 @@ public static TestResultSet Parse(FileInfo trxFile) var testResultSet = new TestResultSet(results) { TestRunName = testRun.Name ?? string.Empty, - TestRunId = testRun.Id ?? string.Empty, TestFilePath = trxFile.FullName, + TestRunId = testRun.Id ?? string.Empty, + DeploymentRoot = testRun.TestSettings?.Deployment?.RunDeploymentRoot ?? string.Empty, + TestSettingsName = testRun.TestSettings?.Name ?? string.Empty, OriginalTestRun = testRun }; @@ -159,4 +172,196 @@ public static TestResultSet Parse(FileInfo trxFile) return testResultSet; } + + private static TestRun? DeserializeTestRun(Stream stream) + { + XDocument doc; + try + { + doc = XDocument.Load(stream); + } + catch (System.Xml.XmlException) + { + return null; + } + + var root = doc.Root; + if (root == null) + return null; + + var testRun = new TestRun + { + Name = (string?)root.Attribute("name"), + Id = (string?)root.Attribute("id"), + RunUser = (string?)root.Attribute("runUser"), + }; + + var timesEl = root.Element(TrxNs + "Times"); + if (timesEl != null) + { + testRun.Times = new Times + { + Creation = (string?)timesEl.Attribute("creation"), + Queuing = (string?)timesEl.Attribute("queuing"), + Start = (string?)timesEl.Attribute("start"), + Finish = (string?)timesEl.Attribute("finish"), + }; + } + + var testSettingsEl = root.Element(TrxNs + "TestSettings"); + if (testSettingsEl != null) + { + testRun.TestSettings = new TestSettings + { + Name = (string?)testSettingsEl.Attribute("name"), + Id = (string?)testSettingsEl.Attribute("id"), + Deployment = testSettingsEl.Element(TrxNs + "Deployment") is XElement deployEl + ? new Deployment { RunDeploymentRoot = (string?)deployEl.Attribute("runDeploymentRoot") } + : null, + }; + } + + var testDefsEl = root.Element(TrxNs + "TestDefinitions"); + if (testDefsEl != null) + { + testRun.TestDefinitions = new TestDefinitions + { + UnitTests = testDefsEl.Elements(TrxNs + "UnitTest").Select(ut => + { + var tmEl = ut.Element(TrxNs + "TestMethod"); + return new UnitTest + { + Id = (string?)ut.Attribute("id"), + Name = (string?)ut.Attribute("name"), + Storage = (string?)ut.Attribute("storage"), + Execution = ut.Element(TrxNs + "Execution") is XElement execEl + ? new Execution { Id = (string?)execEl.Attribute("id") } + : null, + TestMethod = tmEl != null ? new TestMethod + { + CodeBase = (string?)tmEl.Attribute("codeBase"), + ClassName = (string?)tmEl.Attribute("className"), + Name = (string?)tmEl.Attribute("name"), + AdapterTypeName = (string?)tmEl.Attribute("adapterTypeName"), + } : null, + }; + }).ToList(), + }; + } + + var resultsEl = root.Element(TrxNs + "Results"); + if (resultsEl != null) + { + testRun.Results = new Results + { + UnitTestResults = resultsEl.Elements(TrxNs + "UnitTestResult").Select(ur => + { + Output? output = null; + if (ur.Element(TrxNs + "Output") is XElement outputEl) + { + ErrorInfo? errorInfo = null; + if (outputEl.Element(TrxNs + "ErrorInfo") is XElement errorInfoEl) + { + errorInfo = new ErrorInfo + { + Message = (string?)errorInfoEl.Element(TrxNs + "Message"), + StackTrace = (string?)errorInfoEl.Element(TrxNs + "StackTrace"), + }; + } + output = new Output + { + StdOut = (string?)outputEl.Element(TrxNs + "StdOut"), + ErrorInfo = errorInfo, + }; + } + return new UnitTestResult + { + TestId = (string?)ur.Attribute("testId"), + TestName = (string?)ur.Attribute("testName"), + Outcome = (string?)ur.Attribute("outcome"), + StartTime = (string?)ur.Attribute("startTime"), + EndTime = (string?)ur.Attribute("endTime"), + Duration = (string?)ur.Attribute("duration"), + ComputerName = (string?)ur.Attribute("computerName"), + ExecutionId = (string?)ur.Attribute("executionId"), + TestListId = (string?)ur.Attribute("testListId"), + TestType = (string?)ur.Attribute("testType"), + RelativeResultsDirectory = (string?)ur.Attribute("relativeResultsDirectory"), + Output = output, + }; + }).ToList(), + }; + } + + var resultSummaryEl = root.Element(TrxNs + "ResultSummary"); + if (resultSummaryEl != null) + { + Counters? counters = null; + if (resultSummaryEl.Element(TrxNs + "Counters") is XElement countersEl) + { + counters = new Counters + { + Total = (int?)countersEl.Attribute("total") ?? 0, + Executed = (int?)countersEl.Attribute("executed") ?? 0, + Passed = (int?)countersEl.Attribute("passed") ?? 0, + Failed = (int?)countersEl.Attribute("failed") ?? 0, + Error = (int?)countersEl.Attribute("error") ?? 0, + Timeout = (int?)countersEl.Attribute("timeout") ?? 0, + Aborted = (int?)countersEl.Attribute("aborted") ?? 0, + Inconclusive = (int?)countersEl.Attribute("inconclusive") ?? 0, + PassedButRunAborted = (int?)countersEl.Attribute("passedButRunAborted") ?? 0, + NotRunnable = (int?)countersEl.Attribute("notRunnable") ?? 0, + NotExecuted = (int?)countersEl.Attribute("notExecuted") ?? 0, + Disconnected = (int?)countersEl.Attribute("disconnected") ?? 0, + Warning = (int?)countersEl.Attribute("warning") ?? 0, + Completed = (int?)countersEl.Attribute("completed") ?? 0, + InProgress = (int?)countersEl.Attribute("inProgress") ?? 0, + Pending = (int?)countersEl.Attribute("pending") ?? 0, + }; + } + Output? summaryOutput = null; + if (resultSummaryEl.Element(TrxNs + "Output") is XElement summaryOutputEl) + { + summaryOutput = new Output + { + StdOut = (string?)summaryOutputEl.Element(TrxNs + "StdOut"), + }; + } + testRun.ResultSummary = new ResultSummary + { + Outcome = (string?)resultSummaryEl.Attribute("outcome"), + Counters = counters, + Output = summaryOutput, + }; + } + + var testListsEl = root.Element(TrxNs + "TestLists"); + if (testListsEl != null) + { + testRun.TestLists = new TestLists + { + Items = testListsEl.Elements(TrxNs + "TestList").Select(tl => new TestList + { + Name = (string?)tl.Attribute("name"), + Id = (string?)tl.Attribute("id"), + }).ToList(), + }; + } + + var testEntriesEl = root.Element(TrxNs + "TestEntries"); + if (testEntriesEl != null) + { + testRun.TestEntries = new TestEntries + { + Items = testEntriesEl.Elements(TrxNs + "TestEntry").Select(te => new TestEntry + { + TestId = (string?)te.Attribute("testId"), + ExecutionId = (string?)te.Attribute("executionId"), + TestListId = (string?)te.Attribute("testListId"), + }).ToList(), + }; + } + + return testRun; + } } diff --git a/TrxLib/UnitTest.cs b/TrxLib/UnitTest.cs index ad7c4de..3c02707 100644 --- a/TrxLib/UnitTest.cs +++ b/TrxLib/UnitTest.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -12,31 +10,26 @@ public class UnitTest /// Gets or sets the unique identifier of the unit test. /// This ID is referenced by UnitTestResult elements. /// - [XmlAttribute("id")] public string? Id { get; set; } /// /// Gets or sets the name of the unit test. /// - [XmlAttribute("name")] public string? Name { get; set; } /// /// Gets or sets the test method information for this unit test. /// Contains details about the method that implements the test. /// - [XmlElement("TestMethod")] public TestMethod? TestMethod { get; set; } /// /// Gets or sets the path to the test assembly. Stored as a lowercased path by vstest. /// - [XmlAttribute("storage")] public string? Storage { get; set; } /// /// Gets or sets the execution element containing the execution ID for this test definition. /// - [XmlElement("Execution")] public Execution? Execution { get; set; } } diff --git a/TrxLib/UnitTestResult.cs b/TrxLib/UnitTestResult.cs index 05b3a9e..3c26654 100644 --- a/TrxLib/UnitTestResult.cs +++ b/TrxLib/UnitTestResult.cs @@ -1,5 +1,3 @@ -using System.Xml.Serialization; - namespace TrxLib; /// @@ -12,73 +10,61 @@ public class UnitTestResult /// Gets or sets the unique identifier of the test definition referenced by this test result. /// This ID maps to a UnitTest element in the TestDefinitions section. /// - [XmlAttribute("testId")] public string? TestId { get; set; } /// /// Gets or sets the name of the test that was executed. /// - [XmlAttribute("testName")] public string? TestName { get; set; } /// /// Gets or sets the outcome of the test execution. /// Common values include "Passed", "Failed", "NotExecuted", "Inconclusive", "Timeout", and "Pending". /// - [XmlAttribute("outcome")] public string? Outcome { get; set; } /// /// Gets or sets the start time of the test execution in ISO 8601 format. /// - [XmlAttribute("startTime")] public string? StartTime { get; set; } /// /// Gets or sets the end time of the test execution in ISO 8601 format. /// - [XmlAttribute("endTime")] public string? EndTime { get; set; } /// /// Gets or sets the duration of the test execution, typically in the format "hh:mm:ss.fffffff". /// - [XmlAttribute("duration")] public string? Duration { get; set; } /// /// Gets or sets the name of the computer where the test was executed. /// - [XmlAttribute("computerName")] public string? ComputerName { get; set; } /// /// Gets or sets the output information of the test execution, including error information and standard output. /// - [XmlElement("Output")] public Output? Output { get; set; } /// /// Gets or sets the execution identifier. Links this result to its TestEntry and UnitTest/Execution records. /// - [XmlAttribute("executionId")] public string? ExecutionId { get; set; } /// /// Gets or sets the test list identifier. References a TestList in the TestLists section. /// - [XmlAttribute("testListId")] public string? TestListId { get; set; } /// /// Gets or sets the test type GUID identifying the kind of test (e.g. unit test adapter GUID). /// - [XmlAttribute("testType")] public string? TestType { get; set; } /// /// Gets or sets the relative results directory for this test result's attachments. /// - [XmlAttribute("relativeResultsDirectory")] public string? RelativeResultsDirectory { get; set; } } From 5a86342ccca063509aa5e35e3a8aad79a0092c06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 22:09:06 -0700 Subject: [PATCH 02/11] Bump actions/upload-artifact from 6 to 7 (#27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6 to 7.
Release notes

Sourced from actions/upload-artifact's releases.

v7.0.0

v7 What's new

Direct Uploads

Adds support for uploading single files directly (unzipped). Callers can set the new archive parameter to false to skip zipping the file during upload. Right now, we only support single files. The action will fail if the glob passed resolves to multiple files. The name parameter is also ignored with this setting. Instead, the name of the artifact will be the name of the uploaded file.

ESM

To support new versions of the @actions/* packages, we've upgraded the package to ESM.

What's Changed

New Contributors

Full Changelog: https://github.com/actions/upload-artifact/compare/v6...v7.0.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/upload-artifact&package-manager=github_actions&previous-version=6&new-version=7)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) You can trigger a rebase of this PR by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
> **Note** > Automatic rebases have been disabled on this pull request as it has been open for over 30 days. Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2c42a98..03ade4c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -37,7 +37,7 @@ jobs: - name: Upload Artifacts if: startsWith(github.ref, 'refs/tags/v') - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: NuGet path: ${{ github.workspace }}/PackedNuget From 0419ec43475c0f98e403e3be797aab9e8416a9a8 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 22:10:27 -0700 Subject: [PATCH 03/11] fix: Workflow does not contain permissions (#44) Potential fix for [https://github.com/BenjaminMichaelis/TrxLib/security/code-scanning/3](https://github.com/BenjaminMichaelis/TrxLib/security/code-scanning/3) Add an explicit `permissions` block for the `build-and-test` job in `.github/workflows/build-and-test.yml`, with the minimum required scope: - `contents: read` This is the best fix because it preserves existing behavior (checkout/build/test still work) while ensuring `GITHUB_TOKEN` cannot get unintended write privileges from repo/org defaults. Edit only the `build-and-test` job section, directly under `runs-on`. _Suggested fixes powered by Copilot Autofix. Review carefully before merging._ Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .github/workflows/build-and-test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 397afc2..38490ac 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -33,6 +33,8 @@ jobs: use-github-auto-merge: true build-and-test: runs-on: ${{ matrix.os }} + permissions: + contents: read strategy: matrix: From 2bb80533b6bd8ef0886b780625c101c85a7ec66d Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 22:15:54 -0700 Subject: [PATCH 04/11] test: add failing regression tests for 2 known bugs Bug 3 - vstest outcomes Error/Aborted/NotRunnable silently coerced to NotExecuted by catch-all in TrxParser. Three tests cover the three most impactful missing outcomes. Bug 4 - Test project directory derived by hardcoded 3-level upward traversal from codeBase DLL path. Breaks for RID-qualified output paths (bin/Debug/net8.0//Foo.dll) where 4 levels are required. Note: Bugs 1 (TestResultSet convenience fields) and 2 (UnitTestResult missing executionId/testListId/relativeResultsDirectory) were already fixed in origin/main before this branch was rebased. --- TrxLib.Tests/KnownBugsTests.cs | 161 +++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 TrxLib.Tests/KnownBugsTests.cs diff --git a/TrxLib.Tests/KnownBugsTests.cs b/TrxLib.Tests/KnownBugsTests.cs new file mode 100644 index 0000000..5649267 --- /dev/null +++ b/TrxLib.Tests/KnownBugsTests.cs @@ -0,0 +1,161 @@ +using AwesomeAssertions; + +namespace TrxLib.Tests; + +/// +/// Tests that document currently known bugs found by comparing our object model to the +/// upstream vstest TRX ObjectModel (commit ba0077af). +/// Every test in this file is expected to FAIL until the corresponding bug is fixed. +/// +public class KnownBugsTests +{ + // ------------------------------------------------------------------------- + // Bug 3 – Outcomes defined by vstest (Error, Aborted, NotRunnable, Disconnected, + // Warning, Completed, InProgress, PassedButRunAborted) are absent from + // local TestOutcome enum. The parser's catch-all arm (TrxParser.cs:52) + // maps every unrecognised string to NotExecuted, causing silent + // misclassification of distinct failure modes. + // ------------------------------------------------------------------------- + + [Fact] + public void Bug_Parse_ErrorOutcome_IsNotSilentlyMappedToNotExecuted() + { + // vstest writes outcome="Error" when the test adapter itself crashes or the + // test process aborts unexpectedly. This is NOT the same as "not executed". + using var trxFile = new TempTrxFile(MinimalTrxWithOutcome("Error")); + var results = TrxParser.Parse(trxFile.FileInfo); + + // FAILS: TrxParser catch-all maps "error" → TestOutcome.NotExecuted (line 51). + results.Single().Outcome.Should().NotBe(TestOutcome.NotExecuted, + because: "outcome=\"Error\" is a distinct vstest state and must not be coerced to NotExecuted"); + } + + [Fact] + public void Bug_Parse_AbortedOutcome_IsNotSilentlyMappedToNotExecuted() + { + // vstest writes outcome="Aborted" when the framework (not the user) terminates + // the test mid-execution. + using var trxFile = new TempTrxFile(MinimalTrxWithOutcome("Aborted")); + var results = TrxParser.Parse(trxFile.FileInfo); + + // FAILS: TrxParser catch-all maps "aborted" → TestOutcome.NotExecuted (line 51). + results.Single().Outcome.Should().NotBe(TestOutcome.NotExecuted, + because: "outcome=\"Aborted\" is a distinct vstest state and must not be coerced to NotExecuted"); + } + + [Fact] + public void Bug_Parse_NotRunnableOutcome_IsNotSilentlyMappedToNotExecuted() + { + // vstest writes outcome="NotRunnable" when ITestElement.IsRunnable == false. + using var trxFile = new TempTrxFile(MinimalTrxWithOutcome("NotRunnable")); + var results = TrxParser.Parse(trxFile.FileInfo); + + // FAILS: TrxParser catch-all maps "notrunnable" → TestOutcome.NotExecuted (line 51). + results.Single().Outcome.Should().NotBe(TestOutcome.NotExecuted, + because: "outcome=\"NotRunnable\" is a distinct vstest state and must not be coerced to NotExecuted"); + } + + // ------------------------------------------------------------------------- + // Bug 4 – Test project directory is derived by a hardcoded 3-level upward + // traversal from the codeBase DLL path (TrxParser.cs:96-101). + // This breaks for .NET 8+ RID-qualified output layouts where the DLL + // is 4 levels below the project root: + // /bin/Debug/net8.0//Foo.dll + // ------------------------------------------------------------------------- + + [Fact] + public void Bug_Parse_TestProjectDirectory_IsCorrectForRidQualifiedOutputPath() + { + // Represents: /bin/Debug/net8.0/win-x64/MyProject.dll + // Going up 3 levels lands on bin/ — one level too shallow. + var projectRoot = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "fake_project_rid")); + var codebase = Path.Combine(projectRoot, "bin", "Debug", "net8.0", "win-x64", "MyProject.dll"); + + using var trxFile = new TempTrxFile(MinimalTrxWithCodebase(codebase)); + var results = TrxParser.Parse(trxFile.FileInfo); + + // FAILS: hardcoded 3-level traversal returns bin/ instead of the project root. + results.Single().TestProjectDirectory!.FullName.TrimEnd(Path.DirectorySeparatorChar) + .Should().Be(projectRoot.TrimEnd(Path.DirectorySeparatorChar)); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private static string MinimalTrxWithOutcome(string outcome) + { + const string testId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; + const string execId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; + return $""" + + + + + + + + + + + + """; + } + + private static string MinimalTrxWithCodebase(string codebasePath) + { + const string testId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; + const string execId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; + // XML attribute value must use forward slashes to avoid escaping issues + var codebaseXml = codebasePath.Replace('\\', '/'); + return $""" + + + + + + + + + + + + """; + } + + /// Helper that writes a TRX string to a temp file and deletes it on dispose. + private sealed class TempTrxFile : IDisposable + { + public FileInfo FileInfo { get; } + + public TempTrxFile(string content) + { + var path = Path.Combine(Path.GetTempPath(), $"trxtest_{Guid.NewGuid():N}.trx"); + File.WriteAllText(path, content); + FileInfo = new FileInfo(path); + } + + public void Dispose() => FileInfo.Delete(); + } +} From cb12d82a464357554219e09081536a67e10f99d7 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 22:38:07 -0700 Subject: [PATCH 05/11] fix: resolve Bug 3 (outcome coercion) and Bug 4 (directory heuristic) Bug 3 - Add 8 missing vstest TestOutcome enum values (Error, Aborted, NotRunnable, Disconnected, Warning, Completed, InProgress, PassedButRunAborted) to TestOutcome.cs and add corresponding switch arms in TrxParser.cs. Also map null outcome attribute to TestOutcome.Error: vstest omits the outcome= attribute when outcome is Error because Error is ordinal 0 (the enum default) and XmlPersistence.SaveSimpleField skips writing attributes whose value equals the default. Bug 4 - Replace hardcoded 3-level upward traversal with a 'bin-anchor' heuristic: walk up until reaching the 'bin' directory, then return its parent. Correctly handles all standard .NET SDK output layouts including RID-qualified (depth 4) and publish (depth 4/5) paths. Test updates: - Strengthen Bug 3 assertions from NotBe(NotExecuted) to Be(exact value) - Add Bug_Parse_MissingOutcomeAttribute_MapsToError (the real-world form of the Error outcome in vstest-generated TRX files) - Add Bug_Parse_TestProjectDirectory_IsCorrectForPublishOutputPath - Add Bug_Parse_TestProjectDirectory_IsCorrectForRidPlusPublishOutputPath All 59 tests pass. Research validated by GPT-5.5 tracing the vstest source (commit ba0077af): Converter.ToOutcome(), XmlPersistence.SaveSimpleField(), TrxLogger.cs. --- TrxLib.Tests/KnownBugsTests.cs | 89 +++++++++++++++++++++++++++++++--- TrxLib/TestOutcome.cs | 45 ++++++++++++++++- TrxLib/TrxParser.cs | 44 ++++++++++++----- 3 files changed, 158 insertions(+), 20 deletions(-) diff --git a/TrxLib.Tests/KnownBugsTests.cs b/TrxLib.Tests/KnownBugsTests.cs index 5649267..f613833 100644 --- a/TrxLib.Tests/KnownBugsTests.cs +++ b/TrxLib.Tests/KnownBugsTests.cs @@ -25,9 +25,9 @@ public void Bug_Parse_ErrorOutcome_IsNotSilentlyMappedToNotExecuted() using var trxFile = new TempTrxFile(MinimalTrxWithOutcome("Error")); var results = TrxParser.Parse(trxFile.FileInfo); - // FAILS: TrxParser catch-all maps "error" → TestOutcome.NotExecuted (line 51). - results.Single().Outcome.Should().NotBe(TestOutcome.NotExecuted, - because: "outcome=\"Error\" is a distinct vstest state and must not be coerced to NotExecuted"); + // FAILS: TrxParser catch-all maps "error" → TestOutcome.NotExecuted (line 52). + results.Single().Outcome.Should().Be(TestOutcome.Error, + because: "outcome=\"Error\" is a distinct vstest state indicating a system error"); } [Fact] @@ -38,8 +38,8 @@ public void Bug_Parse_AbortedOutcome_IsNotSilentlyMappedToNotExecuted() using var trxFile = new TempTrxFile(MinimalTrxWithOutcome("Aborted")); var results = TrxParser.Parse(trxFile.FileInfo); - // FAILS: TrxParser catch-all maps "aborted" → TestOutcome.NotExecuted (line 51). - results.Single().Outcome.Should().NotBe(TestOutcome.NotExecuted, + // FAILS: TrxParser catch-all maps "aborted" → TestOutcome.NotExecuted (line 52). + results.Single().Outcome.Should().Be(TestOutcome.Aborted, because: "outcome=\"Aborted\" is a distinct vstest state and must not be coerced to NotExecuted"); } @@ -50,11 +50,26 @@ public void Bug_Parse_NotRunnableOutcome_IsNotSilentlyMappedToNotExecuted() using var trxFile = new TempTrxFile(MinimalTrxWithOutcome("NotRunnable")); var results = TrxParser.Parse(trxFile.FileInfo); - // FAILS: TrxParser catch-all maps "notrunnable" → TestOutcome.NotExecuted (line 51). - results.Single().Outcome.Should().NotBe(TestOutcome.NotExecuted, + // FAILS: TrxParser catch-all maps "notrunnable" → TestOutcome.NotExecuted (line 52). + results.Single().Outcome.Should().Be(TestOutcome.NotRunnable, because: "outcome=\"NotRunnable\" is a distinct vstest state and must not be coerced to NotExecuted"); } + [Fact] + public void Bug_Parse_MissingOutcomeAttribute_MapsToError() + { + // In vstest's serialization, TestOutcome.Error == 0 is the enum default. + // XmlPersistence.SaveSimpleField() skips writing the attribute when value == default, + // so a real TRX file with an attachment error has NO outcome= attribute on + // . A missing attribute is the live form of the Error outcome. + using var trxFile = new TempTrxFile(MinimalTrxWithoutOutcome()); + var results = TrxParser.Parse(trxFile.FileInfo); + + // FAILS: null result.Outcome hits catch-all → TestOutcome.NotExecuted instead of TestOutcome.Error. + results.Single().Outcome.Should().Be(TestOutcome.Error, + because: "a missing outcome attribute means TestOutcome.Error in vstest's serialization model"); + } + // ------------------------------------------------------------------------- // Bug 4 – Test project directory is derived by a hardcoded 3-level upward // traversal from the codeBase DLL path (TrxParser.cs:96-101). @@ -79,10 +94,70 @@ public void Bug_Parse_TestProjectDirectory_IsCorrectForRidQualifiedOutputPath() .Should().Be(projectRoot.TrimEnd(Path.DirectorySeparatorChar)); } + [Fact] + public void Bug_Parse_TestProjectDirectory_IsCorrectForPublishOutputPath() + { + // Represents: /bin/Release/net8.0/publish/MyProject.dll + // Going up 3 levels lands on bin/ — one level too shallow. + var projectRoot = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "fake_project_publish")); + var codebase = Path.Combine(projectRoot, "bin", "Release", "net8.0", "publish", "MyProject.dll"); + + using var trxFile = new TempTrxFile(MinimalTrxWithCodebase(codebase)); + var results = TrxParser.Parse(trxFile.FileInfo); + + // FAILS: hardcoded 3-level traversal returns bin/ instead of the project root. + results.Single().TestProjectDirectory!.FullName.TrimEnd(Path.DirectorySeparatorChar) + .Should().Be(projectRoot.TrimEnd(Path.DirectorySeparatorChar)); + } + + [Fact] + public void Bug_Parse_TestProjectDirectory_IsCorrectForRidPlusPublishOutputPath() + { + // Represents: /bin/Release/net8.0/linux-x64/publish/MyProject.dll + // Going up 3 levels lands on Release/ — two levels too shallow. + var projectRoot = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "fake_project_rid_publish")); + var codebase = Path.Combine(projectRoot, "bin", "Release", "net8.0", "linux-x64", "publish", "MyProject.dll"); + + using var trxFile = new TempTrxFile(MinimalTrxWithCodebase(codebase)); + var results = TrxParser.Parse(trxFile.FileInfo); + + // FAILS: hardcoded 3-level traversal returns Release/ instead of the project root. + results.Single().TestProjectDirectory!.FullName.TrimEnd(Path.DirectorySeparatorChar) + .Should().Be(projectRoot.TrimEnd(Path.DirectorySeparatorChar)); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- + private static string MinimalTrxWithoutOutcome() + { + const string testId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; + const string execId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; + return $""" + + + + + + + + + + + + """; + } + private static string MinimalTrxWithOutcome(string outcome) { const string testId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; diff --git a/TrxLib/TestOutcome.cs b/TrxLib/TestOutcome.cs index f94c4b6..a9509b1 100644 --- a/TrxLib/TestOutcome.cs +++ b/TrxLib/TestOutcome.cs @@ -33,5 +33,48 @@ public enum TestOutcome /// /// The test is awaiting execution or further action. /// - Pending + Pending, + + /// + /// A system error occurred during test execution (e.g., failure to copy result attachments). + /// In TRX files written by vstest, this outcome is represented by the absence of the + /// outcome attribute on <UnitTestResult> (Error is the enum default). + /// + Error, + + /// + /// The test was aborted by the framework (not by a user gesture). + /// + Aborted, + + /// + /// The test could not be run because ITestElement.IsRunnable is false. + /// + NotRunnable, + + /// + /// The test run was disconnected before it finished. + /// + Disconnected, + + /// + /// The test produced a warning-level result. Typically a run-level outcome. + /// + Warning, + + /// + /// The test completed but no qualitative measure of completeness was established. + /// Typically a run-level outcome. + /// + Completed, + + /// + /// The test is currently in progress. + /// + InProgress, + + /// + /// The test passed but the run was aborted before all tests completed. + /// + PassedButRunAborted, } diff --git a/TrxLib/TrxParser.cs b/TrxLib/TrxParser.cs index 1bba8bb..56d14cc 100644 --- a/TrxLib/TrxParser.cs +++ b/TrxLib/TrxParser.cs @@ -43,13 +43,25 @@ public static TestResultSet Parse(FileInfo trxFile) var outcome = result.Outcome?.ToLowerInvariant() switch { - "passed" => TestOutcome.Passed, - "failed" => TestOutcome.Failed, - "notexecuted" => TestOutcome.NotExecuted, - "inconclusive" => TestOutcome.Inconclusive, - "timeout" => TestOutcome.Timeout, - "pending" => TestOutcome.Pending, - _ => TestOutcome.NotExecuted + "passed" => TestOutcome.Passed, + "failed" => TestOutcome.Failed, + "notexecuted" => TestOutcome.NotExecuted, + "inconclusive" => TestOutcome.Inconclusive, + "timeout" => TestOutcome.Timeout, + "pending" => TestOutcome.Pending, + "error" => TestOutcome.Error, + "aborted" => TestOutcome.Aborted, + "notrunnable" => TestOutcome.NotRunnable, + "disconnected" => TestOutcome.Disconnected, + "warning" => TestOutcome.Warning, + "completed" => TestOutcome.Completed, + "inprogress" => TestOutcome.InProgress, + "passedbutrunaborted" => TestOutcome.PassedButRunAborted, + // A null/absent outcome attribute means Error in vstest's serialization: + // TestOutcome.Error is ordinal 0 (the enum default), so XmlPersistence + // omits the attribute when outcome == Error. + null => TestOutcome.Error, + _ => TestOutcome.NotExecuted }; DateTimeOffset? resultStartTime = null, resultEndTime = null; @@ -108,17 +120,25 @@ public static TestResultSet Parse(FileInfo trxFile) codebaseFile = new FileInfo(testMethodDomain.CodeBase); } - // Calculate test project directory from codebase if available + // Calculate test project directory from codebase if available. + // Walk up to the 'bin' folder, then take its parent — this handles + // all standard .NET SDK output layouts: + // bin/{config}/{tfm}/ (depth 3 — classic) + // bin/{config}/{tfm}/{rid}/ (depth 4 — self-contained) + // bin/{config}/{tfm}/publish/ (depth 4 — publish output) + // bin/{config}/{tfm}/{rid}/publish/ (depth 5 — self-contained publish) DirectoryInfo? testProjectDirectory = null; - if (codebaseFile != null && codebaseFile.Directory != null) + if (codebaseFile?.Directory != null) { - // Go up 3 levels to get the project directory (bin/Debug/netcoreappX.X/) var dir = codebaseFile.Directory; - for (int i = 0; i < 3 && dir?.Parent != null; i++) + while (dir?.Parent != null && !string.Equals(dir.Name, "bin", StringComparison.OrdinalIgnoreCase)) { dir = dir.Parent; } - testProjectDirectory = dir; + // dir is now the 'bin' folder (or root if no 'bin' found); its parent is the project root. + testProjectDirectory = string.Equals(dir?.Name, "bin", StringComparison.OrdinalIgnoreCase) + ? dir!.Parent + : dir; } // Extract StdOut if available From 41de1a2664b2f399159930a9d8034f4474a8bddb Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 23:13:37 -0700 Subject: [PATCH 06/11] refactor: DRY up KnownBugsTests and remove stale comments - Consolidate MinimalTrxWithOutcome/MinimalTrxWithoutOutcome/MinimalTrxWithCodebase into a single MinimalTrx(string? outcome = null, string codeBase = 'test.dll') factory - Extract AssertProjectRootResolves helper; collapse 3 directory test bodies to one-liners - Remove all '// FAILS: ...' and 'going up 3 levels' comments (bugs are fixed) - Update class summary and section comments to describe current correct behavior instead of the old broken behavior --- TrxLib.Tests/KnownBugsTests.cs | 148 +++++++-------------------------- 1 file changed, 28 insertions(+), 120 deletions(-) diff --git a/TrxLib.Tests/KnownBugsTests.cs b/TrxLib.Tests/KnownBugsTests.cs index f613833..9d3d804 100644 --- a/TrxLib.Tests/KnownBugsTests.cs +++ b/TrxLib.Tests/KnownBugsTests.cs @@ -3,29 +3,26 @@ namespace TrxLib.Tests; /// -/// Tests that document currently known bugs found by comparing our object model to the -/// upstream vstest TRX ObjectModel (commit ba0077af). -/// Every test in this file is expected to FAIL until the corresponding bug is fixed. +/// Regression tests for bugs found by comparing our object model to the +/// upstream vstest TRX ObjectModel. /// public class KnownBugsTests { // ------------------------------------------------------------------------- - // Bug 3 – Outcomes defined by vstest (Error, Aborted, NotRunnable, Disconnected, - // Warning, Completed, InProgress, PassedButRunAborted) are absent from - // local TestOutcome enum. The parser's catch-all arm (TrxParser.cs:52) - // maps every unrecognised string to NotExecuted, causing silent - // misclassification of distinct failure modes. + // Bug 3 – All vstest outcome strings (Error, Aborted, NotRunnable, Disconnected, + // Warning, Completed, InProgress, PassedButRunAborted) must round-trip + // correctly through the parser. A missing outcome attribute must map + // to TestOutcome.Error, not NotExecuted. // ------------------------------------------------------------------------- [Fact] public void Bug_Parse_ErrorOutcome_IsNotSilentlyMappedToNotExecuted() { // vstest writes outcome="Error" when the test adapter itself crashes or the - // test process aborts unexpectedly. This is NOT the same as "not executed". - using var trxFile = new TempTrxFile(MinimalTrxWithOutcome("Error")); + // test process aborts unexpectedly. This is NOT the same as "not executed". + using var trxFile = new TempTrxFile(MinimalTrx(outcome: "Error")); var results = TrxParser.Parse(trxFile.FileInfo); - // FAILS: TrxParser catch-all maps "error" → TestOutcome.NotExecuted (line 52). results.Single().Outcome.Should().Be(TestOutcome.Error, because: "outcome=\"Error\" is a distinct vstest state indicating a system error"); } @@ -35,10 +32,9 @@ public void Bug_Parse_AbortedOutcome_IsNotSilentlyMappedToNotExecuted() { // vstest writes outcome="Aborted" when the framework (not the user) terminates // the test mid-execution. - using var trxFile = new TempTrxFile(MinimalTrxWithOutcome("Aborted")); + using var trxFile = new TempTrxFile(MinimalTrx(outcome: "Aborted")); var results = TrxParser.Parse(trxFile.FileInfo); - // FAILS: TrxParser catch-all maps "aborted" → TestOutcome.NotExecuted (line 52). results.Single().Outcome.Should().Be(TestOutcome.Aborted, because: "outcome=\"Aborted\" is a distinct vstest state and must not be coerced to NotExecuted"); } @@ -47,10 +43,9 @@ public void Bug_Parse_AbortedOutcome_IsNotSilentlyMappedToNotExecuted() public void Bug_Parse_NotRunnableOutcome_IsNotSilentlyMappedToNotExecuted() { // vstest writes outcome="NotRunnable" when ITestElement.IsRunnable == false. - using var trxFile = new TempTrxFile(MinimalTrxWithOutcome("NotRunnable")); + using var trxFile = new TempTrxFile(MinimalTrx(outcome: "NotRunnable")); var results = TrxParser.Parse(trxFile.FileInfo); - // FAILS: TrxParser catch-all maps "notrunnable" → TestOutcome.NotExecuted (line 52). results.Single().Outcome.Should().Be(TestOutcome.NotRunnable, because: "outcome=\"NotRunnable\" is a distinct vstest state and must not be coerced to NotExecuted"); } @@ -62,66 +57,37 @@ public void Bug_Parse_MissingOutcomeAttribute_MapsToError() // XmlPersistence.SaveSimpleField() skips writing the attribute when value == default, // so a real TRX file with an attachment error has NO outcome= attribute on // . A missing attribute is the live form of the Error outcome. - using var trxFile = new TempTrxFile(MinimalTrxWithoutOutcome()); + using var trxFile = new TempTrxFile(MinimalTrx()); var results = TrxParser.Parse(trxFile.FileInfo); - // FAILS: null result.Outcome hits catch-all → TestOutcome.NotExecuted instead of TestOutcome.Error. results.Single().Outcome.Should().Be(TestOutcome.Error, because: "a missing outcome attribute means TestOutcome.Error in vstest's serialization model"); } // ------------------------------------------------------------------------- - // Bug 4 – Test project directory is derived by a hardcoded 3-level upward - // traversal from the codeBase DLL path (TrxParser.cs:96-101). - // This breaks for .NET 8+ RID-qualified output layouts where the DLL - // is 4 levels below the project root: - // /bin/Debug/net8.0//Foo.dll + // Bug 4 – TestProjectDirectory must be resolved correctly for all standard + // .NET SDK output layouts, including RID-qualified and publish + // subdirectories nested under bin/. // ------------------------------------------------------------------------- [Fact] - public void Bug_Parse_TestProjectDirectory_IsCorrectForRidQualifiedOutputPath() - { - // Represents: /bin/Debug/net8.0/win-x64/MyProject.dll - // Going up 3 levels lands on bin/ — one level too shallow. - var projectRoot = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "fake_project_rid")); - var codebase = Path.Combine(projectRoot, "bin", "Debug", "net8.0", "win-x64", "MyProject.dll"); - - using var trxFile = new TempTrxFile(MinimalTrxWithCodebase(codebase)); - var results = TrxParser.Parse(trxFile.FileInfo); - - // FAILS: hardcoded 3-level traversal returns bin/ instead of the project root. - results.Single().TestProjectDirectory!.FullName.TrimEnd(Path.DirectorySeparatorChar) - .Should().Be(projectRoot.TrimEnd(Path.DirectorySeparatorChar)); - } + public void Bug_Parse_TestProjectDirectory_IsCorrectForRidQualifiedOutputPath() => + AssertProjectRootResolves("fake_project_rid", "bin", "Debug", "net8.0", "win-x64", "MyProject.dll"); [Fact] - public void Bug_Parse_TestProjectDirectory_IsCorrectForPublishOutputPath() - { - // Represents: /bin/Release/net8.0/publish/MyProject.dll - // Going up 3 levels lands on bin/ — one level too shallow. - var projectRoot = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "fake_project_publish")); - var codebase = Path.Combine(projectRoot, "bin", "Release", "net8.0", "publish", "MyProject.dll"); - - using var trxFile = new TempTrxFile(MinimalTrxWithCodebase(codebase)); - var results = TrxParser.Parse(trxFile.FileInfo); - - // FAILS: hardcoded 3-level traversal returns bin/ instead of the project root. - results.Single().TestProjectDirectory!.FullName.TrimEnd(Path.DirectorySeparatorChar) - .Should().Be(projectRoot.TrimEnd(Path.DirectorySeparatorChar)); - } + public void Bug_Parse_TestProjectDirectory_IsCorrectForPublishOutputPath() => + AssertProjectRootResolves("fake_project_publish", "bin", "Release", "net8.0", "publish", "MyProject.dll"); [Fact] - public void Bug_Parse_TestProjectDirectory_IsCorrectForRidPlusPublishOutputPath() - { - // Represents: /bin/Release/net8.0/linux-x64/publish/MyProject.dll - // Going up 3 levels lands on Release/ — two levels too shallow. - var projectRoot = Path.GetFullPath(Path.Combine(Path.GetTempPath(), "fake_project_rid_publish")); - var codebase = Path.Combine(projectRoot, "bin", "Release", "net8.0", "linux-x64", "publish", "MyProject.dll"); + public void Bug_Parse_TestProjectDirectory_IsCorrectForRidPlusPublishOutputPath() => + AssertProjectRootResolves("fake_project_rid_publish", "bin", "Release", "net8.0", "linux-x64", "publish", "MyProject.dll"); - using var trxFile = new TempTrxFile(MinimalTrxWithCodebase(codebase)); + private static void AssertProjectRootResolves(string subfolder, params string[] relativeSegments) + { + var projectRoot = Path.GetFullPath(Path.Combine(Path.GetTempPath(), subfolder)); + var codebase = Path.Combine(new[] { projectRoot }.Concat(relativeSegments).ToArray()); + using var trxFile = new TempTrxFile(MinimalTrx(codeBase: codebase)); var results = TrxParser.Parse(trxFile.FileInfo); - - // FAILS: hardcoded 3-level traversal returns Release/ instead of the project root. results.Single().TestProjectDirectory!.FullName.TrimEnd(Path.DirectorySeparatorChar) .Should().Be(projectRoot.TrimEnd(Path.DirectorySeparatorChar)); } @@ -130,69 +96,11 @@ public void Bug_Parse_TestProjectDirectory_IsCorrectForRidPlusPublishOutputPath( // Helpers // ------------------------------------------------------------------------- - private static string MinimalTrxWithoutOutcome() - { - const string testId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; - const string execId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; - return $""" - - - - - - - - - - - - """; - } - - private static string MinimalTrxWithOutcome(string outcome) - { - const string testId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; - const string execId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; - return $""" - - - - - - - - - - - - """; - } - - private static string MinimalTrxWithCodebase(string codebasePath) + private static string MinimalTrx(string? outcome = null, string codeBase = "test.dll") { const string testId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"; const string execId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"; - // XML attribute value must use forward slashes to avoid escaping issues - var codebaseXml = codebasePath.Replace('\\', '/'); + var codebaseXml = codeBase.Replace('\\', '/'); return $""" From 717dc24aa19c958dee26610aa5f6bd7a25e78944 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 23:17:01 -0700 Subject: [PATCH 07/11] refactor: remove ephemeral bug numbering from tests and comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename KnownBugsTests -> TrxParserRegressionTests (class + file) - Drop 'Bug_' prefix from all 7 test method names - Replace 'Bug 3 –' / 'Bug 4 –' section headings with descriptive topic labels ('Outcome parsing' / 'Directory resolution') --- .upstream-vstest-objectmodel/TestMethod.cs | 89 +++ .upstream-vstest-objectmodel/TestOutcome.cs | 90 +++ .upstream-vstest-objectmodel/TestResult.cs | 535 ++++++++++++++++++ .upstream-vstest-objectmodel/TestRun.cs | 197 +++++++ .../UnitTestElement.cs | 88 +++ .../UnitTestResult.cs | 27 + ...gsTests.cs => TrxParserRegressionTests.cs} | 33 +- 7 files changed, 1042 insertions(+), 17 deletions(-) create mode 100644 .upstream-vstest-objectmodel/TestMethod.cs create mode 100644 .upstream-vstest-objectmodel/TestOutcome.cs create mode 100644 .upstream-vstest-objectmodel/TestResult.cs create mode 100644 .upstream-vstest-objectmodel/TestRun.cs create mode 100644 .upstream-vstest-objectmodel/UnitTestElement.cs create mode 100644 .upstream-vstest-objectmodel/UnitTestResult.cs rename TrxLib.Tests/{KnownBugsTests.cs => TrxParserRegressionTests.cs} (82%) diff --git a/.upstream-vstest-objectmodel/TestMethod.cs b/.upstream-vstest-objectmodel/TestMethod.cs new file mode 100644 index 0000000..01e74c1 --- /dev/null +++ b/.upstream-vstest-objectmodel/TestMethod.cs @@ -0,0 +1,89 @@ +// 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.Xml; + +using Microsoft.TestPlatform.Extensions.TrxLogger.XML; +using Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger; + +namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; + +/// +/// TestMethod contains information about a unit test method that needs to be executed +/// +internal sealed class TestMethod : IXmlTestStore +{ + public TestMethod(string name, string className) + { + TPDebug.Assert(!name.IsNullOrEmpty(), "name is null"); + TPDebug.Assert(!className.IsNullOrEmpty(), "className is null"); + Name = name; + ClassName = className; + } + + /// + /// Gets the name. + /// + public string Name { get; } + + /// + /// Gets the class name. + /// + public string ClassName { get; } + + /// + /// Gets or sets a value indicating whether is valid. + /// + public bool IsValid { get; set; } + + #region Override + + /// + /// Override function for Equals. + /// + /// + /// The object to compare. + /// + /// + /// The . + /// + public override bool Equals(object? obj) + { + return obj is TestMethod otherTestMethod && Name == otherTestMethod.Name + && ClassName == otherTestMethod.ClassName && IsValid == otherTestMethod.IsValid; + } + + /// + /// Override function for GetHashCode. + /// + /// + /// The . + /// + public override int GetHashCode() + { + return Name?.GetHashCode() ?? 0; + } + + #endregion Override + + #region IXmlTestStore Members + + /// + /// Saves the class under the XmlElement.. + /// + /// + /// The parent xml. + /// + /// + /// The parameter + /// + public void Save(XmlElement element, XmlTestStoreParameters? parameters) + { + XmlPersistence helper = new(); + helper.SaveSimpleField(element, "@className", ClassName, string.Empty); + helper.SaveSimpleField(element, "@name", Name, string.Empty); + helper.SaveSimpleField(element, "isValid", IsValid, false); + } + + #endregion +} diff --git a/.upstream-vstest-objectmodel/TestOutcome.cs b/.upstream-vstest-objectmodel/TestOutcome.cs new file mode 100644 index 0000000..9091d8f --- /dev/null +++ b/.upstream-vstest-objectmodel/TestOutcome.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; + +/// +/// Outcome of a test or a run. +/// If a new successful state needs to be added you will need to modify +/// RunResultAndStatistics in TestRun.cs and TestOutcomeHelper below. +/// ---------------------------------------------------------------- +/// NOTE: the order is important and is used for computing outcome for aggregations. +/// More important outcomes come first. See TestOutcomeHelper.GetAggregationOutcome. +/// +internal enum TestOutcome +{ + /// + /// There was a system error while we were trying to execute a test. + /// + Error, + + /// + /// Test was executed, but there were issues. + /// Issues may involve exceptions or failed assertions. + /// + Failed, + + /// + /// The test timed out + /// + Timeout, + + /// + /// Test was aborted. + /// This was not caused by a user gesture, but rather by a framework decision. + /// + Aborted, + + /// + /// Test has completed, but we can't say if it passed or failed. + /// May be used for aborted tests... + /// + Inconclusive, + + /// + /// Test was executed w/o any issues, but run was aborted. + /// + PassedButRunAborted, + + /// + /// Test had it chance for been executed but was not, as ITestElement.IsRunnable == false. + /// + NotRunnable, + + /// + /// Test was not executed. + /// This was caused by a user gesture - e.g. user hit stop button. + /// + NotExecuted, + + /// + /// Test run was disconnected before it finished running. + /// + Disconnected, + + /// + /// To be used by Run level results. + /// This is not a failure. + /// + Warning, + + /// + /// Test was executed w/o any issues. + /// + Passed, + + /// + /// Test has completed, but there is no qualitative measure of completeness. + /// + Completed, + + /// + /// Test is currently executing. + /// + InProgress, + + /// + /// Test is in the execution queue, was not started yet. + /// + Pending +} diff --git a/.upstream-vstest-objectmodel/TestResult.cs b/.upstream-vstest-objectmodel/TestResult.cs new file mode 100644 index 0000000..339ad7a --- /dev/null +++ b/.upstream-vstest-objectmodel/TestResult.cs @@ -0,0 +1,535 @@ +// 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; + +/// +/// Class to uniquely identify test results +/// +internal sealed class TestResultId : IXmlTestStore +{ + private readonly Guid _runId; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The run id. + /// + /// + /// The execution id. + /// + /// + /// The parent execution id. + /// + /// + /// The test id. + /// + public TestResultId(Guid runId, Guid executionId, Guid parentExecutionId, Guid testId) + { + _runId = runId; + ExecutionId = executionId; + ParentExecutionId = parentExecutionId; + TestId = testId; + } + + /// + /// Gets the execution id. + /// + public Guid ExecutionId { get; } + + /// + /// Gets the parent execution id. + /// + public Guid ParentExecutionId { get; } + + /// + /// Gets the test id. + /// + public Guid TestId { get; } + + + #region Overrides + + /// + /// Override function for Equals + /// + /// + /// The object to compare + /// + /// + /// The . + /// + public override bool Equals(object? obj) + { + return obj is TestResultId tmpId && _runId.Equals(tmpId._runId) && ExecutionId.Equals((object)tmpId.ExecutionId); + } + + /// + /// Override function for GetHashCode. + /// + /// + /// The . + /// + public override int GetHashCode() + { + return _runId.GetHashCode() ^ ExecutionId.GetHashCode(); + } + + /// + /// Override function for ToString. + /// + /// + /// The . + /// + public override string ToString() + { + return ExecutionId.ToString("B"); + } + #endregion + + #region IXmlTestStore Members + + /// + /// Saves the class under the XmlElement.. + /// + /// + /// The parent xml. + /// + /// + /// The parameter + /// + 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 +} + +/// +/// The test result error info class. +/// +internal sealed class TestResultErrorInfo : IXmlTestStore +{ + [StoreXmlSimpleField("Message", "")] + private string? _message; + + [StoreXmlSimpleField("StackTrace", "")] + private string? _stackTrace; + + + /// + /// Gets or sets the message. + /// + public string? Message + { + get { return _message; } + set { _message = value; } + } + + /// + /// Gets or sets the stack trace. + /// + public string? StackTrace + { + get { return _stackTrace; } + set { _stackTrace = value; } + } + + #region IXmlTestStore Members + + /// + /// Saves the class under the XmlElement.. + /// + /// + /// The parent xml. + /// + /// + /// The parameter + /// + public void Save(System.Xml.XmlElement element, XmlTestStoreParameters? parameters) + { + XmlPersistence.SaveUsingReflection(element, this, typeof(TestResultErrorInfo), parameters); + } + + #endregion +} + +/// +/// Class for test result. +/// +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; + + /// + /// Paths to test result files, relative to the test results folder, sorted in increasing order + /// + private readonly SortedList _resultFiles = new(StringComparer.OrdinalIgnoreCase); + + /// + /// Information provided by data collectors for the test case + /// + private readonly List _collectorDataEntries = new(); + + /// + /// Initializes a new instance of the class. + /// + /// + /// The computer name. + /// + /// + /// The run id. + /// + /// Execution id. + /// Parent execution id. + /// Result name. + /// + /// The test id. + /// + /// + /// The outcome. + /// + /// + /// + /// + 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; + } + + /// + /// Gets or sets the end time. + /// + public DateTime EndTime { get; set; } + + /// + /// Gets or sets the start time. + /// + public DateTime StartTime { get; set; } + + /// + /// Gets or sets the duration. + /// + 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; + } + } + + /// + /// Gets the computer name. + /// + public string ComputerName { get; } + + /// + /// Gets or sets the outcome. + /// + public TestOutcome Outcome { get; set; } + + + /// + /// Gets or sets the id. + /// + public TestResultId Id { get; internal set; } + + /// + /// Gets or sets the error message. + /// + public string ErrorMessage + { + get { return _errorInfo?.Message ?? string.Empty; } + set + { + _errorInfo ??= new TestResultErrorInfo(); + + _errorInfo.Message = value; + } + } + + /// + /// Gets or sets the error stack trace. + /// + public string ErrorStackTrace + { + get { return _errorInfo?.StackTrace ?? string.Empty; } + + set + { + _errorInfo ??= new TestResultErrorInfo(); + + _errorInfo.StackTrace = value; + } + } + + /// + /// Gets the text messages. + /// + /// + /// Additional information messages from TestTextResultMessage, e.g. generated by TestOutcome.WriteLine. + /// Avoid using this property in the following way: for (int i=0; i<prop.Length; i++) { ... prop[i] ...} + /// + [NotNull] + public string[]? TextMessages + { + get { return (string[])_textMessages.ToArray(typeof(string)); } + + set + { + if (value != null) + _textMessages = new ArrayList(value); + else + _textMessages.Clear(); + } + } + + /// + /// Gets or sets the standard out. + /// + public string StdOut + { + get { return _stdOut ?? string.Empty; } + set { _stdOut = value; } + } + + /// + /// Gets or sets the standard err. + /// + public string StdErr + { + get { return _stdErr ?? string.Empty; } + set { _stdErr = value; } + } + + /// + /// Gets or sets the debug trace. + /// + public string DebugTrace + { + get { return _debugTrace ?? string.Empty; } + set { _debugTrace = value; } + } + + /// + /// Gets the path to the test results directory + /// + public string TestResultsDirectory + { + get + { + if (_testRun == null) + { + Debug.Fail("'m_testRun' is null"); + throw new InvalidOperationException(TrxLoggerResources.Common_MissingRunInResult); + } + + return _testRun.GetResultFilesDirectory(this); + } + } + + /// + /// Gets the directory containing the test result files, relative to the root results directory + /// + public string RelativeTestResultsDirectory { get; } + + /// + /// Gets or sets the data row info. + /// + public int DataRowInfo { get; set; } + + /// + /// Gets or sets the result type. + /// + 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 + + /// + /// Helper function to add a text message info to the test result + /// + /// Message to be added + public void AddTextMessage(string text) + { + EqtAssert.ParameterNotNull(text, nameof(text)); + _textMessages.Add(text); + } + + /// + /// Sets the test run the test was executed in + /// + /// The test run the test was executed in + internal virtual void SetTestRun(TestRun testRun) + { + TPDebug.Assert(testRun != null, "'testRun' is null"); + _testRun = testRun; + } + + /// + /// Adds result files to the collection + /// + /// Paths to the result files + internal void AddResultFiles(IEnumerable 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; + } + } + + /// + /// Adds collector data entries to the collection + /// + /// The collector data entry to add + internal void AddCollectorDataEntries(IEnumerable 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 + + /// + /// Saves the class under the XmlElement.. + /// + /// + /// The parent xml. + /// + /// + /// The parameter + /// + 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 +} diff --git a/.upstream-vstest-objectmodel/TestRun.cs b/.upstream-vstest-objectmodel/TestRun.cs new file mode 100644 index 0000000..4bae23d --- /dev/null +++ b/.upstream-vstest-objectmodel/TestRun.cs @@ -0,0 +1,197 @@ +// 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.Diagnostics; +using System.Globalization; +using System.IO; +using System.Security.Principal; + +using Microsoft.TestPlatform.Extensions.TrxLogger.Utility; +using Microsoft.TestPlatform.Extensions.TrxLogger.XML; + +using TrxLoggerResources = Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger.Resources.TrxResource; + +namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; + +/// +/// Class having information about a test run. +/// +internal sealed class TestRun +{ + // These fields will be valid when the test run summary is loaded from a results file. + // The summary fields need to be first in the class so they get serialized first. When we + // read the summary we don't want to parse the XML tags for other fields because they can + // be quite large. + // + // When reading the results file, the summary is considered complete when all summary fields + // are non-null. Any new summary fields that are initialized in the constructor should be + // placed before the last non-initialized field. + // + // The summary parsing code is in XmlTestReader.ReadTestRunSummary. + [StoreXmlSimpleField("@id")] + private readonly Guid _id; + + [StoreXmlSimpleField("@name")] + private string _name; + + [StoreXmlSimpleField("@runUser", "")] + private readonly string _runUser; + + private TestRunConfiguration? _runConfig; + + [StoreXmlSimpleField("Times/@creation")] + private readonly DateTime _created; + + [StoreXmlSimpleField("Times/@queuing")] + private readonly DateTime _queued; + + [StoreXmlSimpleField("Times/@start")] + private DateTime _started; + + [StoreXmlSimpleField("Times/@finish")] + private DateTime _finished; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The run id. + /// + internal TestRun(Guid runId) + { + _id = Guid.NewGuid(); + _name = string.Format(CultureInfo.CurrentCulture, TrxLoggerResources.Common_TestRunName, Environment.GetEnvironmentVariable("UserName"), Environment.MachineName, FormatDateTimeForRunName(DateTime.Now)); + + // Fix for issue (https://github.com/Microsoft/vstest/issues/213). Since there is no way to find current user in linux machine. + // We are catching PlatformNotSupportedException for non windows machine. + try + { + _runUser = WindowsIdentity.GetCurrent().Name; + } + catch (PlatformNotSupportedException) + { + _runUser = string.Empty; + } + _created = DateTime.UtcNow; + _queued = DateTime.UtcNow; + _started = DateTime.UtcNow; + _finished = DateTime.UtcNow; + + EqtAssert.IsTrue(!Guid.Empty.Equals(runId), "Can't use Guid.Empty for run ID."); + _id = runId; + } + + /// + /// Gets or sets the run configuration. + /// + internal TestRunConfiguration? RunConfiguration + { + get + { + return _runConfig; + } + + set + { + EqtAssert.ParameterNotNull(value, "RunConfiguration"); + _runConfig = value; + } + } + + /// + /// Gets or sets the start time. + /// + internal DateTime Started + { + get + { + return _started; + } + + set + { + _started = value; + } + } + + /// + /// Gets or sets the finished time of Test run. + /// + internal DateTime Finished + { + get { return _finished; } + set { _finished = value; } + } + + /// + /// Gets or sets the name. + /// + internal string Name + { + get + { + return _name; + } + + set + { + EqtAssert.StringNotNullOrEmpty(value, "Name"); + _name = value; + } + } + + /// + /// Gets the id. + /// + internal Guid Id + { + get { return _id; } + } + + /// + /// WARNING: do not use from inside Test Adapters, use from only on HA by UI etc. + /// Returns directory on HA for dependent files for TestResult. XmlPersistence method for UI. + /// Throws on error (e.g. if deployment directory was not set for test run). + /// + /// + /// Test Result to get dependent files directory for. + /// + /// + /// Result directory. + /// + internal string GetResultFilesDirectory(TestResult result) + { + EqtAssert.ParameterNotNull(result, nameof(result)); + return Path.Combine(GetResultsDirectory(), result.RelativeTestResultsDirectory); + } + + /// + /// Gets the results directory, which is the run deployment In directory + /// + /// The results directory + /// This method is called by public properties/methods, so it needs to throw on error + internal string GetResultsDirectory() + { + if (RunConfiguration == null) + { + Debug.Fail("'RunConfiguration' is null"); + throw new Exception(TrxLoggerResources.Common_MissingRunConfigInRun); + } + + if (string.IsNullOrEmpty(RunConfiguration.RunDeploymentRootDirectory)) + { + Debug.Fail("'RunConfiguration.RunDeploymentRootDirectory' is null or empty"); + throw new Exception(TrxLoggerResources.Common_MissingRunDeploymentRootInRunConfig); + } + + return RunConfiguration.RunDeploymentInDirectory; + } + + private static string FormatDateTimeForRunName(DateTime timeStamp) + { + // We use custom format string to make sure that runs are sorted in the same way on all intl machines. + // This is both for directory names and for Data Warehouse. + return timeStamp.ToString("yyyy-MM-dd HH:mm:ss", DateTimeFormatInfo.InvariantInfo); + } +} diff --git a/.upstream-vstest-objectmodel/UnitTestElement.cs b/.upstream-vstest-objectmodel/UnitTestElement.cs new file mode 100644 index 0000000..ae4084b --- /dev/null +++ b/.upstream-vstest-objectmodel/UnitTestElement.cs @@ -0,0 +1,88 @@ +// 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 Microsoft.TestPlatform.Extensions.TrxLogger.Utility; + +using Microsoft.TestPlatform.Extensions.TrxLogger.XML; +using Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger; + +namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; + +/// +/// Unit test element. +/// +internal class UnitTestElement : TestElement, IXmlTestStoreCustom +{ + private string? _codeBase; + + public UnitTestElement( + Guid id, + string name, + string adapter, + TestMethod testMethod) : base(id, name, adapter) + { + TPDebug.Assert(!string.IsNullOrEmpty(adapter), "adapter is null"); + TPDebug.Assert(testMethod != null, "testMethod is null"); + TPDebug.Assert(testMethod != null && testMethod.ClassName != null, "className is null"); + + TestMethod = testMethod; + } + + string IXmlTestStoreCustom.ElementName + { + get { return Constants.UnitTestElementName; } + } + + string? IXmlTestStoreCustom.NamespaceUri + { + get { return null; } + } + + /// + /// Gets the test type. + /// + public override TestType TestType + { + get { return Constants.UnitTestType; } + } + + /// + /// Gets the test method. + /// + public TestMethod TestMethod { get; } + + /// + /// Gets or sets the storage. + /// + public string? CodeBase + { + get { return _codeBase; } + + set + { + EqtAssert.StringNotNullOrEmpty(value, "CodeBase"); + _codeBase = value; + } + } + + /// + /// Saves the class under the XmlElement.. + /// + /// + /// The parent xml. + /// + /// + /// The parameter + /// + public override void Save(System.Xml.XmlElement element, XmlTestStoreParameters? parameters) + { + base.Save(element, parameters); + XmlPersistence h = new(); + + h.SaveSimpleField(element, "TestMethod/@codeBase", _codeBase, string.Empty); + h.SaveSimpleField(element, "TestMethod/@adapterTypeName", _adapter, string.Empty); + h.SaveObject(TestMethod, element, "TestMethod", parameters); + } +} diff --git a/.upstream-vstest-objectmodel/UnitTestResult.cs b/.upstream-vstest-objectmodel/UnitTestResult.cs new file mode 100644 index 0000000..be0b0e1 --- /dev/null +++ b/.upstream-vstest-objectmodel/UnitTestResult.cs @@ -0,0 +1,27 @@ +// 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 Microsoft.TestPlatform.Extensions.TrxLogger.Utility; + +namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; + +/// +/// Class for unit test result. +/// +internal class UnitTestResult : TestResultAggregation +{ + public UnitTestResult( + Guid runId, + Guid testId, + Guid executionId, + Guid parentExecutionId, + string resultName, + string computerName, + TestOutcome outcome, + TestType testType, + TestListCategoryId testCategoryId, + TrxFileHelper trxFileHelper + ) : base(runId, testId, executionId, parentExecutionId, resultName, computerName, outcome, testType, testCategoryId, trxFileHelper) { } +} diff --git a/TrxLib.Tests/KnownBugsTests.cs b/TrxLib.Tests/TrxParserRegressionTests.cs similarity index 82% rename from TrxLib.Tests/KnownBugsTests.cs rename to TrxLib.Tests/TrxParserRegressionTests.cs index 9d3d804..3bd815d 100644 --- a/TrxLib.Tests/KnownBugsTests.cs +++ b/TrxLib.Tests/TrxParserRegressionTests.cs @@ -3,20 +3,19 @@ namespace TrxLib.Tests; /// -/// Regression tests for bugs found by comparing our object model to the -/// upstream vstest TRX ObjectModel. +/// Regression tests verifying TRX parsing correctness against the vstest object model. /// -public class KnownBugsTests +public class TrxParserRegressionTests { // ------------------------------------------------------------------------- - // Bug 3 – All vstest outcome strings (Error, Aborted, NotRunnable, Disconnected, - // Warning, Completed, InProgress, PassedButRunAborted) must round-trip - // correctly through the parser. A missing outcome attribute must map - // to TestOutcome.Error, not NotExecuted. + // Outcome parsing – All vstest outcome strings (Error, Aborted, NotRunnable, + // Disconnected, Warning, Completed, InProgress, PassedButRunAborted) must + // round-trip correctly through the parser. A missing outcome attribute must + // map to TestOutcome.Error, not NotExecuted. // ------------------------------------------------------------------------- [Fact] - public void Bug_Parse_ErrorOutcome_IsNotSilentlyMappedToNotExecuted() + public void Parse_ErrorOutcome_IsNotSilentlyMappedToNotExecuted() { // vstest writes outcome="Error" when the test adapter itself crashes or the // test process aborts unexpectedly. This is NOT the same as "not executed". @@ -28,7 +27,7 @@ public void Bug_Parse_ErrorOutcome_IsNotSilentlyMappedToNotExecuted() } [Fact] - public void Bug_Parse_AbortedOutcome_IsNotSilentlyMappedToNotExecuted() + public void Parse_AbortedOutcome_IsNotSilentlyMappedToNotExecuted() { // vstest writes outcome="Aborted" when the framework (not the user) terminates // the test mid-execution. @@ -40,7 +39,7 @@ public void Bug_Parse_AbortedOutcome_IsNotSilentlyMappedToNotExecuted() } [Fact] - public void Bug_Parse_NotRunnableOutcome_IsNotSilentlyMappedToNotExecuted() + public void Parse_NotRunnableOutcome_IsNotSilentlyMappedToNotExecuted() { // vstest writes outcome="NotRunnable" when ITestElement.IsRunnable == false. using var trxFile = new TempTrxFile(MinimalTrx(outcome: "NotRunnable")); @@ -51,7 +50,7 @@ public void Bug_Parse_NotRunnableOutcome_IsNotSilentlyMappedToNotExecuted() } [Fact] - public void Bug_Parse_MissingOutcomeAttribute_MapsToError() + public void Parse_MissingOutcomeAttribute_MapsToError() { // In vstest's serialization, TestOutcome.Error == 0 is the enum default. // XmlPersistence.SaveSimpleField() skips writing the attribute when value == default, @@ -65,21 +64,21 @@ public void Bug_Parse_MissingOutcomeAttribute_MapsToError() } // ------------------------------------------------------------------------- - // Bug 4 – TestProjectDirectory must be resolved correctly for all standard - // .NET SDK output layouts, including RID-qualified and publish - // subdirectories nested under bin/. + // Directory resolution – TestProjectDirectory must be resolved correctly for + // all standard .NET SDK output layouts, including RID-qualified and publish + // subdirectories nested under bin/. // ------------------------------------------------------------------------- [Fact] - public void Bug_Parse_TestProjectDirectory_IsCorrectForRidQualifiedOutputPath() => + public void Parse_TestProjectDirectory_IsCorrectForRidQualifiedOutputPath() => AssertProjectRootResolves("fake_project_rid", "bin", "Debug", "net8.0", "win-x64", "MyProject.dll"); [Fact] - public void Bug_Parse_TestProjectDirectory_IsCorrectForPublishOutputPath() => + public void Parse_TestProjectDirectory_IsCorrectForPublishOutputPath() => AssertProjectRootResolves("fake_project_publish", "bin", "Release", "net8.0", "publish", "MyProject.dll"); [Fact] - public void Bug_Parse_TestProjectDirectory_IsCorrectForRidPlusPublishOutputPath() => + public void Parse_TestProjectDirectory_IsCorrectForRidPlusPublishOutputPath() => AssertProjectRootResolves("fake_project_rid_publish", "bin", "Release", "net8.0", "linux-x64", "publish", "MyProject.dll"); private static void AssertProjectRootResolves(string subfolder, params string[] relativeSegments) From 185e0797af5b75b99efe624b0284d26c610c7f37 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 23:17:16 -0700 Subject: [PATCH 08/11] chore: remove stale upstream reference files These were downloaded locally during the vstest audit and should not be committed to the repository. --- .upstream-vstest-objectmodel/TestMethod.cs | 89 --- .upstream-vstest-objectmodel/TestOutcome.cs | 90 --- .upstream-vstest-objectmodel/TestResult.cs | 535 ------------------ .upstream-vstest-objectmodel/TestRun.cs | 197 ------- .../UnitTestElement.cs | 88 --- .../UnitTestResult.cs | 27 - 6 files changed, 1026 deletions(-) delete mode 100644 .upstream-vstest-objectmodel/TestMethod.cs delete mode 100644 .upstream-vstest-objectmodel/TestOutcome.cs delete mode 100644 .upstream-vstest-objectmodel/TestResult.cs delete mode 100644 .upstream-vstest-objectmodel/TestRun.cs delete mode 100644 .upstream-vstest-objectmodel/UnitTestElement.cs delete mode 100644 .upstream-vstest-objectmodel/UnitTestResult.cs diff --git a/.upstream-vstest-objectmodel/TestMethod.cs b/.upstream-vstest-objectmodel/TestMethod.cs deleted file mode 100644 index 01e74c1..0000000 --- a/.upstream-vstest-objectmodel/TestMethod.cs +++ /dev/null @@ -1,89 +0,0 @@ -// 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.Xml; - -using Microsoft.TestPlatform.Extensions.TrxLogger.XML; -using Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger; - -namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; - -/// -/// TestMethod contains information about a unit test method that needs to be executed -/// -internal sealed class TestMethod : IXmlTestStore -{ - public TestMethod(string name, string className) - { - TPDebug.Assert(!name.IsNullOrEmpty(), "name is null"); - TPDebug.Assert(!className.IsNullOrEmpty(), "className is null"); - Name = name; - ClassName = className; - } - - /// - /// Gets the name. - /// - public string Name { get; } - - /// - /// Gets the class name. - /// - public string ClassName { get; } - - /// - /// Gets or sets a value indicating whether is valid. - /// - public bool IsValid { get; set; } - - #region Override - - /// - /// Override function for Equals. - /// - /// - /// The object to compare. - /// - /// - /// The . - /// - public override bool Equals(object? obj) - { - return obj is TestMethod otherTestMethod && Name == otherTestMethod.Name - && ClassName == otherTestMethod.ClassName && IsValid == otherTestMethod.IsValid; - } - - /// - /// Override function for GetHashCode. - /// - /// - /// The . - /// - public override int GetHashCode() - { - return Name?.GetHashCode() ?? 0; - } - - #endregion Override - - #region IXmlTestStore Members - - /// - /// Saves the class under the XmlElement.. - /// - /// - /// The parent xml. - /// - /// - /// The parameter - /// - public void Save(XmlElement element, XmlTestStoreParameters? parameters) - { - XmlPersistence helper = new(); - helper.SaveSimpleField(element, "@className", ClassName, string.Empty); - helper.SaveSimpleField(element, "@name", Name, string.Empty); - helper.SaveSimpleField(element, "isValid", IsValid, false); - } - - #endregion -} diff --git a/.upstream-vstest-objectmodel/TestOutcome.cs b/.upstream-vstest-objectmodel/TestOutcome.cs deleted file mode 100644 index 9091d8f..0000000 --- a/.upstream-vstest-objectmodel/TestOutcome.cs +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; - -/// -/// Outcome of a test or a run. -/// If a new successful state needs to be added you will need to modify -/// RunResultAndStatistics in TestRun.cs and TestOutcomeHelper below. -/// ---------------------------------------------------------------- -/// NOTE: the order is important and is used for computing outcome for aggregations. -/// More important outcomes come first. See TestOutcomeHelper.GetAggregationOutcome. -/// -internal enum TestOutcome -{ - /// - /// There was a system error while we were trying to execute a test. - /// - Error, - - /// - /// Test was executed, but there were issues. - /// Issues may involve exceptions or failed assertions. - /// - Failed, - - /// - /// The test timed out - /// - Timeout, - - /// - /// Test was aborted. - /// This was not caused by a user gesture, but rather by a framework decision. - /// - Aborted, - - /// - /// Test has completed, but we can't say if it passed or failed. - /// May be used for aborted tests... - /// - Inconclusive, - - /// - /// Test was executed w/o any issues, but run was aborted. - /// - PassedButRunAborted, - - /// - /// Test had it chance for been executed but was not, as ITestElement.IsRunnable == false. - /// - NotRunnable, - - /// - /// Test was not executed. - /// This was caused by a user gesture - e.g. user hit stop button. - /// - NotExecuted, - - /// - /// Test run was disconnected before it finished running. - /// - Disconnected, - - /// - /// To be used by Run level results. - /// This is not a failure. - /// - Warning, - - /// - /// Test was executed w/o any issues. - /// - Passed, - - /// - /// Test has completed, but there is no qualitative measure of completeness. - /// - Completed, - - /// - /// Test is currently executing. - /// - InProgress, - - /// - /// Test is in the execution queue, was not started yet. - /// - Pending -} diff --git a/.upstream-vstest-objectmodel/TestResult.cs b/.upstream-vstest-objectmodel/TestResult.cs deleted file mode 100644 index 339ad7a..0000000 --- a/.upstream-vstest-objectmodel/TestResult.cs +++ /dev/null @@ -1,535 +0,0 @@ -// 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; - -/// -/// Class to uniquely identify test results -/// -internal sealed class TestResultId : IXmlTestStore -{ - private readonly Guid _runId; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The run id. - /// - /// - /// The execution id. - /// - /// - /// The parent execution id. - /// - /// - /// The test id. - /// - public TestResultId(Guid runId, Guid executionId, Guid parentExecutionId, Guid testId) - { - _runId = runId; - ExecutionId = executionId; - ParentExecutionId = parentExecutionId; - TestId = testId; - } - - /// - /// Gets the execution id. - /// - public Guid ExecutionId { get; } - - /// - /// Gets the parent execution id. - /// - public Guid ParentExecutionId { get; } - - /// - /// Gets the test id. - /// - public Guid TestId { get; } - - - #region Overrides - - /// - /// Override function for Equals - /// - /// - /// The object to compare - /// - /// - /// The . - /// - public override bool Equals(object? obj) - { - return obj is TestResultId tmpId && _runId.Equals(tmpId._runId) && ExecutionId.Equals((object)tmpId.ExecutionId); - } - - /// - /// Override function for GetHashCode. - /// - /// - /// The . - /// - public override int GetHashCode() - { - return _runId.GetHashCode() ^ ExecutionId.GetHashCode(); - } - - /// - /// Override function for ToString. - /// - /// - /// The . - /// - public override string ToString() - { - return ExecutionId.ToString("B"); - } - #endregion - - #region IXmlTestStore Members - - /// - /// Saves the class under the XmlElement.. - /// - /// - /// The parent xml. - /// - /// - /// The parameter - /// - 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 -} - -/// -/// The test result error info class. -/// -internal sealed class TestResultErrorInfo : IXmlTestStore -{ - [StoreXmlSimpleField("Message", "")] - private string? _message; - - [StoreXmlSimpleField("StackTrace", "")] - private string? _stackTrace; - - - /// - /// Gets or sets the message. - /// - public string? Message - { - get { return _message; } - set { _message = value; } - } - - /// - /// Gets or sets the stack trace. - /// - public string? StackTrace - { - get { return _stackTrace; } - set { _stackTrace = value; } - } - - #region IXmlTestStore Members - - /// - /// Saves the class under the XmlElement.. - /// - /// - /// The parent xml. - /// - /// - /// The parameter - /// - public void Save(System.Xml.XmlElement element, XmlTestStoreParameters? parameters) - { - XmlPersistence.SaveUsingReflection(element, this, typeof(TestResultErrorInfo), parameters); - } - - #endregion -} - -/// -/// Class for test result. -/// -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; - - /// - /// Paths to test result files, relative to the test results folder, sorted in increasing order - /// - private readonly SortedList _resultFiles = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Information provided by data collectors for the test case - /// - private readonly List _collectorDataEntries = new(); - - /// - /// Initializes a new instance of the class. - /// - /// - /// The computer name. - /// - /// - /// The run id. - /// - /// Execution id. - /// Parent execution id. - /// Result name. - /// - /// The test id. - /// - /// - /// The outcome. - /// - /// - /// - /// - 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; - } - - /// - /// Gets or sets the end time. - /// - public DateTime EndTime { get; set; } - - /// - /// Gets or sets the start time. - /// - public DateTime StartTime { get; set; } - - /// - /// Gets or sets the duration. - /// - 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; - } - } - - /// - /// Gets the computer name. - /// - public string ComputerName { get; } - - /// - /// Gets or sets the outcome. - /// - public TestOutcome Outcome { get; set; } - - - /// - /// Gets or sets the id. - /// - public TestResultId Id { get; internal set; } - - /// - /// Gets or sets the error message. - /// - public string ErrorMessage - { - get { return _errorInfo?.Message ?? string.Empty; } - set - { - _errorInfo ??= new TestResultErrorInfo(); - - _errorInfo.Message = value; - } - } - - /// - /// Gets or sets the error stack trace. - /// - public string ErrorStackTrace - { - get { return _errorInfo?.StackTrace ?? string.Empty; } - - set - { - _errorInfo ??= new TestResultErrorInfo(); - - _errorInfo.StackTrace = value; - } - } - - /// - /// Gets the text messages. - /// - /// - /// Additional information messages from TestTextResultMessage, e.g. generated by TestOutcome.WriteLine. - /// Avoid using this property in the following way: for (int i=0; i<prop.Length; i++) { ... prop[i] ...} - /// - [NotNull] - public string[]? TextMessages - { - get { return (string[])_textMessages.ToArray(typeof(string)); } - - set - { - if (value != null) - _textMessages = new ArrayList(value); - else - _textMessages.Clear(); - } - } - - /// - /// Gets or sets the standard out. - /// - public string StdOut - { - get { return _stdOut ?? string.Empty; } - set { _stdOut = value; } - } - - /// - /// Gets or sets the standard err. - /// - public string StdErr - { - get { return _stdErr ?? string.Empty; } - set { _stdErr = value; } - } - - /// - /// Gets or sets the debug trace. - /// - public string DebugTrace - { - get { return _debugTrace ?? string.Empty; } - set { _debugTrace = value; } - } - - /// - /// Gets the path to the test results directory - /// - public string TestResultsDirectory - { - get - { - if (_testRun == null) - { - Debug.Fail("'m_testRun' is null"); - throw new InvalidOperationException(TrxLoggerResources.Common_MissingRunInResult); - } - - return _testRun.GetResultFilesDirectory(this); - } - } - - /// - /// Gets the directory containing the test result files, relative to the root results directory - /// - public string RelativeTestResultsDirectory { get; } - - /// - /// Gets or sets the data row info. - /// - public int DataRowInfo { get; set; } - - /// - /// Gets or sets the result type. - /// - 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 - - /// - /// Helper function to add a text message info to the test result - /// - /// Message to be added - public void AddTextMessage(string text) - { - EqtAssert.ParameterNotNull(text, nameof(text)); - _textMessages.Add(text); - } - - /// - /// Sets the test run the test was executed in - /// - /// The test run the test was executed in - internal virtual void SetTestRun(TestRun testRun) - { - TPDebug.Assert(testRun != null, "'testRun' is null"); - _testRun = testRun; - } - - /// - /// Adds result files to the collection - /// - /// Paths to the result files - internal void AddResultFiles(IEnumerable 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; - } - } - - /// - /// Adds collector data entries to the collection - /// - /// The collector data entry to add - internal void AddCollectorDataEntries(IEnumerable 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 - - /// - /// Saves the class under the XmlElement.. - /// - /// - /// The parent xml. - /// - /// - /// The parameter - /// - 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 -} diff --git a/.upstream-vstest-objectmodel/TestRun.cs b/.upstream-vstest-objectmodel/TestRun.cs deleted file mode 100644 index 4bae23d..0000000 --- a/.upstream-vstest-objectmodel/TestRun.cs +++ /dev/null @@ -1,197 +0,0 @@ -// 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.Diagnostics; -using System.Globalization; -using System.IO; -using System.Security.Principal; - -using Microsoft.TestPlatform.Extensions.TrxLogger.Utility; -using Microsoft.TestPlatform.Extensions.TrxLogger.XML; - -using TrxLoggerResources = Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger.Resources.TrxResource; - -namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; - -/// -/// Class having information about a test run. -/// -internal sealed class TestRun -{ - // These fields will be valid when the test run summary is loaded from a results file. - // The summary fields need to be first in the class so they get serialized first. When we - // read the summary we don't want to parse the XML tags for other fields because they can - // be quite large. - // - // When reading the results file, the summary is considered complete when all summary fields - // are non-null. Any new summary fields that are initialized in the constructor should be - // placed before the last non-initialized field. - // - // The summary parsing code is in XmlTestReader.ReadTestRunSummary. - [StoreXmlSimpleField("@id")] - private readonly Guid _id; - - [StoreXmlSimpleField("@name")] - private string _name; - - [StoreXmlSimpleField("@runUser", "")] - private readonly string _runUser; - - private TestRunConfiguration? _runConfig; - - [StoreXmlSimpleField("Times/@creation")] - private readonly DateTime _created; - - [StoreXmlSimpleField("Times/@queuing")] - private readonly DateTime _queued; - - [StoreXmlSimpleField("Times/@start")] - private DateTime _started; - - [StoreXmlSimpleField("Times/@finish")] - private DateTime _finished; - - /// - /// Initializes a new instance of the class. - /// - /// - /// The run id. - /// - internal TestRun(Guid runId) - { - _id = Guid.NewGuid(); - _name = string.Format(CultureInfo.CurrentCulture, TrxLoggerResources.Common_TestRunName, Environment.GetEnvironmentVariable("UserName"), Environment.MachineName, FormatDateTimeForRunName(DateTime.Now)); - - // Fix for issue (https://github.com/Microsoft/vstest/issues/213). Since there is no way to find current user in linux machine. - // We are catching PlatformNotSupportedException for non windows machine. - try - { - _runUser = WindowsIdentity.GetCurrent().Name; - } - catch (PlatformNotSupportedException) - { - _runUser = string.Empty; - } - _created = DateTime.UtcNow; - _queued = DateTime.UtcNow; - _started = DateTime.UtcNow; - _finished = DateTime.UtcNow; - - EqtAssert.IsTrue(!Guid.Empty.Equals(runId), "Can't use Guid.Empty for run ID."); - _id = runId; - } - - /// - /// Gets or sets the run configuration. - /// - internal TestRunConfiguration? RunConfiguration - { - get - { - return _runConfig; - } - - set - { - EqtAssert.ParameterNotNull(value, "RunConfiguration"); - _runConfig = value; - } - } - - /// - /// Gets or sets the start time. - /// - internal DateTime Started - { - get - { - return _started; - } - - set - { - _started = value; - } - } - - /// - /// Gets or sets the finished time of Test run. - /// - internal DateTime Finished - { - get { return _finished; } - set { _finished = value; } - } - - /// - /// Gets or sets the name. - /// - internal string Name - { - get - { - return _name; - } - - set - { - EqtAssert.StringNotNullOrEmpty(value, "Name"); - _name = value; - } - } - - /// - /// Gets the id. - /// - internal Guid Id - { - get { return _id; } - } - - /// - /// WARNING: do not use from inside Test Adapters, use from only on HA by UI etc. - /// Returns directory on HA for dependent files for TestResult. XmlPersistence method for UI. - /// Throws on error (e.g. if deployment directory was not set for test run). - /// - /// - /// Test Result to get dependent files directory for. - /// - /// - /// Result directory. - /// - internal string GetResultFilesDirectory(TestResult result) - { - EqtAssert.ParameterNotNull(result, nameof(result)); - return Path.Combine(GetResultsDirectory(), result.RelativeTestResultsDirectory); - } - - /// - /// Gets the results directory, which is the run deployment In directory - /// - /// The results directory - /// This method is called by public properties/methods, so it needs to throw on error - internal string GetResultsDirectory() - { - if (RunConfiguration == null) - { - Debug.Fail("'RunConfiguration' is null"); - throw new Exception(TrxLoggerResources.Common_MissingRunConfigInRun); - } - - if (string.IsNullOrEmpty(RunConfiguration.RunDeploymentRootDirectory)) - { - Debug.Fail("'RunConfiguration.RunDeploymentRootDirectory' is null or empty"); - throw new Exception(TrxLoggerResources.Common_MissingRunDeploymentRootInRunConfig); - } - - return RunConfiguration.RunDeploymentInDirectory; - } - - private static string FormatDateTimeForRunName(DateTime timeStamp) - { - // We use custom format string to make sure that runs are sorted in the same way on all intl machines. - // This is both for directory names and for Data Warehouse. - return timeStamp.ToString("yyyy-MM-dd HH:mm:ss", DateTimeFormatInfo.InvariantInfo); - } -} diff --git a/.upstream-vstest-objectmodel/UnitTestElement.cs b/.upstream-vstest-objectmodel/UnitTestElement.cs deleted file mode 100644 index ae4084b..0000000 --- a/.upstream-vstest-objectmodel/UnitTestElement.cs +++ /dev/null @@ -1,88 +0,0 @@ -// 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 Microsoft.TestPlatform.Extensions.TrxLogger.Utility; - -using Microsoft.TestPlatform.Extensions.TrxLogger.XML; -using Microsoft.VisualStudio.TestPlatform.Extensions.TrxLogger; - -namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; - -/// -/// Unit test element. -/// -internal class UnitTestElement : TestElement, IXmlTestStoreCustom -{ - private string? _codeBase; - - public UnitTestElement( - Guid id, - string name, - string adapter, - TestMethod testMethod) : base(id, name, adapter) - { - TPDebug.Assert(!string.IsNullOrEmpty(adapter), "adapter is null"); - TPDebug.Assert(testMethod != null, "testMethod is null"); - TPDebug.Assert(testMethod != null && testMethod.ClassName != null, "className is null"); - - TestMethod = testMethod; - } - - string IXmlTestStoreCustom.ElementName - { - get { return Constants.UnitTestElementName; } - } - - string? IXmlTestStoreCustom.NamespaceUri - { - get { return null; } - } - - /// - /// Gets the test type. - /// - public override TestType TestType - { - get { return Constants.UnitTestType; } - } - - /// - /// Gets the test method. - /// - public TestMethod TestMethod { get; } - - /// - /// Gets or sets the storage. - /// - public string? CodeBase - { - get { return _codeBase; } - - set - { - EqtAssert.StringNotNullOrEmpty(value, "CodeBase"); - _codeBase = value; - } - } - - /// - /// Saves the class under the XmlElement.. - /// - /// - /// The parent xml. - /// - /// - /// The parameter - /// - public override void Save(System.Xml.XmlElement element, XmlTestStoreParameters? parameters) - { - base.Save(element, parameters); - XmlPersistence h = new(); - - h.SaveSimpleField(element, "TestMethod/@codeBase", _codeBase, string.Empty); - h.SaveSimpleField(element, "TestMethod/@adapterTypeName", _adapter, string.Empty); - h.SaveObject(TestMethod, element, "TestMethod", parameters); - } -} diff --git a/.upstream-vstest-objectmodel/UnitTestResult.cs b/.upstream-vstest-objectmodel/UnitTestResult.cs deleted file mode 100644 index be0b0e1..0000000 --- a/.upstream-vstest-objectmodel/UnitTestResult.cs +++ /dev/null @@ -1,27 +0,0 @@ -// 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 Microsoft.TestPlatform.Extensions.TrxLogger.Utility; - -namespace Microsoft.TestPlatform.Extensions.TrxLogger.ObjectModel; - -/// -/// Class for unit test result. -/// -internal class UnitTestResult : TestResultAggregation -{ - public UnitTestResult( - Guid runId, - Guid testId, - Guid executionId, - Guid parentExecutionId, - string resultName, - string computerName, - TestOutcome outcome, - TestType testType, - TestListCategoryId testCategoryId, - TrxFileHelper trxFileHelper - ) : base(runId, testId, executionId, parentExecutionId, resultName, computerName, outcome, testType, testCategoryId, trxFileHelper) { } -} From 5e4860bb140d544d7a3f762c050ad647a2344e1d Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 23:32:59 -0700 Subject: [PATCH 09/11] refactor: convert regression tests to Theory/MemberData - 4 outcome Facts -> single [Theory][InlineData] (Parse_OutcomeAttribute_RoundTrips) - 3 directory Facts + helper -> [Theory][MemberData] (Parse_TestProjectDirectory_ResolvesFromBinAnchor) - Remove AssertProjectRootResolves helper and section-divider banners --- TrxLib.Tests/TrxParserRegressionTests.cs | 91 ++++++------------------ 1 file changed, 21 insertions(+), 70 deletions(-) diff --git a/TrxLib.Tests/TrxParserRegressionTests.cs b/TrxLib.Tests/TrxParserRegressionTests.cs index 3bd815d..a5b8220 100644 --- a/TrxLib.Tests/TrxParserRegressionTests.cs +++ b/TrxLib.Tests/TrxParserRegressionTests.cs @@ -7,84 +7,35 @@ namespace TrxLib.Tests; ///
public class TrxParserRegressionTests { - // ------------------------------------------------------------------------- - // Outcome parsing – All vstest outcome strings (Error, Aborted, NotRunnable, - // Disconnected, Warning, Completed, InProgress, PassedButRunAborted) must - // round-trip correctly through the parser. A missing outcome attribute must - // map to TestOutcome.Error, not NotExecuted. - // ------------------------------------------------------------------------- - - [Fact] - public void Parse_ErrorOutcome_IsNotSilentlyMappedToNotExecuted() - { - // vstest writes outcome="Error" when the test adapter itself crashes or the - // test process aborts unexpectedly. This is NOT the same as "not executed". - using var trxFile = new TempTrxFile(MinimalTrx(outcome: "Error")); - var results = TrxParser.Parse(trxFile.FileInfo); - - results.Single().Outcome.Should().Be(TestOutcome.Error, - because: "outcome=\"Error\" is a distinct vstest state indicating a system error"); - } - - [Fact] - public void Parse_AbortedOutcome_IsNotSilentlyMappedToNotExecuted() + // All vstest outcome strings must round-trip correctly through the parser. + // A null/missing outcome attribute maps to Error: TestOutcome.Error is ordinal 0 + // (the enum default), so vstest omits the attribute rather than writing "Error". + [Theory] + [InlineData("Error", TestOutcome.Error)] + [InlineData("Aborted", TestOutcome.Aborted)] + [InlineData("NotRunnable", TestOutcome.NotRunnable)] + [InlineData(null, TestOutcome.Error)] // absent attribute = Error + public void Parse_OutcomeAttribute_RoundTrips(string? outcomeAttr, TestOutcome expected) { - // vstest writes outcome="Aborted" when the framework (not the user) terminates - // the test mid-execution. - using var trxFile = new TempTrxFile(MinimalTrx(outcome: "Aborted")); + using var trxFile = new TempTrxFile(MinimalTrx(outcome: outcomeAttr)); var results = TrxParser.Parse(trxFile.FileInfo); - - results.Single().Outcome.Should().Be(TestOutcome.Aborted, - because: "outcome=\"Aborted\" is a distinct vstest state and must not be coerced to NotExecuted"); + results.Single().Outcome.Should().Be(expected); } - [Fact] - public void Parse_NotRunnableOutcome_IsNotSilentlyMappedToNotExecuted() + // TestProjectDirectory must resolve to the project root for all standard .NET SDK + // output layouts, including RID-qualified and publish subdirectories nested under bin/. + public static TheoryData DirectoryLayouts => new() { - // vstest writes outcome="NotRunnable" when ITestElement.IsRunnable == false. - using var trxFile = new TempTrxFile(MinimalTrx(outcome: "NotRunnable")); - var results = TrxParser.Parse(trxFile.FileInfo); - - results.Single().Outcome.Should().Be(TestOutcome.NotRunnable, - because: "outcome=\"NotRunnable\" is a distinct vstest state and must not be coerced to NotExecuted"); - } - - [Fact] - public void Parse_MissingOutcomeAttribute_MapsToError() - { - // In vstest's serialization, TestOutcome.Error == 0 is the enum default. - // XmlPersistence.SaveSimpleField() skips writing the attribute when value == default, - // so a real TRX file with an attachment error has NO outcome= attribute on - // . A missing attribute is the live form of the Error outcome. - using var trxFile = new TempTrxFile(MinimalTrx()); - var results = TrxParser.Parse(trxFile.FileInfo); - - results.Single().Outcome.Should().Be(TestOutcome.Error, - because: "a missing outcome attribute means TestOutcome.Error in vstest's serialization model"); - } - - // ------------------------------------------------------------------------- - // Directory resolution – TestProjectDirectory must be resolved correctly for - // all standard .NET SDK output layouts, including RID-qualified and publish - // subdirectories nested under bin/. - // ------------------------------------------------------------------------- - - [Fact] - public void Parse_TestProjectDirectory_IsCorrectForRidQualifiedOutputPath() => - AssertProjectRootResolves("fake_project_rid", "bin", "Debug", "net8.0", "win-x64", "MyProject.dll"); - - [Fact] - public void Parse_TestProjectDirectory_IsCorrectForPublishOutputPath() => - AssertProjectRootResolves("fake_project_publish", "bin", "Release", "net8.0", "publish", "MyProject.dll"); - - [Fact] - public void Parse_TestProjectDirectory_IsCorrectForRidPlusPublishOutputPath() => - AssertProjectRootResolves("fake_project_rid_publish", "bin", "Release", "net8.0", "linux-x64", "publish", "MyProject.dll"); + { "fake_project_rid", ["bin", "Debug", "net8.0", "win-x64", "MyProject.dll"] }, + { "fake_project_publish", ["bin", "Release", "net8.0", "publish", "MyProject.dll"] }, + { "fake_project_rid_publish", ["bin", "Release", "net8.0", "linux-x64", "publish", "MyProject.dll"] }, + }; - private static void AssertProjectRootResolves(string subfolder, params string[] relativeSegments) + [Theory, MemberData(nameof(DirectoryLayouts))] + public void Parse_TestProjectDirectory_ResolvesFromBinAnchor(string subfolder, string[] segments) { var projectRoot = Path.GetFullPath(Path.Combine(Path.GetTempPath(), subfolder)); - var codebase = Path.Combine(new[] { projectRoot }.Concat(relativeSegments).ToArray()); + var codebase = Path.Combine(new[] { projectRoot }.Concat(segments).ToArray()); using var trxFile = new TempTrxFile(MinimalTrx(codeBase: codebase)); var results = TrxParser.Parse(trxFile.FileInfo); results.Single().TestProjectDirectory!.FullName.TrimEnd(Path.DirectorySeparatorChar) From d81e0e969b9158cad26a9e6513c77cf331312463 Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 23:37:00 -0700 Subject: [PATCH 10/11] fix: return null when no bin folder found in codebase path The previous ternary returned the filesystem root directory when the codebase path contained no 'bin' segment, silently yielding a wrong result. Replaced with an explicit guard so TestProjectDirectory is null when the bin anchor cannot be found. Also drops the redundant null-conditional operators (dir?.Parent, dir?.Name) that were unnecessary since dir cannot be null after the while loop. --- TrxLib/TrxParser.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/TrxLib/TrxParser.cs b/TrxLib/TrxParser.cs index 56d14cc..a1931d9 100644 --- a/TrxLib/TrxParser.cs +++ b/TrxLib/TrxParser.cs @@ -131,14 +131,14 @@ public static TestResultSet Parse(FileInfo trxFile) if (codebaseFile?.Directory != null) { var dir = codebaseFile.Directory; - while (dir?.Parent != null && !string.Equals(dir.Name, "bin", StringComparison.OrdinalIgnoreCase)) + while (dir.Parent != null && !string.Equals(dir.Name, "bin", StringComparison.OrdinalIgnoreCase)) { dir = dir.Parent; } - // dir is now the 'bin' folder (or root if no 'bin' found); its parent is the project root. - testProjectDirectory = string.Equals(dir?.Name, "bin", StringComparison.OrdinalIgnoreCase) - ? dir!.Parent - : dir; + // Return the parent of the 'bin' folder (the project root). + // If no 'bin' folder was found, leave testProjectDirectory as null. + if (string.Equals(dir.Name, "bin", StringComparison.OrdinalIgnoreCase)) + testProjectDirectory = dir.Parent; } // Extract StdOut if available From 2b14f1bd59748c4764977bf88f3f057a092091db Mon Sep 17 00:00:00 2001 From: Benjamin Michaelis Date: Sat, 16 May 2026 23:40:10 -0700 Subject: [PATCH 11/11] style: use pattern matching (is/is not) throughout TrxParser Replace == null / != null with is null / is not null, use is { } for nullable struct unwrapping (DateTimeOffset? timings), and is true for nullable bool comparisons. --- TrxLib/TrxParser.cs | 51 ++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/TrxLib/TrxParser.cs b/TrxLib/TrxParser.cs index a1931d9..08f5fbe 100644 --- a/TrxLib/TrxParser.cs +++ b/TrxLib/TrxParser.cs @@ -18,22 +18,22 @@ public static TestResultSet Parse(FileInfo trxFile) { using var stream = trxFile.OpenRead(); TestRun? testRun = DeserializeTestRun(stream); - if (testRun == null) + if (testRun is null) return new TestResultSet(); // Build a lookup for UnitTest definitions - var testDefinitions = testRun.TestDefinitions?.UnitTests?.Where(u => u.Id != null).ToDictionary(u => u.Id!) ?? new(); + var testDefinitions = testRun.TestDefinitions?.UnitTests?.Where(u => u.Id is not null).ToDictionary(u => u.Id!) ?? new(); var results = new List(); foreach (var result in testRun.Results?.UnitTestResults ?? Enumerable.Empty()) { // Find the test definition UnitTest? unitTest = null; - if (result.TestId != null) + if (result.TestId is not null) testDefinitions.TryGetValue(result.TestId, out unitTest); var testMethod = unitTest?.TestMethod; - var testMethodDomain = testMethod == null ? null : new TestMethod + var testMethodDomain = testMethod is null ? null : new TestMethod { CodeBase = testMethod.CodeBase ?? string.Empty, ClassName = testMethod.ClassName ?? string.Empty, @@ -75,7 +75,7 @@ public static TestResultSet Parse(FileInfo trxFile) // Build the fully qualified test name correctly string fullyQualifiedTestName = string.Empty; - if (testMethodDomain != null) + if (testMethodDomain is not null) { if (!string.IsNullOrEmpty(testMethodDomain.ClassName) && !string.IsNullOrEmpty(testMethodDomain.Name)) { @@ -98,7 +98,7 @@ public static TestResultSet Parse(FileInfo trxFile) else if (result.TestName.StartsWith(methodShortName, StringComparison.Ordinal)) candidate = result.TestName.Substring(methodShortName.Length); - if (candidate?.StartsWith("(", StringComparison.Ordinal) == true) + if (candidate?.StartsWith("(", StringComparison.Ordinal) is true) paramSuffix = candidate; } fullyQualifiedTestName = baseFqtn + paramSuffix; @@ -128,10 +128,9 @@ public static TestResultSet Parse(FileInfo trxFile) // bin/{config}/{tfm}/publish/ (depth 4 — publish output) // bin/{config}/{tfm}/{rid}/publish/ (depth 5 — self-contained publish) DirectoryInfo? testProjectDirectory = null; - if (codebaseFile?.Directory != null) + if (codebaseFile?.Directory is { } dir) { - var dir = codebaseFile.Directory; - while (dir.Parent != null && !string.Equals(dir.Name, "bin", StringComparison.OrdinalIgnoreCase)) + while (dir.Parent is not null && !string.Equals(dir.Name, "bin", StringComparison.OrdinalIgnoreCase)) { dir = dir.Parent; } @@ -181,14 +180,14 @@ public static TestResultSet Parse(FileInfo trxFile) }; // Set timing properties from the Times element if available - if (creationTime.HasValue) - testResultSet.CreatedTime = creationTime.Value; - if (queueingTime.HasValue) - testResultSet.QueuedTime = queueingTime.Value; - if (startTime.HasValue) - testResultSet.StartedTime = startTime.Value; - if (finishTime.HasValue) - testResultSet.CompletedTime = finishTime.Value; + if (creationTime is { } created) + testResultSet.CreatedTime = created; + if (queueingTime is { } queued) + testResultSet.QueuedTime = queued; + if (startTime is { } started) + testResultSet.StartedTime = started; + if (finishTime is { } finished) + testResultSet.CompletedTime = finished; return testResultSet; } @@ -206,7 +205,7 @@ public static TestResultSet Parse(FileInfo trxFile) } var root = doc.Root; - if (root == null) + if (root is null) return null; var testRun = new TestRun @@ -217,7 +216,7 @@ public static TestResultSet Parse(FileInfo trxFile) }; var timesEl = root.Element(TrxNs + "Times"); - if (timesEl != null) + if (timesEl is not null) { testRun.Times = new Times { @@ -229,7 +228,7 @@ public static TestResultSet Parse(FileInfo trxFile) } var testSettingsEl = root.Element(TrxNs + "TestSettings"); - if (testSettingsEl != null) + if (testSettingsEl is not null) { testRun.TestSettings = new TestSettings { @@ -242,7 +241,7 @@ public static TestResultSet Parse(FileInfo trxFile) } var testDefsEl = root.Element(TrxNs + "TestDefinitions"); - if (testDefsEl != null) + if (testDefsEl is not null) { testRun.TestDefinitions = new TestDefinitions { @@ -257,7 +256,7 @@ public static TestResultSet Parse(FileInfo trxFile) Execution = ut.Element(TrxNs + "Execution") is XElement execEl ? new Execution { Id = (string?)execEl.Attribute("id") } : null, - TestMethod = tmEl != null ? new TestMethod + TestMethod = tmEl is not null ? new TestMethod { CodeBase = (string?)tmEl.Attribute("codeBase"), ClassName = (string?)tmEl.Attribute("className"), @@ -270,7 +269,7 @@ public static TestResultSet Parse(FileInfo trxFile) } var resultsEl = root.Element(TrxNs + "Results"); - if (resultsEl != null) + if (resultsEl is not null) { testRun.Results = new Results { @@ -314,7 +313,7 @@ public static TestResultSet Parse(FileInfo trxFile) } var resultSummaryEl = root.Element(TrxNs + "ResultSummary"); - if (resultSummaryEl != null) + if (resultSummaryEl is not null) { Counters? counters = null; if (resultSummaryEl.Element(TrxNs + "Counters") is XElement countersEl) @@ -356,7 +355,7 @@ public static TestResultSet Parse(FileInfo trxFile) } var testListsEl = root.Element(TrxNs + "TestLists"); - if (testListsEl != null) + if (testListsEl is not null) { testRun.TestLists = new TestLists { @@ -369,7 +368,7 @@ public static TestResultSet Parse(FileInfo trxFile) } var testEntriesEl = root.Element(TrxNs + "TestEntries"); - if (testEntriesEl != null) + if (testEntriesEl is not null) { testRun.TestEntries = new TestEntries {