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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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"),