diff --git a/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs b/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs index 2ec97b4..99ddfe9 100644 --- a/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs +++ b/src/DemaConsulting.TestResults/IO/JUnitSerializer.cs @@ -29,6 +29,26 @@ namespace DemaConsulting.TestResults.IO; /// public static class JUnitSerializer { + /// + /// Default suite name for tests without a class name + /// + private const string DefaultSuiteName = "DefaultSuite"; + + /// + /// Format string for time values in seconds with 3 decimal places + /// + private const string TimeFormatString = "F3"; + + /// + /// Error message for invalid JUnit XML file + /// + private const string InvalidJUnitFileMessage = "Invalid JUnit XML file"; + + /// + /// Attribute name for message in XML elements + /// + private const string MessageAttributeName = "message"; + /// /// Serializes the TestResults object to a JUnit XML file /// @@ -48,77 +68,7 @@ public static string Serialize(TestResults results) // Add test suites for each class foreach (var suiteGroup in testSuites) { - var className = string.IsNullOrEmpty(suiteGroup.Key) ? "DefaultSuite" : suiteGroup.Key; - var suiteTests = suiteGroup.ToList(); - - var testSuite = new XElement("testsuite", - new XAttribute("name", className), - new XAttribute("tests", suiteTests.Count), - new XAttribute("failures", suiteTests.Count(t => t.Outcome == TestOutcome.Failed)), - new XAttribute("errors", suiteTests.Count(t => t.Outcome == TestOutcome.Error || t.Outcome == TestOutcome.Timeout || t.Outcome == TestOutcome.Aborted)), - new XAttribute("skipped", suiteTests.Count(t => !t.Outcome.IsExecuted())), - new XAttribute("time", suiteTests.Sum(t => t.Duration.TotalSeconds).ToString("F3", CultureInfo.InvariantCulture))); - - // Add test cases - foreach (var test in suiteTests) - { - var testCase = new XElement("testcase", - new XAttribute("name", test.Name), - new XAttribute("classname", string.IsNullOrEmpty(test.ClassName) ? "DefaultSuite" : test.ClassName), - new XAttribute("time", test.Duration.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture))); - - // Add failure or error information - if (test.Outcome == TestOutcome.Failed) - { - var failure = new XElement("failure"); - if (!string.IsNullOrEmpty(test.ErrorMessage)) - { - failure.Add(new XAttribute("message", test.ErrorMessage)); - } - if (!string.IsNullOrEmpty(test.ErrorStackTrace)) - { - failure.Add(new XCData(test.ErrorStackTrace)); - } - testCase.Add(failure); - } - else if (test.Outcome == TestOutcome.Error || test.Outcome == TestOutcome.Timeout || test.Outcome == TestOutcome.Aborted) - { - var error = new XElement("error"); - if (!string.IsNullOrEmpty(test.ErrorMessage)) - { - error.Add(new XAttribute("message", test.ErrorMessage)); - } - if (!string.IsNullOrEmpty(test.ErrorStackTrace)) - { - error.Add(new XCData(test.ErrorStackTrace)); - } - testCase.Add(error); - } - else if (!test.Outcome.IsExecuted()) - { - var skipped = new XElement("skipped"); - if (!string.IsNullOrEmpty(test.ErrorMessage)) - { - skipped.Add(new XAttribute("message", test.ErrorMessage)); - } - testCase.Add(skipped); - } - - // Add system output - if (!string.IsNullOrEmpty(test.SystemOutput)) - { - testCase.Add(new XElement("system-out", new XCData(test.SystemOutput))); - } - - // Add system error - if (!string.IsNullOrEmpty(test.SystemError)) - { - testCase.Add(new XElement("system-err", new XCData(test.SystemError))); - } - - testSuite.Add(testCase); - } - + var testSuite = CreateTestSuiteElement(suiteGroup); root.Add(testSuite); } @@ -129,6 +79,125 @@ public static string Serialize(TestResults results) return writer.ToString(); } + /// + /// Creates a test suite element for a group of tests + /// + /// A grouping of test results by class name + /// An XElement representing a testsuite with all test cases and statistics + private static XElement CreateTestSuiteElement(IGrouping suiteGroup) + { + var className = string.IsNullOrEmpty(suiteGroup.Key) ? DefaultSuiteName : suiteGroup.Key; + var suiteTests = suiteGroup.ToList(); + + var testSuite = new XElement("testsuite", + new XAttribute("name", className), + new XAttribute("tests", suiteTests.Count), + new XAttribute("failures", suiteTests.Count(t => t.Outcome == TestOutcome.Failed)), + new XAttribute("errors", suiteTests.Count(t => IsErrorOutcome(t.Outcome))), + new XAttribute("skipped", suiteTests.Count(t => !t.Outcome.IsExecuted())), + new XAttribute("time", suiteTests.Sum(t => t.Duration.TotalSeconds).ToString(TimeFormatString, CultureInfo.InvariantCulture))); + + // Add test cases + foreach (var test in suiteTests) + { + var testCase = CreateTestCaseElement(test); + testSuite.Add(testCase); + } + + return testSuite; + } + + /// + /// Determines if an outcome represents an error condition + /// + /// The test outcome to evaluate + /// True if the outcome is Error, Timeout, or Aborted; otherwise false + private static bool IsErrorOutcome(TestOutcome outcome) + { + return outcome == TestOutcome.Error || outcome == TestOutcome.Timeout || outcome == TestOutcome.Aborted; + } + + /// + /// Creates a test case element + /// + /// The test result to serialize + /// An XElement representing a testcase with outcome, system output, and system error + private static XElement CreateTestCaseElement(TestResult test) + { + var testCase = new XElement("testcase", + new XAttribute("name", test.Name), + new XAttribute("classname", string.IsNullOrEmpty(test.ClassName) ? DefaultSuiteName : test.ClassName), + new XAttribute("time", test.Duration.TotalSeconds.ToString(TimeFormatString, CultureInfo.InvariantCulture))); + + // Add failure or error information based on outcome + AddOutcomeElement(testCase, test); + + // Add system output + if (!string.IsNullOrEmpty(test.SystemOutput)) + { + testCase.Add(new XElement("system-out", new XCData(test.SystemOutput))); + } + + // Add system error + if (!string.IsNullOrEmpty(test.SystemError)) + { + testCase.Add(new XElement("system-err", new XCData(test.SystemError))); + } + + return testCase; + } + + /// + /// Adds the appropriate outcome element (failure, error, or skipped) to a test case + /// + /// The testcase element to add the outcome element to + /// The test result containing the outcome information + private static void AddOutcomeElement(XElement testCase, TestResult test) + { + if (test.Outcome == TestOutcome.Failed) + { + var failure = CreateFailureOrErrorElement("failure", test); + testCase.Add(failure); + } + else if (IsErrorOutcome(test.Outcome)) + { + var error = CreateFailureOrErrorElement("error", test); + testCase.Add(error); + } + else if (!test.Outcome.IsExecuted()) + { + var skipped = new XElement("skipped"); + if (!string.IsNullOrEmpty(test.ErrorMessage)) + { + skipped.Add(new XAttribute(MessageAttributeName, test.ErrorMessage)); + } + testCase.Add(skipped); + } + } + + /// + /// Creates a failure or error element with message and stack trace + /// + /// The name of the element to create ("failure" or "error") + /// The test result containing error message and stack trace + /// An XElement with the specified name containing message attribute and stack trace content + private static XElement CreateFailureOrErrorElement(string elementName, TestResult test) + { + var element = new XElement(elementName); + + if (!string.IsNullOrEmpty(test.ErrorMessage)) + { + element.Add(new XAttribute(MessageAttributeName, test.ErrorMessage)); + } + + if (!string.IsNullOrEmpty(test.ErrorStackTrace)) + { + element.Add(new XCData(test.ErrorStackTrace)); + } + + return element; + } + /// /// Deserializes a JUnit XML file to a TestResults object /// @@ -143,8 +212,7 @@ public static TestResults Deserialize(string junitContents) var results = new TestResults(); // Get the root element (testsuites) - var rootElement = doc.Root ?? - throw new InvalidOperationException("Invalid JUnit XML file"); + var rootElement = doc.Root ?? throw new InvalidOperationException(InvalidJUnitFileMessage); // Get the test suite name (from testsuites or first testsuite) results.Name = rootElement.Attribute("name")?.Value ?? string.Empty; @@ -157,74 +225,117 @@ public static TestResults Deserialize(string junitContents) // Process each test suite foreach (var testSuiteElement in testSuiteElements) { - // Get test cases - var testCaseElements = testSuiteElement.Elements("testcase"); - - foreach (var testCaseElement in testCaseElements) - { - // Parse test case attributes - var name = testCaseElement.Attribute("name")?.Value ?? string.Empty; - var className = testCaseElement.Attribute("classname")?.Value ?? string.Empty; - var timeStr = testCaseElement.Attribute("time")?.Value ?? "0"; - var duration = double.TryParse(timeStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var timeValue) - ? TimeSpan.FromSeconds(timeValue) - : TimeSpan.Zero; - - // Determine outcome based on child elements - var failureElement = testCaseElement.Element("failure"); - var errorElement = testCaseElement.Element("error"); - var skippedElement = testCaseElement.Element("skipped"); - - TestOutcome outcome; - string errorMessage = string.Empty; - string errorStackTrace = string.Empty; - - if (failureElement != null) - { - outcome = TestOutcome.Failed; - errorMessage = failureElement.Attribute("message")?.Value ?? string.Empty; - errorStackTrace = failureElement.Value; - } - else if (errorElement != null) - { - outcome = TestOutcome.Error; - errorMessage = errorElement.Attribute("message")?.Value ?? string.Empty; - errorStackTrace = errorElement.Value; - } - else if (skippedElement != null) - { - outcome = TestOutcome.NotExecuted; - errorMessage = skippedElement.Attribute("message")?.Value ?? string.Empty; - } - else - { - outcome = TestOutcome.Passed; - } - - // Get system output and error - var systemOutput = testCaseElement.Element("system-out")?.Value ?? string.Empty; - var systemError = testCaseElement.Element("system-err")?.Value ?? string.Empty; - - // Create test result - var testResult = new TestResult - { - Name = name, - ClassName = className == "DefaultSuite" ? string.Empty : className, - Duration = duration, - Outcome = outcome, - ErrorMessage = errorMessage, - ErrorStackTrace = errorStackTrace, - SystemOutput = systemOutput, - SystemError = systemError - }; - - results.Results.Add(testResult); - } + ParseTestSuite(testSuiteElement, results); } return results; } + /// + /// Parses a test suite element and adds test cases to results + /// + /// The testsuite XML element to parse + /// The TestResults object to populate with test case data + private static void ParseTestSuite(XElement testSuiteElement, TestResults results) + { + var testCaseElements = testSuiteElement.Elements("testcase"); + + foreach (var testCaseElement in testCaseElements) + { + var testResult = ParseTestCase(testCaseElement); + results.Results.Add(testResult); + } + } + + /// + /// Parses a test case element + /// + /// The testcase XML element to parse + /// A TestResult object populated with data from the XML element + private static TestResult ParseTestCase(XElement testCaseElement) + { + // Parse basic test case attributes + var name = testCaseElement.Attribute("name")?.Value ?? string.Empty; + var className = testCaseElement.Attribute("classname")?.Value ?? string.Empty; + var duration = ParseDuration(testCaseElement.Attribute("time")?.Value); + + // Determine outcome and error information + var (outcome, errorMessage, errorStackTrace) = ParseOutcome(testCaseElement); + + // Get system output and error + var systemOutput = testCaseElement.Element("system-out")?.Value ?? string.Empty; + var systemError = testCaseElement.Element("system-err")?.Value ?? string.Empty; + + // Create test result + return new TestResult + { + Name = name, + ClassName = className == DefaultSuiteName ? string.Empty : className, + Duration = duration, + Outcome = outcome, + ErrorMessage = errorMessage, + ErrorStackTrace = errorStackTrace, + SystemOutput = systemOutput, + SystemError = systemError + }; + } + + /// + /// Parses a duration string to a TimeSpan + /// + /// The time string to parse, representing seconds as a decimal number + /// A TimeSpan representing the duration, or TimeSpan.Zero if parsing fails + private static TimeSpan ParseDuration(string? timeStr) + { + if (string.IsNullOrEmpty(timeStr)) + return TimeSpan.Zero; + + return double.TryParse(timeStr, NumberStyles.Float, CultureInfo.InvariantCulture, out var timeValue) + ? TimeSpan.FromSeconds(timeValue) + : TimeSpan.Zero; + } + + /// + /// Parses outcome information from test case child elements + /// + /// The testcase XML element to parse for outcome information + /// A tuple containing the test outcome, error message, and error stack trace + private static (TestOutcome outcome, string errorMessage, string errorStackTrace) ParseOutcome(XElement testCaseElement) + { + var failureElement = testCaseElement.Element("failure"); + var errorElement = testCaseElement.Element("error"); + var skippedElement = testCaseElement.Element("skipped"); + + if (failureElement != null) + { + return ( + TestOutcome.Failed, + failureElement.Attribute(MessageAttributeName)?.Value ?? string.Empty, + failureElement.Value + ); + } + + if (errorElement != null) + { + return ( + TestOutcome.Error, + errorElement.Attribute(MessageAttributeName)?.Value ?? string.Empty, + errorElement.Value + ); + } + + if (skippedElement != null) + { + return ( + TestOutcome.NotExecuted, + skippedElement.Attribute(MessageAttributeName)?.Value ?? string.Empty, + string.Empty + ); + } + + return (TestOutcome.Passed, string.Empty, string.Empty); + } + /// /// String writer that uses UTF-8 encoding /// diff --git a/src/DemaConsulting.TestResults/IO/TrxSerializer.cs b/src/DemaConsulting.TestResults/IO/TrxSerializer.cs index 968af4f..6c232bd 100644 --- a/src/DemaConsulting.TestResults/IO/TrxSerializer.cs +++ b/src/DemaConsulting.TestResults/IO/TrxSerializer.cs @@ -36,6 +36,36 @@ public static class TrxSerializer /// private static readonly XNamespace TrxNamespace = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"; + /// + /// Standard test type GUID for unit tests + /// + private const string TestTypeGuid = "13CDC9D9-DDB5-4fa4-A97D-D965CCFC6D4B"; + + /// + /// Standard test list ID for all loaded results + /// + private const string TestListId = "19431567-8539-422a-85D7-44EE4E166BDA"; + + /// + /// Name for the standard test list + /// + private const string TestListName = "All Loaded Results"; + + /// + /// Error message for invalid TRX file + /// + private const string InvalidTrxFileMessage = "Invalid TRX file"; + + /// + /// Date/time format string for invariant culture + /// + private const string DateTimeFormatString = "o"; + + /// + /// Duration format string for TimeSpan + /// + private const string DurationFormatString = "c"; + /// /// Serializes the TestResults object to a TRX file /// @@ -47,129 +77,227 @@ public static string Serialize(TestResults results) var doc = new XDocument(); // Construct the root element - var root = new XElement(TrxNamespace + "TestRun", + var root = CreateRootElement(results); + doc.Add(root); + + // Add results section + var resultsElement = CreateResultsElement(results.Results); + root.Add(resultsElement); + + // Add definitions section + var definitionsElement = CreateDefinitionsElement(results.Results); + root.Add(definitionsElement); + + // Add test entries section + var entriesElement = CreateTestEntriesElement(results.Results); + root.Add(entriesElement); + + // Add test lists section + var testListsElement = CreateTestListsElement(); + root.Add(testListsElement); + + // Add summary section + var summaryElement = CreateSummaryElement(results.Results); + root.Add(summaryElement); + + // Write the TRX text + var writer = new Utf8StringWriter(); + doc.Save(writer); + return writer.ToString(); + } + + /// + /// Creates the root TestRun element + /// + /// The test results containing test run metadata + /// An XElement representing the TestRun root element + private static XElement CreateRootElement(TestResults results) + { + return new XElement(TrxNamespace + "TestRun", new XAttribute("id", results.Id), new XAttribute("name", results.Name), new XAttribute("runUser", results.UserName)); - doc.Add(root); + } - // Construct the results + /// + /// Creates the Results section with all test results + /// + /// The collection of test results to serialize + /// An XElement containing all UnitTestResult elements + private static XElement CreateResultsElement(List testResults) + { var resultsElement = new XElement(TrxNamespace + "Results"); - root.Add(resultsElement); - foreach (var c in results.Results) + + foreach (var test in testResults) { - // Construct the result - var resultElement = new XElement(TrxNamespace + "UnitTestResult", - new XAttribute("executionId", c.ExecutionId), - new XAttribute("testId", c.TestId), - new XAttribute("testName", c.Name), - new XAttribute("computerName", c.ComputerName), - new XAttribute("testType", "13CDC9D9-DDB5-4fa4-A97D-D965CCFC6D4B"), - new XAttribute("outcome", c.Outcome), - new XAttribute("duration", c.Duration.ToString("c")), - new XAttribute("startTime", c.StartTime.ToString("o", CultureInfo.InvariantCulture)), - new XAttribute("endTime", (c.StartTime + c.Duration).ToString("o", CultureInfo.InvariantCulture)), - new XAttribute("testListId", "19431567-8539-422a-85D7-44EE4E166BDA")); + var resultElement = CreateUnitTestResultElement(test); resultsElement.Add(resultElement); + } + + return resultsElement; + } - // Construct the output element - var outputElement = new XElement(TrxNamespace + "Output"); - resultElement.Add(outputElement); - - // Construct the stdout output - if (!string.IsNullOrEmpty(c.SystemOutput)) - { - outputElement.Add( - new XElement(TrxNamespace + "StdOut", - new XCData(c.SystemOutput))); - } - - // Construct the stderr output - if (!string.IsNullOrEmpty(c.SystemError)) - { - outputElement.Add( - new XElement(TrxNamespace + "StdErr", - new XCData(c.SystemError))); - } - - // Skip writing the error info element if there is no error information - if (string.IsNullOrEmpty(c.ErrorMessage) && - string.IsNullOrEmpty(c.ErrorStackTrace)) - { - continue; - } - - // Construct the error info element - var errorInfoElement = new XElement(TrxNamespace + "ErrorInfo"); + /// + /// Creates a single UnitTestResult element + /// + /// The test result to serialize + /// An XElement representing a UnitTestResult with all test attributes and output + private static XElement CreateUnitTestResultElement(TestResult test) + { + var resultElement = new XElement(TrxNamespace + "UnitTestResult", + new XAttribute("executionId", test.ExecutionId), + new XAttribute("testId", test.TestId), + new XAttribute("testName", test.Name), + new XAttribute("computerName", test.ComputerName), + new XAttribute("testType", TestTypeGuid), + new XAttribute("outcome", test.Outcome), + new XAttribute("duration", test.Duration.ToString(DurationFormatString)), + new XAttribute("startTime", test.StartTime.ToString(DateTimeFormatString, CultureInfo.InvariantCulture)), + new XAttribute("endTime", (test.StartTime + test.Duration).ToString(DateTimeFormatString, CultureInfo.InvariantCulture)), + new XAttribute("testListId", TestListId)); + + var outputElement = CreateOutputElement(test); + resultElement.Add(outputElement); + + return resultElement; + } + + /// + /// Creates the Output element with stdout, stderr, and error information + /// + /// The test result containing output and error information + /// An XElement containing StdOut, StdErr, and ErrorInfo child elements as appropriate + private static XElement CreateOutputElement(TestResult test) + { + var outputElement = new XElement(TrxNamespace + "Output"); + + // Add stdout if present + if (!string.IsNullOrEmpty(test.SystemOutput)) + { + outputElement.Add( + new XElement(TrxNamespace + "StdOut", + new XCData(test.SystemOutput))); + } + + // Add stderr if present + if (!string.IsNullOrEmpty(test.SystemError)) + { + outputElement.Add( + new XElement(TrxNamespace + "StdErr", + new XCData(test.SystemError))); + } + + // Add error info if present + if (!string.IsNullOrEmpty(test.ErrorMessage) || !string.IsNullOrEmpty(test.ErrorStackTrace)) + { + var errorInfoElement = CreateErrorInfoElement(test); outputElement.Add(errorInfoElement); + } - // Construct the error message - if (!string.IsNullOrEmpty(c.ErrorMessage)) - { - errorInfoElement.Add( - new XElement(TrxNamespace + "Message", - new XCData(c.ErrorMessage))); - } - - // Construct the stack trace - if (!string.IsNullOrEmpty(c.ErrorStackTrace)) - { - errorInfoElement.Add( - new XElement(TrxNamespace + "StackTrace", - new XCData(c.ErrorStackTrace))); - } + return outputElement; + } + + /// + /// Creates the ErrorInfo element with message and stack trace + /// + /// The test result containing error message and stack trace + /// An XElement containing Message and StackTrace child elements + private static XElement CreateErrorInfoElement(TestResult test) + { + var errorInfoElement = new XElement(TrxNamespace + "ErrorInfo"); + + if (!string.IsNullOrEmpty(test.ErrorMessage)) + { + errorInfoElement.Add( + new XElement(TrxNamespace + "Message", + new XCData(test.ErrorMessage))); } - // Construct definitions + if (!string.IsNullOrEmpty(test.ErrorStackTrace)) + { + errorInfoElement.Add( + new XElement(TrxNamespace + "StackTrace", + new XCData(test.ErrorStackTrace))); + } + + return errorInfoElement; + } + + /// + /// Creates the TestDefinitions section with all unit test definitions + /// + /// The collection of test results to create definitions for + /// An XElement containing all UnitTest definition elements + private static XElement CreateDefinitionsElement(List testResults) + { var definitionsElement = new XElement(TrxNamespace + "TestDefinitions"); - root.Add(definitionsElement); - foreach (var c in results.Results) + + foreach (var test in testResults) { definitionsElement.Add( new XElement(TrxNamespace + "UnitTest", - new XAttribute("name", c.Name), - new XAttribute("id", c.TestId), + new XAttribute("name", test.Name), + new XAttribute("id", test.TestId), new XElement(TrxNamespace + "Execution", - new XAttribute("id", c.ExecutionId)), + new XAttribute("id", test.ExecutionId)), new XElement(TrxNamespace + "TestMethod", - new XAttribute("codeBase", c.CodeBase), - new XAttribute("className", c.ClassName), - new XAttribute("name", c.Name)))); + new XAttribute("codeBase", test.CodeBase), + new XAttribute("className", test.ClassName), + new XAttribute("name", test.Name)))); } - // Construct the Test Entries + return definitionsElement; + } + + /// + /// Creates the TestEntries section with all test entry mappings + /// + /// The collection of test results to create entries for + /// An XElement containing all TestEntry mapping elements + private static XElement CreateTestEntriesElement(List testResults) + { var entriesElement = new XElement(TrxNamespace + "TestEntries"); - root.Add(entriesElement); - foreach (var c in results.Results) + + foreach (var test in testResults) + { entriesElement.Add( new XElement(TrxNamespace + "TestEntry", - new XAttribute("testId", c.TestId), - new XAttribute("executionId", c.ExecutionId), - new XAttribute("testListId", "19431567-8539-422a-85D7-44EE4E166BDA"))); - - // Construct the test lists - root.Add( - new XElement(TrxNamespace + "TestLists", - new XElement(TrxNamespace + "TestList", - new XAttribute("name", "All Loaded Results"), - new XAttribute("id", "19431567-8539-422a-85D7-44EE4E166BDA")))); - - // Construct the summary - root.Add( - new XElement( - TrxNamespace + "ResultSummary", - new XAttribute("outcome", "Completed"), - new XElement( - TrxNamespace + "Counters", - new XAttribute("total", results.Results.Count), - new XAttribute("executed", results.Results.Count(c => c.Outcome.IsExecuted())), - new XAttribute("passed", results.Results.Count(c => c.Outcome.IsPassed())), - new XAttribute("failed", results.Results.Count(c => c.Outcome.IsFailed()))))); + new XAttribute("testId", test.TestId), + new XAttribute("executionId", test.ExecutionId), + new XAttribute("testListId", TestListId))); + } - // Write the TRX text - var writer = new Utf8StringWriter(); - doc.Save(writer); - return writer.ToString(); + return entriesElement; + } + + /// + /// Creates the TestLists section + /// + /// An XElement containing the standard TestList with the "All Loaded Results" list + private static XElement CreateTestListsElement() + { + return new XElement(TrxNamespace + "TestLists", + new XElement(TrxNamespace + "TestList", + new XAttribute("name", TestListName), + new XAttribute("id", TestListId))); + } + + /// + /// Creates the ResultSummary section with test statistics + /// + /// The collection of test results to calculate statistics from + /// An XElement containing the ResultSummary with Counters for total, executed, passed, and failed tests + private static XElement CreateSummaryElement(List testResults) + { + return new XElement( + TrxNamespace + "ResultSummary", + new XAttribute("outcome", "Completed"), + new XElement( + TrxNamespace + "Counters", + new XAttribute("total", testResults.Count), + new XAttribute("executed", testResults.Count(t => t.Outcome.IsExecuted())), + new XAttribute("passed", testResults.Count(t => t.Outcome.IsPassed())), + new XAttribute("failed", testResults.Count(t => t.Outcome.IsFailed())))); } /// @@ -187,73 +315,100 @@ public static TestResults Deserialize(string trxContents) // Construct the results var results = new TestResults(); - // Get the run element + // Parse the run element + ParseRunElement(doc, nsMgr, results); + + // Parse all test results + ParseTestResults(doc, nsMgr, results); + + // Return the results + return results; + } + + /// + /// Parses the TestRun element and populates basic result properties + /// + /// The XML document containing the TRX file + /// The namespace manager with TRX namespace mappings + /// The TestResults object to populate with run metadata + private static void ParseRunElement(XDocument doc, XmlNamespaceManager nsMgr, TestResults results) + { var runElement = doc.XPathSelectElement("/trx:TestRun", nsMgr) ?? - throw new InvalidOperationException("Invalid TRX file"); + throw new InvalidOperationException(InvalidTrxFileMessage); results.Id = Guid.Parse(runElement.Attribute("id")?.Value ?? Guid.NewGuid().ToString()); results.Name = runElement.Attribute("name")?.Value ?? string.Empty; results.UserName = runElement.Attribute("runUser")?.Value ?? string.Empty; + } - // Get the results + /// + /// Parses all test result elements and adds them to the results collection + /// + /// The XML document containing the TRX file + /// The namespace manager with TRX namespace mappings + /// The TestResults object to populate with test result data + private static void ParseTestResults(XDocument doc, XmlNamespaceManager nsMgr, TestResults results) + { var resultElements = doc.XPathSelectElements( "/trx:TestRun/trx:Results/trx:UnitTestResult", nsMgr); + foreach (var resultElement in resultElements) { - // Get the test ID - var testId = resultElement.Attribute("testId") ?? - throw new InvalidOperationException("Invalid TRX file"); - - // Get the test method element - var methodElement = - doc.XPathSelectElement( - $"/trx:TestRun/trx:TestDefinitions/trx:UnitTest[@id='{testId.Value}']/trx:TestMethod", - nsMgr) ?? - throw new InvalidOperationException("Invalid TRX File"); - - // Get the output element - var outputElement = resultElement.Element(TrxNamespace + "Output"); - - // Get the errorInfo element - var errorInfoElement = outputElement?.Element(TrxNamespace + "ErrorInfo"); - - // Add the test result - results.Results.Add( - new TestResult - { - TestId = Guid.Parse(testId.Value), - ExecutionId = Guid.Parse( - resultElement.Attribute("executionId")?.Value ?? Guid.NewGuid().ToString()), - Name = methodElement.Attribute("name")?.Value ?? string.Empty, - CodeBase = methodElement.Attribute("codeBase")?.Value ?? string.Empty, - ClassName = methodElement.Attribute("className")?.Value ?? string.Empty, - ComputerName = resultElement.Attribute("computerName")?.Value ?? string.Empty, - Outcome = Enum.Parse(resultElement.Attribute("outcome")?.Value ?? "Failed"), - StartTime = DateTime.Parse( - resultElement.Attribute("startTime")?.Value ?? - DateTime.UtcNow.ToString(CultureInfo.InvariantCulture), - CultureInfo.InvariantCulture, - DateTimeStyles.AdjustToUniversal), - Duration = TimeSpan.Parse( - resultElement.Attribute("duration")?.Value ?? "0", - CultureInfo.InvariantCulture), - SystemOutput = outputElement - ?.Element(TrxNamespace + "StdOut") - ?.Value ?? string.Empty, - SystemError = outputElement - ?.Element(TrxNamespace + "StdErr") - ?.Value ?? string.Empty, - ErrorMessage = errorInfoElement - ?.Element(TrxNamespace + "Message") - ?.Value ?? string.Empty, - ErrorStackTrace = errorInfoElement - ?.Element(TrxNamespace + "StackTrace") - ?.Value ?? string.Empty - }); + var testResult = ParseTestResult(doc, nsMgr, resultElement); + results.Results.Add(testResult); } + } - // Return the results - return results; + /// + /// Parses a single UnitTestResult element + /// + /// The XML document containing the TRX file + /// The namespace manager with TRX namespace mappings + /// The UnitTestResult element to parse + /// A TestResult object populated with data from the XML element + private static TestResult ParseTestResult(XDocument doc, XmlNamespaceManager nsMgr, XElement resultElement) + { + var testId = resultElement.Attribute("testId") ?? + throw new InvalidOperationException(InvalidTrxFileMessage); + + var methodElement = doc.XPathSelectElement( + $"/trx:TestRun/trx:TestDefinitions/trx:UnitTest[@id='{testId.Value}']/trx:TestMethod", + nsMgr) ?? throw new InvalidOperationException(InvalidTrxFileMessage); + + var outputElement = resultElement.Element(TrxNamespace + "Output"); + var errorInfoElement = outputElement?.Element(TrxNamespace + "ErrorInfo"); + + return new TestResult + { + TestId = Guid.Parse(testId.Value), + ExecutionId = Guid.Parse( + resultElement.Attribute("executionId")?.Value ?? Guid.NewGuid().ToString()), + Name = methodElement.Attribute("name")?.Value ?? string.Empty, + CodeBase = methodElement.Attribute("codeBase")?.Value ?? string.Empty, + ClassName = methodElement.Attribute("className")?.Value ?? string.Empty, + ComputerName = resultElement.Attribute("computerName")?.Value ?? string.Empty, + Outcome = Enum.Parse(resultElement.Attribute("outcome")?.Value ?? "Failed"), + StartTime = DateTime.Parse( + resultElement.Attribute("startTime")?.Value ?? + DateTime.UtcNow.ToString(CultureInfo.InvariantCulture), + CultureInfo.InvariantCulture, + DateTimeStyles.AdjustToUniversal), + Duration = TimeSpan.Parse( + resultElement.Attribute("duration")?.Value ?? "0", + CultureInfo.InvariantCulture), + SystemOutput = outputElement + ?.Element(TrxNamespace + "StdOut") + ?.Value ?? string.Empty, + SystemError = outputElement + ?.Element(TrxNamespace + "StdErr") + ?.Value ?? string.Empty, + ErrorMessage = errorInfoElement + ?.Element(TrxNamespace + "Message") + ?.Value ?? string.Empty, + ErrorStackTrace = errorInfoElement + ?.Element(TrxNamespace + "StackTrace") + ?.Value ?? string.Empty + }; } ///