diff --git a/.github/skills/test-writing/SKILL.md b/.github/skills/test-writing/SKILL.md new file mode 100644 index 0000000..d0873d7 --- /dev/null +++ b/.github/skills/test-writing/SKILL.md @@ -0,0 +1,17 @@ +--- +name: test-writing +description: Use this skill when writing tests. +--- + +# TRX fixture policy for tests + +When adding or updating sample `.trx` files in this repository, use **real captured TRX output**, not synthetic hand-written fixtures. + +Required workflow: + +1. Create temporary tests that trigger the condition you need in the TRX. +2. Run `dotnet test` with TRX output enabled to generate a real file. +3. Copy that generated TRX file into `TrxLib.Tests/SampleTrxFiles/` (renaming is fine). +4. Remove high-level personal/environment PII from the file (for example: username, machine name, local workspace paths), while preserving the TRX structure and semantics needed by tests. + +Do not fabricate TRX XML by hand when a real capture is possible. diff --git a/TrxLib.Tests/SampleTrxFiles/aborted-outcome.trx b/TrxLib.Tests/SampleTrxFiles/aborted-outcome.trx new file mode 100644 index 0000000..58d1d1b --- /dev/null +++ b/TrxLib.Tests/SampleTrxFiles/aborted-outcome.trx @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + [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: TrxAbortGen +[xUnit.net 00:00:00.12] Discovered: TrxAbortGen +[xUnit.net 00:00:00.14] Starting: TrxAbortGen +[createdump] Writing full dump for process 120264 to file C:\Temp\sample\testhost_120264_20260517T000946_hangdump.dmp +[createdump] Dump successfully written in 1772ms + + + + + The active test run was aborted. Reason: Test host process crashed + + + Data collector 'Blame' message: The specified inactivity time of 5 seconds has elapsed. Collecting hang dumps from testhost and its child processes. + + + + + + + + + + + + + + + + diff --git a/TrxLib.Tests/SampleTrxFiles/no-namespace.trx b/TrxLib.Tests/SampleTrxFiles/no-namespace.trx new file mode 100644 index 0000000..d4deedd --- /dev/null +++ b/TrxLib.Tests/SampleTrxFiles/no-namespace.trx @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + Expected string length 5 but was 7. Strings differ at index 5. + Expected: "value" + But was: "value 2" + ----------------^ + + at NUnitTestExample.Tests.SimpleStringCompare() in C:\Users\Utilizador\RiderProjects\TestSolution\NUnitTestExample\SimpleTest.cs:line 17 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NUnit Adapter 4.2.0.0: Test execution started +Running all tests in C:\Users\Utilizador\RiderProjects\TestSolution\NUnitTestExample\bin\Debug\net6.0\NUnitTestExample.dll + NUnit3TestExecutor discovered 5 of 5 NUnit test cases using Current Discovery mode, Non-Explicit run +NUnit Adapter 4.2.0.0: Test execution complete + + + + diff --git a/TrxLib.Tests/TrxParserTests.cs b/TrxLib.Tests/TrxParserTests.cs index eb83079..b670c02 100644 --- a/TrxLib.Tests/TrxParserTests.cs +++ b/TrxLib.Tests/TrxParserTests.cs @@ -325,6 +325,27 @@ public void Parse_Example2WindowsTrx_TestsDoNotAppearWithMoreThanOneOutcome() results.Count.Should().Be(recombinedCount); } + // ── Regression tests ───────────────────────────────────────────────────────── + + [Fact] + public void Parse_AbortedRunTrx_ParsesRealAbortedRunFixture() + { + var results = TrxParser.Parse(new FileInfo(GetSampleFilePath("aborted-outcome.trx"))); + results.Should().HaveCount(1); + results.Single().Outcome.Should().Be(TestOutcome.Passed); + results.OriginalTestRun?.ResultSummary?.Outcome.Should().Be("Failed"); + } + + // Real NUnit TRX captured without xmlns on the root element, sourced from: + // https://github.com/joaoopereira/dotnet-test-rerun/blob/main/test/dotnet-test-rerun.UnitTests/Fixtures/RerunCommand/NUnitTrxFileWithOneFailedTest.trx + [Fact] + public void Parse_NoNamespaceTrx_ParsesResultsWithoutNamespace() + { + var results = TrxParser.Parse(new FileInfo(GetSampleFilePath("no-namespace.trx"))); + results.Should().HaveCount(5, + "the parser must fall back to namespace-agnostic element matching when xmlns is absent"); + } + [Fact] public void Parse_ComplexTrx_ParsesTestRunNameCorrectly() { diff --git a/TrxLib/TrxParser.cs b/TrxLib/TrxParser.cs index 08f5fbe..2aa872c 100644 --- a/TrxLib/TrxParser.cs +++ b/TrxLib/TrxParser.cs @@ -57,9 +57,6 @@ public static TestResultSet Parse(FileInfo trxFile) "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 }; @@ -130,14 +127,7 @@ public static TestResultSet Parse(FileInfo trxFile) DirectoryInfo? testProjectDirectory = null; if (codebaseFile?.Directory is { } dir) { - while (dir.Parent is not null && !string.Equals(dir.Name, "bin", StringComparison.OrdinalIgnoreCase)) - { - dir = dir.Parent; - } - // 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; + testProjectDirectory = FindProjectDirectory(codebaseFile.Directory); } // Extract StdOut if available @@ -192,6 +182,19 @@ public static TestResultSet Parse(FileInfo trxFile) return testResultSet; } + private static DirectoryInfo? FindProjectDirectory(DirectoryInfo dllDirectory) + { + var dir = dllDirectory; + while (dir.Parent is not null) + { + if (string.Equals(dir.Name, "bin", StringComparison.OrdinalIgnoreCase)) + return dir.Parent; + dir = dir.Parent; + } + return dir; + } + + private static TestRun? DeserializeTestRun(Stream stream) { XDocument doc; @@ -208,6 +211,8 @@ public static TestResultSet Parse(FileInfo trxFile) if (root is null) return null; + var ns = root.Name.Namespace; + var testRun = new TestRun { Name = (string?)root.Attribute("name"), @@ -215,7 +220,7 @@ public static TestResultSet Parse(FileInfo trxFile) RunUser = (string?)root.Attribute("runUser"), }; - var timesEl = root.Element(TrxNs + "Times"); + var timesEl = root.Element(ns + "Times"); if (timesEl is not null) { testRun.Times = new Times @@ -227,33 +232,33 @@ public static TestResultSet Parse(FileInfo trxFile) }; } - var testSettingsEl = root.Element(TrxNs + "TestSettings"); + var testSettingsEl = root.Element(ns + "TestSettings"); if (testSettingsEl is not null) { testRun.TestSettings = new TestSettings { Name = (string?)testSettingsEl.Attribute("name"), Id = (string?)testSettingsEl.Attribute("id"), - Deployment = testSettingsEl.Element(TrxNs + "Deployment") is XElement deployEl + Deployment = testSettingsEl.Element(ns + "Deployment") is XElement deployEl ? new Deployment { RunDeploymentRoot = (string?)deployEl.Attribute("runDeploymentRoot") } : null, }; } - var testDefsEl = root.Element(TrxNs + "TestDefinitions"); + var testDefsEl = root.Element(ns + "TestDefinitions"); if (testDefsEl is not null) { testRun.TestDefinitions = new TestDefinitions { - UnitTests = testDefsEl.Elements(TrxNs + "UnitTest").Select(ut => + UnitTests = testDefsEl.Elements(ns + "UnitTest").Select(ut => { - var tmEl = ut.Element(TrxNs + "TestMethod"); + var tmEl = ut.Element(ns + "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 + Execution = ut.Element(ns + "Execution") is XElement execEl ? new Execution { Id = (string?)execEl.Attribute("id") } : null, TestMethod = tmEl is not null ? new TestMethod @@ -268,28 +273,28 @@ public static TestResultSet Parse(FileInfo trxFile) }; } - var resultsEl = root.Element(TrxNs + "Results"); + var resultsEl = root.Element(ns + "Results"); if (resultsEl is not null) { testRun.Results = new Results { - UnitTestResults = resultsEl.Elements(TrxNs + "UnitTestResult").Select(ur => + UnitTestResults = resultsEl.Elements(ns + "UnitTestResult").Select(ur => { Output? output = null; - if (ur.Element(TrxNs + "Output") is XElement outputEl) + if (ur.Element(ns + "Output") is XElement outputEl) { ErrorInfo? errorInfo = null; - if (outputEl.Element(TrxNs + "ErrorInfo") is XElement errorInfoEl) + if (outputEl.Element(ns + "ErrorInfo") is XElement errorInfoEl) { errorInfo = new ErrorInfo { - Message = (string?)errorInfoEl.Element(TrxNs + "Message"), - StackTrace = (string?)errorInfoEl.Element(TrxNs + "StackTrace"), + Message = (string?)errorInfoEl.Element(ns + "Message"), + StackTrace = (string?)errorInfoEl.Element(ns + "StackTrace"), }; } output = new Output { - StdOut = (string?)outputEl.Element(TrxNs + "StdOut"), + StdOut = (string?)outputEl.Element(ns + "StdOut"), ErrorInfo = errorInfo, }; } @@ -312,11 +317,11 @@ public static TestResultSet Parse(FileInfo trxFile) }; } - var resultSummaryEl = root.Element(TrxNs + "ResultSummary"); + var resultSummaryEl = root.Element(ns + "ResultSummary"); if (resultSummaryEl is not null) { Counters? counters = null; - if (resultSummaryEl.Element(TrxNs + "Counters") is XElement countersEl) + if (resultSummaryEl.Element(ns + "Counters") is XElement countersEl) { counters = new Counters { @@ -339,11 +344,11 @@ public static TestResultSet Parse(FileInfo trxFile) }; } Output? summaryOutput = null; - if (resultSummaryEl.Element(TrxNs + "Output") is XElement summaryOutputEl) + if (resultSummaryEl.Element(ns + "Output") is XElement summaryOutputEl) { summaryOutput = new Output { - StdOut = (string?)summaryOutputEl.Element(TrxNs + "StdOut"), + StdOut = (string?)summaryOutputEl.Element(ns + "StdOut"), }; } testRun.ResultSummary = new ResultSummary @@ -354,12 +359,12 @@ public static TestResultSet Parse(FileInfo trxFile) }; } - var testListsEl = root.Element(TrxNs + "TestLists"); + var testListsEl = root.Element(ns + "TestLists"); if (testListsEl is not null) { testRun.TestLists = new TestLists { - Items = testListsEl.Elements(TrxNs + "TestList").Select(tl => new TestList + Items = testListsEl.Elements(ns + "TestList").Select(tl => new TestList { Name = (string?)tl.Attribute("name"), Id = (string?)tl.Attribute("id"), @@ -367,12 +372,12 @@ public static TestResultSet Parse(FileInfo trxFile) }; } - var testEntriesEl = root.Element(TrxNs + "TestEntries"); + var testEntriesEl = root.Element(ns + "TestEntries"); if (testEntriesEl is not null) { testRun.TestEntries = new TestEntries { - Items = testEntriesEl.Elements(TrxNs + "TestEntry").Select(te => new TestEntry + Items = testEntriesEl.Elements(ns + "TestEntry").Select(te => new TestEntry { TestId = (string?)te.Attribute("testId"), ExecutionId = (string?)te.Attribute("executionId"),