diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 2b30782..8ff59ef 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -58,6 +58,18 @@ An enumeration representing possible test outcomes: ### Serialization Layer +#### `Serializer` + +The `Serializer` class provides automatic format detection and deserialization: + +- Automatically identifies test result format (TRX or JUnit) based on XML structure +- Delegates to the appropriate format-specific serializer +- Simplifies usage when the input format is unknown +- Provides a `TestResultFormat` enum with values: `Unknown`, `Trx`, and `JUnit` +- Includes two main methods: + - `Identify(string contents)`: Detects the format without deserializing + - `Deserialize(string contents)`: Automatically detects format and deserializes + #### `TrxSerializer` The `TrxSerializer` class is responsible for converting between the domain model and TRX XML format: @@ -81,6 +93,8 @@ The `JUnitSerializer` class is responsible for converting between the domain mod ## Data Flow +### Creating Test Results + ```text 1. User creates TestResults instance 2. User adds TestResult objects to the Results collection @@ -88,6 +102,15 @@ The `JUnitSerializer` class is responsible for converting between the domain mod 4. User saves the XML string to a .trx or .xml file ``` +### Reading Test Results + +```text +1. User reads test result file contents +2. User calls Serializer.Deserialize() for automatic format detection + OR calls TrxSerializer.Deserialize() or JUnitSerializer.Deserialize() for specific formats +3. User receives TestResults instance with deserialized data +``` + ## Design Patterns - **Data Transfer Object (DTO)**: The `TestResults` and `TestResult` classes serve as DTOs for test data @@ -148,7 +171,6 @@ Potential enhancements that could be considered: 1. **Additional Formats**: Support for other test result formats (NUnit XML, xUnit XML, etc.) 2. **Streaming**: Support for streaming large test result sets to avoid memory issues 3. **Validation**: Add schema validation to ensure generated files are well-formed -4. **Format Detection**: Automatic detection of input format when deserializing ## Dependencies diff --git a/README.md b/README.md index 2057637..bc884bd 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,29 @@ File.WriteAllText("results.trx", TrxSerializer.Serialize(results)); File.WriteAllText("results.xml", JUnitSerializer.Serialize(results)); ``` +### Automatic Format Detection + +The library can automatically detect the format of test result files: + +```csharp +using DemaConsulting.TestResults.IO; + +// Automatically detect and deserialize any supported format +var testResultsXml = File.ReadAllText("test-results.xml"); +var results = Serializer.Deserialize(testResultsXml); + +// Or identify the format without deserializing +var format = Serializer.Identify(testResultsXml); +if (format == TestResultFormat.Trx) +{ + Console.WriteLine("This is a TRX file"); +} +else if (format == TestResultFormat.JUnit) +{ + Console.WriteLine("This is a JUnit XML file"); +} +``` + ### Converting Between Formats The library supports reading and converting between TRX and JUnit formats: @@ -85,19 +108,17 @@ The library supports reading and converting between TRX and JUnit formats: ```csharp using DemaConsulting.TestResults.IO; -// Read JUnit XML file -var junitXml = File.ReadAllText("junit-results.xml"); -var results = JUnitSerializer.Deserialize(junitXml); - -// Convert to TRX format +// Automatic format detection and conversion +var testResultsXml = File.ReadAllText("test-results.xml"); +var results = Serializer.Deserialize(testResultsXml); // Works with TRX or JUnit var trxXml = TrxSerializer.Serialize(results); File.WriteAllText("converted.trx", trxXml); -// Or convert TRX to JUnit -var trxXml2 = File.ReadAllText("test-results.trx"); -var results2 = TrxSerializer.Deserialize(trxXml2); -var junitXml2 = JUnitSerializer.Serialize(results2); -File.WriteAllText("converted.xml", junitXml2); +// Or use specific deserializers if format is known +var junitXml = File.ReadAllText("junit-results.xml"); +var results2 = JUnitSerializer.Deserialize(junitXml); +var trxXml2 = TrxSerializer.Serialize(results2); +File.WriteAllText("converted-from-junit.trx", trxXml2); ``` ## Advanced Usage diff --git a/docs/guide/guide.md b/docs/guide/guide.md index 6efad42..c2474ce 100644 --- a/docs/guide/guide.md +++ b/docs/guide/guide.md @@ -457,6 +457,53 @@ File.WriteAllText("metadata-results.trx", TrxSerializer.Serialize(results)); File.WriteAllText("metadata-results.xml", JUnitSerializer.Serialize(results)); ``` +### Automatic Format Detection + +The library can automatically detect whether a test result file is in TRX or JUnit format, eliminating the need to +manually determine the format: + +```csharp +using System; +using System.IO; +using DemaConsulting.TestResults; +using DemaConsulting.TestResults.IO; + +// Automatically detect and deserialize any supported format +string testResultsXml = File.ReadAllText("test-results.xml"); +TestResults results = Serializer.Deserialize(testResultsXml); + +Console.WriteLine($"Loaded {results.Results.Count} test results"); +Console.WriteLine($"Test suite: {results.Name}"); + +// You can also identify the format without deserializing +string unknownFile = File.ReadAllText("unknown-format.xml"); +TestResultFormat format = Serializer.Identify(unknownFile); + +switch (format) +{ + case TestResultFormat.Trx: + Console.WriteLine("This is a TRX (Visual Studio) test result file"); + break; + case TestResultFormat.JUnit: + Console.WriteLine("This is a JUnit XML test result file"); + break; + case TestResultFormat.Unknown: + Console.WriteLine("Unknown or invalid test result format"); + break; +} +``` + +The `Serializer` class provides two key methods: + +- **`Identify(string contents)`**: Determines the format without deserializing the content +- **`Deserialize(string contents)`**: Automatically detects the format and deserializes using the appropriate parser + +This is particularly useful when: + +- Processing test results from different CI/CD systems +- Building tools that handle multiple formats +- You don't know in advance which format will be provided + ### Converting Between Formats The library makes it easy to convert test results between TRX and JUnit formats: @@ -467,18 +514,24 @@ using System.IO; using DemaConsulting.TestResults; using DemaConsulting.TestResults.IO; -// Convert JUnit to TRX +// Automatic format detection and conversion +string testResultsXml = File.ReadAllText("test-results.xml"); +TestResults results = Serializer.Deserialize(testResultsXml); // Works with either format +string trxXml = TrxSerializer.Serialize(results); +File.WriteAllText("converted-to-trx.trx", trxXml); +Console.WriteLine("Converted to TRX format"); + +// Or use format-specific deserializers if you know the format string junitXml = File.ReadAllText("original-junit-results.xml"); TestResults resultsFromJUnit = JUnitSerializer.Deserialize(junitXml); -string trxXml = TrxSerializer.Serialize(resultsFromJUnit); -File.WriteAllText("converted-to-trx.trx", trxXml); +string trxXml2 = TrxSerializer.Serialize(resultsFromJUnit); +File.WriteAllText("converted-junit-to-trx.trx", trxXml2); Console.WriteLine("Converted JUnit to TRX format"); -// Convert TRX to JUnit -string trxXml2 = File.ReadAllText("original-trx-results.trx"); -TestResults resultsFromTrx = TrxSerializer.Deserialize(trxXml2); +string trxInput = File.ReadAllText("original-trx-results.trx"); +TestResults resultsFromTrx = TrxSerializer.Deserialize(trxInput); string junitXml2 = JUnitSerializer.Serialize(resultsFromTrx); -File.WriteAllText("converted-to-junit.xml", junitXml2); +File.WriteAllText("converted-trx-to-junit.xml", junitXml2); Console.WriteLine("Converted TRX to JUnit format"); ``` @@ -686,15 +739,15 @@ using System.Linq; using DemaConsulting.TestResults; using DemaConsulting.TestResults.IO; -// Read multiple test result files +// Read multiple test result files (automatically detect format) var unitTestsXml = File.ReadAllText("unit-tests.xml"); var integrationTestsXml = File.ReadAllText("integration-tests.xml"); var e2eTestsTrx = File.ReadAllText("e2e-tests.trx"); -// Deserialize each file -var unitTests = JUnitSerializer.Deserialize(unitTestsXml); -var integrationTests = JUnitSerializer.Deserialize(integrationTestsXml); -var e2eTests = TrxSerializer.Deserialize(e2eTestsTrx); +// Deserialize each file (format is automatically detected) +var unitTests = Serializer.Deserialize(unitTestsXml); +var integrationTests = Serializer.Deserialize(integrationTestsXml); +var e2eTests = Serializer.Deserialize(e2eTestsTrx); // Create combined results var combinedResults = new TestResults @@ -831,18 +884,46 @@ Both formats can be created from scratch or converted from one to the other. Yes! The library provides bidirectional conversion between formats: ```csharp -// TRX to JUnit +// Automatic format detection and conversion +var results = Serializer.Deserialize(File.ReadAllText("test-results.xml")); +var trxXml = TrxSerializer.Serialize(results); +var junitXml = JUnitSerializer.Serialize(results); + +// Or use format-specific deserializers var trxResults = TrxSerializer.Deserialize(File.ReadAllText("test.trx")); -var junitXml = JUnitSerializer.Serialize(trxResults); +var junitFromTrx = JUnitSerializer.Serialize(trxResults); -// JUnit to TRX var junitResults = JUnitSerializer.Deserialize(File.ReadAllText("test.xml")); -var trxXml = TrxSerializer.Serialize(junitResults); +var trxFromJunit = TrxSerializer.Serialize(junitResults); ``` The conversion preserves all compatible information between formats, including test names, outcomes, durations, error messages, and output. +### How do I automatically detect the format of a test result file? + +Use the `Serializer` class to automatically identify and deserialize test result files: + +```csharp +// Identify format without deserializing +var format = Serializer.Identify(File.ReadAllText("test-results.xml")); +if (format == TestResultFormat.Trx) +{ + Console.WriteLine("This is a TRX file"); +} +else if (format == TestResultFormat.JUnit) +{ + Console.WriteLine("This is a JUnit XML file"); +} + +// Or just deserialize directly - format is detected automatically +var results = Serializer.Deserialize(File.ReadAllText("test-results.xml")); +``` + +The `Serializer.Identify()` method analyzes the XML structure to determine if it's a TRX file (by checking for the +Visual Studio namespace and TestRun root element) or a JUnit file (by checking for testsuites or testsuite root +elements). It returns `TestResultFormat.Unknown` for invalid or unrecognized formats. + ### How do I set custom properties or metadata? Currently, the library supports the standard metadata fields available in `TestResults` and `TestResult` classes: diff --git a/src/DemaConsulting.TestResults/IO/Serializer.cs b/src/DemaConsulting.TestResults/IO/Serializer.cs new file mode 100644 index 0000000..59b9a27 --- /dev/null +++ b/src/DemaConsulting.TestResults/IO/Serializer.cs @@ -0,0 +1,121 @@ +// Copyright(c) 2025 DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using System.Xml.Linq; + +namespace DemaConsulting.TestResults.IO; + +/// +/// Test result format types +/// +public enum TestResultFormat +{ + /// + /// Unknown format + /// + Unknown, + + /// + /// TRX (Visual Studio Test Results) format + /// + Trx, + + /// + /// JUnit XML format + /// + JUnit +} + +/// +/// General purpose serializer for test results +/// +public static class Serializer +{ +#pragma warning disable S1075 // URIs should not be hardcoded - This is an XML namespace URI, not a file path + /// + /// TRX namespace for identifying TRX files + /// + private const string TrxNamespaceUri = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010"; +#pragma warning restore S1075 + + /// + /// Identifies the test result format based on the contents + /// + /// The test result file contents + /// The identified test result format + public static TestResultFormat Identify(string contents) + { + if (string.IsNullOrWhiteSpace(contents)) + { + return TestResultFormat.Unknown; + } + + try + { + // Parse the XML document + var doc = XDocument.Parse(contents); + var root = doc.Root; + + if (root == null) + { + return TestResultFormat.Unknown; + } + + // Check for TRX format by namespace and root element + if (root.Name.Namespace == TrxNamespaceUri && root.Name.LocalName == "TestRun") + { + return TestResultFormat.Trx; + } + + // Check for JUnit format by root element names + if (root.Name.LocalName == "testsuites" || root.Name.LocalName == "testsuite") + { + return TestResultFormat.JUnit; + } + + return TestResultFormat.Unknown; + } + catch + { + // If XML parsing fails, format is unknown + return TestResultFormat.Unknown; + } + } + + /// + /// Deserializes test result contents to TestResults using the appropriate deserializer + /// + /// The test result file contents + /// Deserialized test results + /// Thrown when format cannot be identified or deserialization fails + public static TestResults Deserialize(string contents) + { + // Identify the format + var format = Identify(contents); + + // Deserialize based on format + return format switch + { + TestResultFormat.Trx => TrxSerializer.Deserialize(contents), + TestResultFormat.JUnit => JUnitSerializer.Deserialize(contents), + _ => throw new InvalidOperationException("Unable to identify test result format") + }; + } +} diff --git a/test/DemaConsulting.TestResults.Tests/IO/SerializerTests.cs b/test/DemaConsulting.TestResults.Tests/IO/SerializerTests.cs new file mode 100644 index 0000000..6cb67f0 --- /dev/null +++ b/test/DemaConsulting.TestResults.Tests/IO/SerializerTests.cs @@ -0,0 +1,385 @@ +// Copyright(c) 2025 DEMA Consulting +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +using DemaConsulting.TestResults.IO; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DemaConsulting.TestResults.Tests.IO; + +/// +/// Tests for Serializer class +/// +[TestClass] +public sealed class SerializerTests +{ + /// + /// Test that Identify correctly identifies TRX format + /// + [TestMethod] + public void Identify_TrxContent_ReturnsTrx() + { + // Create a sample TRX content + var trxContent = """ + + + + + + + + + + + + + + + + + + + + + + + + """; + + // Identify format + var format = Serializer.Identify(trxContent); + + // Verify it's identified as TRX + Assert.AreEqual(TestResultFormat.Trx, format); + } + + /// + /// Test that Identify correctly identifies JUnit format with testsuites root + /// + [TestMethod] + public void Identify_JUnitTestsuitesContent_ReturnsJUnit() + { + // Create a sample JUnit content with testsuites root + var junitContent = """ + + + + + + + """; + + // Identify format + var format = Serializer.Identify(junitContent); + + // Verify it's identified as JUnit + Assert.AreEqual(TestResultFormat.JUnit, format); + } + + /// + /// Test that Identify correctly identifies JUnit format with testsuite root + /// + [TestMethod] + public void Identify_JUnitTestsuiteContent_ReturnsJUnit() + { + // Create a sample JUnit content with testsuite root + var junitContent = """ + + + + + """; + + // Identify format + var format = Serializer.Identify(junitContent); + + // Verify it's identified as JUnit + Assert.AreEqual(TestResultFormat.JUnit, format); + } + + /// + /// Test that Identify returns Unknown for empty content + /// + [TestMethod] + public void Identify_EmptyContent_ReturnsUnknown() + { + // Identify format of empty string + var format = Serializer.Identify(string.Empty); + + // Verify it's identified as Unknown + Assert.AreEqual(TestResultFormat.Unknown, format); + } + + /// + /// Test that Identify returns Unknown for null content + /// + [TestMethod] + public void Identify_NullContent_ReturnsUnknown() + { + // Identify format of null string + var format = Serializer.Identify(null!); + + // Verify it's identified as Unknown + Assert.AreEqual(TestResultFormat.Unknown, format); + } + + /// + /// Test that Identify returns Unknown for whitespace content + /// + [TestMethod] + public void Identify_WhitespaceContent_ReturnsUnknown() + { + // Identify format of whitespace string + var format = Serializer.Identify(" \n\t "); + + // Verify it's identified as Unknown + Assert.AreEqual(TestResultFormat.Unknown, format); + } + + /// + /// Test that Identify returns Unknown for invalid XML + /// + [TestMethod] + public void Identify_InvalidXml_ReturnsUnknown() + { + // Create invalid XML content + var invalidXml = ""; + + // Identify format + var format = Serializer.Identify(invalidXml); + + // Verify it's identified as Unknown + Assert.AreEqual(TestResultFormat.Unknown, format); + } + + /// + /// Test that Identify returns Unknown for unrecognized XML format + /// + [TestMethod] + public void Identify_UnrecognizedXmlFormat_ReturnsUnknown() + { + // Create XML with unrecognized root element + var unrecognizedXml = """ + + + value + + """; + + // Identify format + var format = Serializer.Identify(unrecognizedXml); + + // Verify it's identified as Unknown + Assert.AreEqual(TestResultFormat.Unknown, format); + } + + /// + /// Test that Deserialize successfully deserializes TRX content + /// + [TestMethod] + public void Deserialize_TrxContent_ReturnsTestResults() + { + // Create a sample TRX content + var trxContent = """ + + + + + + + + + + + + + + + + + + + + + + + + """; + + // Deserialize + var results = Serializer.Deserialize(trxContent); + + // Verify results + Assert.IsNotNull(results); + Assert.AreEqual("Test Run", results.Name); + Assert.AreEqual("User", results.UserName); + Assert.HasCount(1, results.Results); + Assert.AreEqual("Test1", results.Results[0].Name); + Assert.AreEqual(TestOutcome.Passed, results.Results[0].Outcome); + } + + /// + /// Test that Deserialize successfully deserializes JUnit content + /// + [TestMethod] + public void Deserialize_JUnitContent_ReturnsTestResults() + { + // Create a sample JUnit content + var junitContent = """ + + + + + + Stack trace here + + + + """; + + // Deserialize + var results = Serializer.Deserialize(junitContent); + + // Verify results + Assert.IsNotNull(results); + Assert.AreEqual("Test Suite", results.Name); + Assert.HasCount(2, results.Results); + Assert.AreEqual("Test1", results.Results[0].Name); + Assert.AreEqual(TestOutcome.Passed, results.Results[0].Outcome); + Assert.AreEqual("Test2", results.Results[1].Name); + Assert.AreEqual(TestOutcome.Failed, results.Results[1].Outcome); + Assert.AreEqual("Test failed", results.Results[1].ErrorMessage); + } + + /// + /// Test that Deserialize can handle real TRX example file + /// + [TestMethod] + public void Deserialize_RealTrxExample_ReturnsTestResults() + { + // Load example TRX file + var trxContent = TestHelpers.GetEmbeddedResource( + "DemaConsulting.TestResults.Tests.IO.Examples.example1.trx"); + + // Identify and verify format + var format = Serializer.Identify(trxContent); + Assert.AreEqual(TestResultFormat.Trx, format); + + // Deserialize + var results = Serializer.Deserialize(trxContent); + + // Verify results + Assert.IsNotNull(results); + Assert.AreEqual("Sample TRX Import", results.Name); + Assert.AreEqual("Brian Mancini", results.UserName); + Assert.IsNotEmpty(results.Results); + } + + /// + /// Test that Deserialize handles multiple test outcomes from TRX + /// + [TestMethod] + public void Deserialize_TrxWithMultipleOutcomes_ParsesCorrectly() + { + // Create TRX content with different outcomes + var trxContent = """ + + + + + + + + + + Test failed + + + + + + + + + + + + + + + + + + + + + + + + + + """; + + // Deserialize + var results = Serializer.Deserialize(trxContent); + + // Verify results + Assert.IsNotNull(results); + Assert.HasCount(2, results.Results); + + var passedTest = results.Results[0]; + Assert.AreEqual("PassedTest", passedTest.Name); + Assert.AreEqual(TestOutcome.Passed, passedTest.Outcome); + + var failedTest = results.Results[1]; + Assert.AreEqual("FailedTest", failedTest.Name); + Assert.AreEqual(TestOutcome.Failed, failedTest.Outcome); + Assert.AreEqual("Test failed", failedTest.ErrorMessage); + } + + /// + /// Test that Deserialize handles JUnit with system output and error + /// + [TestMethod] + public void Deserialize_JUnitWithSystemOutput_ParsesCorrectly() + { + // Create JUnit content with system output + var junitContent = """ + + + + + Standard output + Standard error + + + + """; + + // Deserialize + var results = Serializer.Deserialize(junitContent); + + // Verify results + Assert.IsNotNull(results); + Assert.HasCount(1, results.Results); + Assert.AreEqual("Test1", results.Results[0].Name); + Assert.AreEqual("Standard output", results.Results[0].SystemOutput); + Assert.AreEqual("Standard error", results.Results[0].SystemError); + } +}