├── global.json ├── .gitattributes ├── src ├── package │ ├── packageIcon.png │ ├── JUnitXml.TestLogger.props │ ├── JUnitXml.TestLogger.nuspec │ └── package.csproj ├── JUnit.Xml.TestLogger │ ├── Assembly.cs │ ├── JUnit.Xml.TestLogger.csproj │ ├── JUnitXmlTestLogger.cs │ └── JunitXmlSerializer.cs └── JUnit.Xml.TestLogger.TestAdapter │ └── JUnit.Xml.TestLogger.TestAdapter.csproj ├── docs ├── jenkins-recommendation.md ├── assets │ ├── gitlab-test-popup-with-default-failure.png │ ├── gitlab-test-popup-with-verbose-failure.png │ ├── gitlab-test-summary-with-class-option.png │ ├── gitlab-test-summary-with-default-option.png │ ├── circleci-test-expanded-with-failure-default.png │ ├── circleci-test-expanded-with-failure-verbose.png │ ├── circleci-test-collapsed-with-methodname-class.png │ ├── circleci-test-collapsed-with-methodname-default.png │ └── TestResults.xml ├── gitlab-recommendation.md └── circleci-recommendation.md ├── .editorconfig ├── CHANGELOG.md ├── .travis.yml ├── NuGet.config ├── test ├── assets │ ├── NuGet.Debug.config │ ├── NuGet.Release.config │ ├── JUnit.Xml.TestLogger.NetCore.Tests │ │ ├── JUnit.Xml.TestLogger.NetCore.Tests.csproj │ │ ├── UnitTest2.cs │ │ └── UnitTest1.cs │ ├── JUnit.Xml.TestLogger.NetFull.Tests │ │ └── JUnit.Xml.TestLogger.NetFull.Tests.csproj │ ├── JUnit.Xml.TestLogger.NetMulti.Tests │ │ └── JUnit.Xml.TestLogger.NetMulti.Tests.csproj │ └── JUnit.xsd ├── JUnit.Xml.TestLogger.UnitTests │ ├── JUnit.Xml.TestLogger.UnitTests.csproj │ └── JUnitXmlTestSerializerTests.cs └── JUnit.Xml.TestLogger.AcceptanceTests │ ├── JUnitTestLoggerResultDirectoryAcceptanceTests.cs │ ├── JunitXmlValidator.cs │ ├── JUnitTestLoggerPathTests.cs │ ├── JUnit.Xml.TestLogger.AcceptanceTests.csproj │ ├── DotnetTestFixture.cs │ ├── JUnitTestLoggerAcceptanceTests.cs │ └── JUnitTestLoggerFormatOptionsAcceptanceTests.cs ├── scripts ├── stylecop.json ├── dependencies.props ├── stylecop.test.ruleset ├── stylecop.ruleset └── settings.targets ├── .github ├── dependabot.yml └── workflows │ └── dotnet.yml ├── .appveyor.yml ├── LICENSE.md ├── .vscode ├── launch.json └── tasks.json ├── .gitignore ├── README.md └── junit.testlogger.sln /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "allowPrerelease": false 4 | } 5 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto -------------------------------------------------------------------------------- /src/package/packageIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spekt/junit.testlogger/HEAD/src/package/packageIcon.png -------------------------------------------------------------------------------- /docs/jenkins-recommendation.md: -------------------------------------------------------------------------------- 1 | # Jenkins Recommendation 2 | 3 | **TODO** If you are a Jenkins user who is willing to help, please see Issue #38 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root=true 3 | 4 | [*] 5 | trim_trailing_whitespace = true 6 | indent_style = space 7 | 8 | [*.cs] 9 | indent_size = 4 -------------------------------------------------------------------------------- /docs/assets/gitlab-test-popup-with-default-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spekt/junit.testlogger/HEAD/docs/assets/gitlab-test-popup-with-default-failure.png -------------------------------------------------------------------------------- /docs/assets/gitlab-test-popup-with-verbose-failure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spekt/junit.testlogger/HEAD/docs/assets/gitlab-test-popup-with-verbose-failure.png -------------------------------------------------------------------------------- /docs/assets/gitlab-test-summary-with-class-option.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spekt/junit.testlogger/HEAD/docs/assets/gitlab-test-summary-with-class-option.png -------------------------------------------------------------------------------- /docs/assets/gitlab-test-summary-with-default-option.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spekt/junit.testlogger/HEAD/docs/assets/gitlab-test-summary-with-default-option.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | A changelog is maintained on the releases page of the [JUnit Test Logger GitHub repository](https://github.com/spekt/junit.testlogger/). 4 | -------------------------------------------------------------------------------- /docs/assets/circleci-test-expanded-with-failure-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spekt/junit.testlogger/HEAD/docs/assets/circleci-test-expanded-with-failure-default.png -------------------------------------------------------------------------------- /docs/assets/circleci-test-expanded-with-failure-verbose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spekt/junit.testlogger/HEAD/docs/assets/circleci-test-expanded-with-failure-verbose.png -------------------------------------------------------------------------------- /docs/assets/circleci-test-collapsed-with-methodname-class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spekt/junit.testlogger/HEAD/docs/assets/circleci-test-collapsed-with-methodname-class.png -------------------------------------------------------------------------------- /docs/assets/circleci-test-collapsed-with-methodname-default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spekt/junit.testlogger/HEAD/docs/assets/circleci-test-collapsed-with-methodname-default.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | 3 | matrix: 4 | include: 5 | - dotnet: 3.1 6 | mono: latest 7 | 8 | solution: junit.testlogger.sln 9 | 10 | script: 11 | - ./build.sh 12 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/assets/NuGet.Debug.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/assets/NuGet.Release.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /scripts/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "documentationRules": { 5 | "companyName": "Spekt Contributors", 6 | "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the MIT license. See LICENSE file in the project root for full license information.", 7 | "xmlHeader": false, 8 | "fileNamingConvention": "metadata" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /scripts/dependencies.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.0.0 5 | 15.7.2 6 | 4.9.0 7 | 3.1.140 8 | 9 | 10 | 15.5.0 11 | 3.10.1 12 | 3.10.0 13 | 14 | 1.1.118 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/JUnit.Xml.TestLogger.UnitTests/JUnit.Xml.TestLogger.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | ..\..\ 4 | true 5 | 6 | 7 | 8 | 9 | netcoreapp3.1 10 | true 11 | true 12 | false 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/JUnit.Xml.TestLogger/Assembly.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | // 5 | // Skip code analysis errors. 6 | // 7 | 8 | using System.Diagnostics.CodeAnalysis; 9 | 10 | [assembly: ExcludeFromCodeCoverage] 11 | 12 | namespace System.Diagnostics.CodeAnalysis 13 | { 14 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Event | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] 15 | internal sealed class ExcludeFromCodeCoverageAttribute : Attribute { } 16 | } 17 | -------------------------------------------------------------------------------- /scripts/stylecop.test.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /scripts/stylecop.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '3.0.{build}' 2 | image: Visual Studio 2019 3 | 4 | clone_depth: 1 5 | 6 | build_script: 7 | - cmd: dotnet pack -p:PackageVersion=%APPVEYOR_BUILD_VERSION% 8 | - cmd: dotnet pack -c Release -p:PackageVersion=%APPVEYOR_BUILD_VERSION% 9 | - cmd: dotnet test -p:PackageVersion=%APPVEYOR_BUILD_VERSION% test/JUnit.Xml.TestLogger.UnitTests/JUnit.Xml.TestLogger.UnitTests.csproj 10 | - cmd: dotnet test -p:PackageVersion=%APPVEYOR_BUILD_VERSION% test/JUnit.Xml.TestLogger.AcceptanceTests/JUnit.Xml.TestLogger.AcceptanceTests.csproj 11 | 12 | test: off 13 | 14 | artifacts: 15 | - path: 'src\package\bin\Release\*.nupkg' 16 | 17 | deploy: 18 | provider: NuGet 19 | server: https://www.myget.org/F/spekt/api/v2 20 | api_key: 21 | secure: 2C7HbSlU1kcOJ3nzZCpKR97cfWAg/8t38XDf8ywCbJI1ymt93ulfPqT67ugWuMla 22 | artifact: /.*\.nupkg/ 23 | skip_symbols: true 24 | -------------------------------------------------------------------------------- /test/assets/JUnit.Xml.TestLogger.NetCore.Tests/JUnit.Xml.TestLogger.NetCore.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ..\..\..\ 5 | 6 | 7 | 8 | 9 | netcoreapp3.1 10 | 11 | 12 | false 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/JUnit.Xml.TestLogger/JUnit.Xml.TestLogger.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | ..\..\ 4 | 5 | 6 | 7 | 8 | netstandard1.5 9 | Microsoft.VisualStudio.TestPlatform.Extension.JUnit.Xml.TestLogger 10 | 11 | 12 | 13 | Microsoft.VisualStudio.TestPlatform.Extension.JUnit.Xml.TestLogger 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/JUnit.Xml.TestLogger/JUnitXmlTestLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Microsoft.VisualStudio.TestPlatform.Extension.Junit.Xml.TestLogger 5 | { 6 | using Microsoft.VisualStudio.TestPlatform.ObjectModel; 7 | using Spekt.TestLogger; 8 | 9 | [FriendlyName(FriendlyName)] 10 | [ExtensionUri(ExtensionUri)] 11 | public class JUnitXmlTestLogger : TestLogger 12 | { 13 | /// 14 | /// Uri used to uniquely identify the logger. 15 | /// 16 | public const string ExtensionUri = "logger://Microsoft/TestPlatform/JUnitXmlLogger/v1"; 17 | 18 | /// 19 | /// Alternate user friendly string to uniquely identify the console logger. 20 | /// 21 | public const string FriendlyName = "junit"; 22 | 23 | public JUnitXmlTestLogger() 24 | : base(new JunitXmlSerializer()) 25 | { 26 | } 27 | 28 | protected override string DefaultTestResultFile => "TestResults.xml"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/package/JUnitXml.TestLogger.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Microsoft.VisualStudio.TestPlatform.Extension.JUnit.Xml.TestLogger.dll 6 | PreserveNewest 7 | False 8 | 9 | 10 | Microsoft.VisualStudio.TestPlatform.Extension.JUnit.Xml.TestAdapter.dll 11 | PreserveNewest 12 | False 13 | 14 | 15 | Spekt.TestLogger.dll 16 | PreserveNewest 17 | False 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/package/JUnitXml.TestLogger.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | JunitXml.TestLogger 5 | $version$ 6 | JunitXml.TestLogger 7 | Siphonophora,codito,faizan2304,smadala,lahma 8 | Siphonophora,codito,faizan2304,smadala 9 | https://github.com/spekt/junit.testlogger 10 | Xml logger for JUnit v5 compliant xml report when test is running with "dotnet test" or "dotnet vstest". 11 | packageIcon.png 12 | MIT 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/JUnit.Xml.TestLogger.TestAdapter/JUnit.Xml.TestLogger.TestAdapter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ..\..\ 5 | 6 | 7 | 8 | 9 | netstandard1.5 10 | Microsoft.VisualStudio.TestPlatform.Extension.JUnit.Xml.TestAdapter 11 | 12 | 13 | false 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/assets/JUnit.Xml.TestLogger.NetFull.Tests/JUnit.Xml.TestLogger.NetFull.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ..\..\..\ 5 | 6 | 7 | 8 | 9 | net46 10 | /usr/lib/mono/4.5/ 11 | 12 | 13 | false 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | UnitTest1.cs 26 | 27 | 28 | UnitTest2.cs 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/assets/JUnit.Xml.TestLogger.NetMulti.Tests/JUnit.Xml.TestLogger.NetMulti.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ..\..\..\ 5 | 6 | 7 | 8 | 9 | net46;netcoreapp3.1 10 | /usr/lib/mono/4.5/ 11 | 12 | 13 | false 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | UnitTest1.cs 26 | 27 | 28 | UnitTest2.cs 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/test/JUnit.Xml.TestLogger.AcceptanceTests/bin/Debug/netcoreapp2.0/JUnit.Xml.TestLogger.AcceptanceTests.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/test/JUnit.Xml.TestLogger.AcceptanceTests", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ,] 28 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/junit.testlogger.sln" 11 | ], 12 | "problemMatcher": "$msCompile", 13 | "group": { 14 | "kind": "build", 15 | "isDefault": true 16 | } 17 | }, 18 | { 19 | "label": "pack", 20 | "command": "dotnet", 21 | "type": "process", 22 | "args": [ 23 | "pack", 24 | "${workspaceFolder}/junit.testlogger.sln" 25 | ], 26 | "problemMatcher": "$msCompile", 27 | "group": "build" 28 | }, 29 | { 30 | "label": "test-unit", 31 | "dependsOn": [ 32 | "pack" 33 | ], 34 | "command": "dotnet", 35 | "type": "process", 36 | "args": [ 37 | "test", 38 | "${workspaceFolder}/test/JUnit.Xml.TestLogger.UnitTests/JUnit.Xml.TestLogger.UnitTests.csproj" 39 | ], 40 | "problemMatcher": "$msCompile", 41 | "group": "test" 42 | }, 43 | { 44 | "label": "test-all", 45 | "dependsOn": [ 46 | "test-unit" 47 | ], 48 | "command": "dotnet", 49 | "type": "process", 50 | "args": [ 51 | "test", 52 | "${workspaceFolder}/test/JUnit.Xml.TestLogger.AcceptanceTests/JUnit.Xml.TestLogger.AcceptanceTests.csproj" 53 | ], 54 | "problemMatcher": "$msCompile", 55 | "group": { 56 | "kind": "test", 57 | "isDefault": true 58 | } 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /test/JUnit.Xml.TestLogger.AcceptanceTests/JUnitTestLoggerResultDirectoryAcceptanceTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace JUnit.Xml.TestLogger.AcceptanceTests 5 | { 6 | using System; 7 | using System.IO; 8 | using Microsoft.VisualStudio.TestTools.UnitTesting; 9 | 10 | /// 11 | /// Acceptance tests evaluate the most recent output of the build.ps1 script, NOT the most 12 | /// recent build performed by visual studio or dotnet.build 13 | /// 14 | /// These acceptance tests look at the directory name argument. 15 | /// 16 | [TestClass] 17 | public class JUnitTestLoggerResultDirectoryAcceptanceTests 18 | { 19 | [ClassInitialize] 20 | public static void SuiteInitialize(TestContext context) 21 | { 22 | DotnetTestFixture.RootDirectory = Path.GetFullPath( 23 | Path.Combine( 24 | Environment.CurrentDirectory, 25 | "..", 26 | "..", 27 | "..", 28 | "..", 29 | "assets", 30 | "JUnit.Xml.TestLogger.NetCore.Tests")); 31 | DotnetTestFixture.TestAssemblyName = "JUnit.Xml.TestLogger.NetCore.Tests.dll"; 32 | var testResultsPath = Path.Combine(DotnetTestFixture.RootDirectory, "artifacts"); 33 | DotnetTestFixture.Execute("test-results.xml", testResultsPath); 34 | } 35 | 36 | [TestMethod] 37 | public void TestRunWithResultDirectoryAndFileNameShouldCreateResultsFile() 38 | { 39 | var expectedResultsPath = Path.Combine(DotnetTestFixture.RootDirectory, "artifacts", "test-results.xml"); 40 | Assert.IsTrue(File.Exists(expectedResultsPath), $"Results file at '{expectedResultsPath}' not found."); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/dotnet.yml: -------------------------------------------------------------------------------- 1 | name: .NET 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, windows-latest] 14 | runs-on: ${{ matrix.os }} 15 | env: 16 | APP_BUILD_VERSION: ${{ format('3.1.{0}', github.run_number) }} 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Setup .NET 7.0 20 | uses: actions/setup-dotnet@v1 21 | with: 22 | dotnet-version: 7.0.x 23 | - name: Setup .NET 3.1.x 24 | uses: actions/setup-dotnet@v1 25 | with: 26 | dotnet-version: "3.1.x" 27 | - name: Package (debug) 28 | run: dotnet pack -p:PackageVersion=${{ env.APP_BUILD_VERSION }} 29 | - name: Package (release) 30 | run: dotnet pack -c Release -p:PackageVersion=${{ env.APP_BUILD_VERSION }} 31 | - name: Unit test 32 | run: dotnet test -p:PackageVersion=${{ env.APP_BUILD_VERSION }} test/JUnit.Xml.TestLogger.UnitTests/JUnit.Xml.TestLogger.UnitTests.csproj -p:CollectCoverage=true -p:CoverletOutputFormat=opencover 33 | - name: Acceptance test 34 | if: ${{ matrix.os == 'windows-latest' }} 35 | run: dotnet test -p:PackageVersion=${{ env.APP_BUILD_VERSION }} test/JUnit.Xml.TestLogger.AcceptanceTests/JUnit.Xml.TestLogger.AcceptanceTests.csproj 36 | - name: Codecov 37 | uses: codecov/codecov-action@v3.1.0 38 | with: 39 | files: test/JUnit.Xml.TestLogger.UnitTests/coverage.opencover.xml 40 | - name: Publish packages 41 | if: ${{ github.event_name == 'push' && matrix.os == 'windows-latest' }} 42 | run: | 43 | $packageFile = (Get-ChildItem src/package/bin/Release/*.nupkg).FullName 44 | dotnet nuget push "$packageFile" --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/spekt/index.json 45 | dotnet nuget push "$packageFile" --api-key ${{ secrets.SPEKT_MYGET_KEY }} --source https://www.myget.org/F/spekt/api/v3/index.json 46 | -------------------------------------------------------------------------------- /docs/gitlab-recommendation.md: -------------------------------------------------------------------------------- 1 | # GitLab CI/CD Recommendation 2 | 3 | GitLab uses just a few pieces of the XML report to generate the displayed user interface. The formatting options available in this project can improve the usefulness of this UI. 4 | 5 | ## Default Display 6 | 7 | The summary view shows passing and failing tests by method name (including any parameters). GitLab generates its summary of New failing tests, existing failing tests, and newly passing tests based on this string. 8 | 9 | ![Default Test Summary](assets/gitlab-test-summary-with-default-option.png) 10 | 11 | The popup displayed when clicking on a test, shows only the body of the failure. 12 | 13 | ![Default Popup](assets/gitlab-test-popup-with-default-failure.png) 14 | 15 | ## Improved Output With Formatting Options 16 | 17 | The test summary can be modified by setting the method format. The option below used 'MethodFormat=Class'. This can be particularly helpful when using test fixture data (i.e. there are parameters passed to the class) which you would like to display. 18 | 19 | ![Test Summary with Class Option](assets/gitlab-test-summary-with-class-option.png) 20 | 21 | The popup is much more useful with the inclusion of the 'Expected X, Actual Y' data. This is added to the failure body using 'FailureBodyFormat=Verbose' 22 | 23 | ![Verbose Popup](assets/gitlab-test-popup-with-verbose-failure.png) 24 | 25 | ## Example .gitlab-ci.yml 26 | 27 | Below is example .yml which implements the options shown above. Additionally, this example collects the output from all test projects into a single folder, and uploads their reports to gitlab. 28 | 29 | ``` yml 30 | Test: 31 | stage: Test 32 | script: 33 | - 'dotnet test --test-adapter-path:. --logger:"junit;LogFilePath=..\artifacts\{assembly}-test-result.xml;MethodFormat=Class;FailureBodyFormat=Verbose"' 34 | artifacts: 35 | when: always 36 | paths: 37 | - .\artifacts\*test-result.xml 38 | reports: 39 | junit: 40 | - .\artifacts\*test-result.xml 41 | ``` 42 | 43 | ## Notes 44 | 45 | Screen shots and behavior are current as of GitLab Enterprise Edition 13.0.0-pre. 46 | -------------------------------------------------------------------------------- /test/JUnit.Xml.TestLogger.AcceptanceTests/JunitXmlValidator.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace JUnit.Xml.TestLogger.AcceptanceTests 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using System.Xml; 13 | using System.Xml.Linq; 14 | using System.Xml.Schema; 15 | 16 | public class JunitXmlValidator 17 | { 18 | /// 19 | /// Field is provided only to simplify debugging test failures. 20 | /// 21 | private readonly List failures = new List(); 22 | 23 | public bool IsValid(string xml) 24 | { 25 | var xmlReader = new StringReader(xml); 26 | var xsdReader = new StringReader( 27 | File.ReadAllText( 28 | Path.Combine("..", "..", "..", "..", "assets", "JUnit.xsd"))); 29 | 30 | var schema = XmlSchema.Read( 31 | xsdReader, 32 | (sender, args) => { throw new XmlSchemaValidationException(args.Message, args.Exception); }); 33 | 34 | var xmlReaderSettings = new XmlReaderSettings(); 35 | xmlReaderSettings.Schemas.Add(schema); 36 | xmlReaderSettings.ValidationType = ValidationType.Schema; 37 | 38 | var veh = new ValidationEventHandler(this.XmlValidationEventHandler); 39 | 40 | xmlReaderSettings.ValidationEventHandler += veh; 41 | using (XmlReader reader = XmlReader.Create(xmlReader, xmlReaderSettings)) 42 | { 43 | while (reader.Read()) 44 | { 45 | } 46 | } 47 | 48 | xmlReaderSettings.ValidationEventHandler -= veh; 49 | 50 | return this.failures.Any() == false; 51 | } 52 | 53 | public bool IsValid(XDocument doc) => this.IsValid(doc.ToString()); 54 | 55 | private void XmlValidationEventHandler(object sender, ValidationEventArgs e) 56 | { 57 | this.failures.Add(e.Exception); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/JUnit.Xml.TestLogger.AcceptanceTests/JUnitTestLoggerPathTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace JUnit.Xml.TestLogger.AcceptanceTests 5 | { 6 | using System; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Xml.Linq; 10 | using System.Xml.XPath; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | 13 | /// 14 | /// Acceptance tests evaluate the most recent output of the build.ps1 script, NOT the most 15 | /// recent build performed by visual studio or dotnet.build 16 | /// 17 | /// These acceptance tests look at the path parameter and tokens. 18 | /// 19 | [TestClass] 20 | public class JUnitTestLoggerPathTests 21 | { 22 | public JUnitTestLoggerPathTests() 23 | { 24 | } 25 | 26 | [ClassInitialize] 27 | public static void SuiteInitialize(TestContext context) 28 | { 29 | DotnetTestFixture.RootDirectory = Path.GetFullPath( 30 | Path.Combine( 31 | Environment.CurrentDirectory, 32 | "..", 33 | "..", 34 | "..", 35 | "..", 36 | "assets", 37 | "JUnit.Xml.TestLogger.NetMulti.Tests")); 38 | DotnetTestFixture.TestAssemblyName = "JUnit.Xml.TestLogger.NetMulti.Tests.dll"; 39 | DotnetTestFixture.Execute("{assembly}.{framework}.test-results.xml"); 40 | } 41 | 42 | [TestMethod] 43 | public void TestRunWithLoggerAndFilePathShouldCreateResultsFile() 44 | { 45 | string[] expectedResultsFiles = new string[] 46 | { 47 | Path.Combine(DotnetTestFixture.RootDirectory, "JUnit.Xml.TestLogger.NetMulti.Tests.NETFramework46.test-results.xml"), 48 | Path.Combine(DotnetTestFixture.RootDirectory, "JUnit.Xml.TestLogger.NetMulti.Tests.NETCoreApp31.test-results.xml") 49 | }; 50 | foreach (string resultsFile in expectedResultsFiles) 51 | { 52 | Assert.IsTrue(File.Exists(resultsFile), $"{resultsFile} does not exist."); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/JUnit.Xml.TestLogger.AcceptanceTests/JUnit.Xml.TestLogger.AcceptanceTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ..\..\ 5 | true 6 | 7 | 8 | 9 | 10 | netcoreapp3.1 11 | true 12 | true 13 | false 14 | 15 | 16 | 17 | 18 | $(MSBuildThisFileDirectory)../assets/NuGet.$(Configuration).config 19 | $(MSBuildThisFileDirectory)../assets/JUnit.Xml.TestLogger.NetFull.Tests/JUnit.Xml.TestLogger.NetFull.Tests.csproj 20 | $(MSBuildThisFileDirectory)../assets/JUnit.Xml.TestLogger.NetCore.Tests/JUnit.Xml.TestLogger.NetCore.Tests.csproj 21 | $(MSBuildThisFileDirectory)../assets/JUnit.Xml.TestLogger.NetMulti.Tests/JUnit.Xml.TestLogger.NetMulti.Tests.csproj 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ############################################################################ 2 | # For ignoring files that appear under a specific folder, you should 3 | # usually add a .gitignore file to that specific folder. 4 | # This file is reserved for common file patterns that should be 5 | # ignored everywhere. 6 | ############################################################################ 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | *.sln.metaproj 16 | *.sln.metaproj.tmp 17 | .vs/ 18 | *.cache 19 | 20 | # Build results 21 | [Bb]in/ 22 | [Oo]bj/ 23 | !**/LocProjects/[Bb]in/ 24 | 25 | *.tlog 26 | msbuild*.log 27 | msbuild*.wrn 28 | msbuild*.err 29 | build*.log 30 | build*.wrn 31 | build*.err 32 | *loggerFile.xml 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # Visual C++ cache files 39 | ipch/ 40 | *.aps 41 | *.ncb 42 | *.opensdf 43 | *.sdf 44 | *.cachefile 45 | 46 | # Visual Studio profiler 47 | *.psess 48 | *.vsp 49 | *.vspx 50 | 51 | # ReSharper is a .NET coding add-in 52 | _ReSharper*/ 53 | *.[Rr]e[Ss]harper 54 | 55 | # DotCover is a Code Coverage Tool 56 | *.dotCover 57 | 58 | # NCrunch 59 | *.ncrunch* 60 | .*crunch*.local.xml 61 | 62 | # DocProject is a documentation generator add-in 63 | DocProject/buildhelp/ 64 | DocProject/Help/*.HxT 65 | DocProject/Help/*.HxC 66 | DocProject/Help/*.hhc 67 | DocProject/Help/*.hhk 68 | DocProject/Help/*.hhp 69 | DocProject/Help/Html2 70 | DocProject/Help/html 71 | 72 | # Backup & report files from converting an old project file to a newer 73 | # Visual Studio version. Backup files are not needed, because we have git ;-) 74 | _UpgradeReport_Files/ 75 | Backup*/ 76 | UpgradeLog*.XML 77 | UpgradeLog*.htm 78 | 79 | #GlobalAssemblyInfo produced for Microbuild 80 | GlobalAssemblyInfo.cs 81 | 82 | #lock files for UWP projects 83 | project.lock.json 84 | project.fragment.lock.json 85 | 86 | # ========================= 87 | # Windows detritus 88 | # ========================= 89 | 90 | # Windows image file caches 91 | Thumbs.db 92 | ehthumbs.db 93 | 94 | 95 | # =========================== 96 | # Custom ignores 97 | # =========================== 98 | packages 99 | nugetPackage 100 | [tT]ools 101 | 102 | # Rider 103 | .idea 104 | 105 | # Test results 106 | test/**/*test-results.xml 107 | CodeMaid.config 108 | -------------------------------------------------------------------------------- /test/JUnit.Xml.TestLogger.UnitTests/JUnitXmlTestSerializerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace JUnit.Xml.TestLogger.UnitTests 5 | { 6 | using System; 7 | using System.Diagnostics; 8 | using System.IO; 9 | using System.Linq; 10 | using System.Xml.Linq; 11 | using System.Xml.XPath; 12 | using Microsoft.VisualStudio.TestPlatform.Extension.Junit.Xml.TestLogger; 13 | using Microsoft.VisualStudio.TestTools.UnitTesting; 14 | using TestSuite = Microsoft.VisualStudio.TestPlatform.Extension.Junit.Xml.TestLogger.JunitXmlSerializer.TestSuite; 15 | 16 | [TestClass] 17 | public class JUnitXmlTestSerializerTests 18 | { 19 | private const string DummyTestResultsDirectory = "/tmp/testresults"; 20 | 21 | [TestMethod] 22 | public void InitializeShouldThrowIfEventsIsNull() 23 | { 24 | Assert.ThrowsException(() => new JUnitXmlTestLogger().Initialize(null, DummyTestResultsDirectory)); 25 | } 26 | 27 | [TestMethod] 28 | public void CreateTestSuiteShouldReturnEmptyGroupsIfTestSuitesAreExclusive() 29 | { 30 | var suite1 = CreateTestSuite("a.b"); 31 | var suite2 = CreateTestSuite("c.d"); 32 | 33 | var result = JunitXmlSerializer.GroupTestSuites(new[] { suite1, suite2 }).ToArray(); 34 | 35 | Assert.AreEqual(2, result.Length); 36 | Assert.AreEqual("a", result[0].Name); 37 | Assert.AreEqual("c", result[1].Name); 38 | } 39 | 40 | [TestMethod] 41 | public void CreateTestSuiteShouldGroupTestSuitesByName() 42 | { 43 | var suites = new[] { CreateTestSuite("a.b.c"), CreateTestSuite("a.b.e"), CreateTestSuite("c.d") }; 44 | var expectedXmlForA = @""; 45 | var expectedXmlForC = @""; 46 | 47 | var result = JunitXmlSerializer.GroupTestSuites(suites).ToArray(); 48 | 49 | Assert.AreEqual(2, result.Length); 50 | Assert.AreEqual("c", result[0].Name); 51 | Assert.AreEqual(expectedXmlForC, result[0].Element.ToString(SaveOptions.DisableFormatting)); 52 | Assert.AreEqual("a", result[1].Name); 53 | Assert.AreEqual(expectedXmlForA, result[1].Element.ToString(SaveOptions.DisableFormatting)); 54 | } 55 | 56 | private static TestSuite CreateTestSuite(string name) 57 | { 58 | return new TestSuite 59 | { 60 | Element = new XElement("test-suite"), 61 | Name = "n", 62 | FullName = name, 63 | Total = 5, 64 | Passed = 1, 65 | Failed = 1, 66 | Inconclusive = 1, 67 | Skipped = 1, 68 | Error = 1 69 | }; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/package/package.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ..\..\ 5 | packageIcon.png 6 | 7 | 8 | 9 | 10 | netstandard1.5 11 | $(PackageVersion) 12 | JUnitXml.TestLogger 13 | 14 | 15 | false 16 | false 17 | false 18 | false 19 | false 20 | false 21 | false 22 | false 23 | false 24 | 25 | 26 | false 27 | false 28 | false 29 | false 30 | true 31 | 32 | 33 | $(NoWarn);2008;NU5127 34 | 35 | 36 | bin\$(Configuration)\$(TargetFramework)\JUnitXml.TestLogger.nuspec 37 | version=$(Version) 38 | false 39 | false 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | PreserveNewest 54 | 55 | 56 | PreserveNewest 57 | 58 | 59 | 60 | 61 | 62 | Always 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /scripts/settings.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildThisFileDirectory)../../ 5 | 3.0.0 6 | 7 | 8 | 10 | $(SourcePrefix)-dev 11 | $(SourcePrefix)-dev 12 | true 13 | false 14 | false 15 | false 16 | 17 | 24 | true 25 | $(NoWarn),1573,1591,1712 26 | 27 | 28 | true 29 | 30 | 31 | 32 | $(DefineConstants);RELEASE 33 | 34 | 35 | 36 | $(DefineConstants);CODE_ANALYSIS 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | stylecop.json 45 | 46 | 47 | 48 | $(StylecopVersion) 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | $(MSTestVersion) 58 | 59 | 60 | $(MSTestVersion) 61 | 62 | 63 | $(MoqVersion) 64 | 65 | 66 | $(NETTestSdkVersion) 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | $(SourceRoot)scripts/stylecop.ruleset 75 | $(SourceRoot)scripts/stylecop.test.ruleset 76 | 77 | 78 | -------------------------------------------------------------------------------- /test/assets/JUnit.Xml.TestLogger.NetCore.Tests/UnitTest2.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | 5 | namespace JUnit.Xml.TestLogger.Tests2 6 | { 7 | [TestFixture] 8 | public class UnitTest1 9 | { 10 | [Test] 11 | [Description("Passing test description")] 12 | public async Task PassTest11() 13 | { 14 | await Task.Delay(TimeSpan.FromMilliseconds(400)); 15 | } 16 | 17 | [Test] 18 | public void FailTest11() 19 | { 20 | Assert.False(true); 21 | } 22 | 23 | [Test] 24 | public void Inconclusive() 25 | { 26 | Assert.Inconclusive("test inconclusive"); 27 | } 28 | 29 | [Test] 30 | [Ignore("ignore reason")] 31 | public void Ignored() 32 | { 33 | } 34 | } 35 | 36 | public class UnitTest2 37 | { 38 | [Test] 39 | [Category("passing category")] 40 | public void PassTest21() 41 | { 42 | Assert.That(2, Is.EqualTo(2)); 43 | } 44 | 45 | [Test] 46 | [Category("failing category")] 47 | public void FailTest22() 48 | { 49 | Assert.False(true); 50 | } 51 | 52 | [Test] 53 | public void Inconclusive() 54 | { 55 | Assert.Inconclusive(); 56 | } 57 | 58 | [Test] 59 | [Ignore("ignore reason")] 60 | public void IgnoredTest() 61 | { 62 | } 63 | 64 | [Test] 65 | public void WarningTest() 66 | { 67 | Assert.Warn("Warning"); 68 | } 69 | 70 | [Test] 71 | [Explicit] 72 | public void ExplicitTest() 73 | { 74 | } 75 | } 76 | 77 | [TestFixture] 78 | public class SuccessFixture 79 | { 80 | [Test] 81 | public void SuccessTest() 82 | { 83 | } 84 | } 85 | 86 | [TestFixture] 87 | public class SuccessAndInconclusiveFixture 88 | { 89 | [Test] 90 | public void SuccessTest() 91 | { 92 | } 93 | 94 | [Test] 95 | public void InconclusiveTest() 96 | { 97 | Assert.Inconclusive(); 98 | } 99 | } 100 | 101 | [TestFixture] 102 | public class FailingOneTimeSetUp 103 | { 104 | [OneTimeSetUp] 105 | public void OneTimeSetUp() 106 | { 107 | throw new InvalidOperationException(); 108 | } 109 | 110 | [Test] 111 | public void TestA() 112 | { 113 | } 114 | } 115 | 116 | [TestFixture] 117 | public class FailingTestSetup 118 | { 119 | [SetUp] 120 | public void SetUp() 121 | { 122 | throw new InvalidOperationException(); 123 | } 124 | 125 | [Test] 126 | public void TestB() 127 | { 128 | } 129 | } 130 | 131 | [TestFixture] 132 | public class FailingTearDown 133 | { 134 | [TearDown] 135 | public void TearDown() 136 | { 137 | throw new InvalidOperationException(); 138 | } 139 | 140 | [Test] 141 | public void TestC() 142 | { 143 | } 144 | } 145 | 146 | [TestFixture] 147 | public class FailingOneTimeTearDown 148 | { 149 | [OneTimeTearDown] 150 | public void OneTimeTearDown() 151 | { 152 | throw new InvalidOperationException(); 153 | } 154 | 155 | [Test] 156 | public void TestD() 157 | { 158 | } 159 | } 160 | 161 | [TestFixture] 162 | [TestFixtureSource("FixtureArgs")] 163 | public class ParametrizedFixture 164 | { 165 | public ParametrizedFixture(string word, int num) 166 | { 167 | } 168 | 169 | [Test] 170 | public void TestE() 171 | { 172 | } 173 | 174 | static object[] FixtureArgs = 175 | { 176 | new object[] {"Question", 1}, 177 | new object[] {"Answer", 42} 178 | }; 179 | } 180 | 181 | [TestFixture] 182 | public class ParametrizedTestCases 183 | { 184 | [Test] 185 | public void TestData([Values(1, 2)] int x, [Values("A", "B")] string s) 186 | { 187 | Assert.That(x, Is.Not.EqualTo(2), "failing for second case"); 188 | Assert.That(s, Is.Not.Null); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /test/JUnit.Xml.TestLogger.AcceptanceTests/DotnetTestFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace JUnit.Xml.TestLogger.AcceptanceTests 5 | { 6 | using System; 7 | using System.Diagnostics; 8 | using System.IO; 9 | 10 | public class DotnetTestFixture 11 | { 12 | private const string DotnetVersion = "netcoreapp3.1"; 13 | 14 | public static string RootDirectory { get; set; } = Path.GetFullPath( 15 | Path.Combine( 16 | Environment.CurrentDirectory, 17 | "..", 18 | "..", 19 | "..", 20 | "..", 21 | "assets", 22 | "JUnit.Xml.TestLogger.NetCore.Tests")); 23 | 24 | public static string TestAssemblyName { get; set; } = "JUnit.Xml.TestLogger.NetCore.Tests.dll"; 25 | 26 | public static string TestAssembly 27 | { 28 | get 29 | { 30 | #if DEBUG 31 | var config = "Debug"; 32 | #else 33 | var config = "Release"; 34 | #endif 35 | return Path.Combine(RootDirectory, "bin", config, DotnetVersion, TestAssemblyName); 36 | } 37 | } 38 | 39 | public static void Execute(string resultsFile) 40 | { 41 | var testProject = RootDirectory; 42 | var testLogger = $"--logger:\"junit;LogFilePath={resultsFile}\""; 43 | 44 | // Delete stale results file 45 | var testLogFile = Path.Combine(testProject, resultsFile); 46 | 47 | // Strip out tokens 48 | var sanitizedResultFile = System.Text.RegularExpressions.Regex.Replace(resultsFile, @"{.*}\.*", string.Empty); 49 | foreach (string fileName in Directory.GetFiles(testProject)) 50 | { 51 | if (fileName.Contains("test-results.xml")) 52 | { 53 | File.Delete(fileName); 54 | } 55 | } 56 | 57 | // Log the contents of test output directory. Useful to verify if the logger is copied 58 | Console.WriteLine("------------"); 59 | Console.WriteLine("Contents of test output directory:"); 60 | foreach (var f in Directory.GetFiles(Path.Combine(testProject, $"bin/Debug/{DotnetVersion}"))) 61 | { 62 | Console.WriteLine(" " + f); 63 | } 64 | 65 | Console.WriteLine(); 66 | 67 | // Run dotnet test with logger 68 | using (var p = new Process()) 69 | { 70 | p.StartInfo.UseShellExecute = false; 71 | p.StartInfo.RedirectStandardOutput = true; 72 | p.StartInfo.FileName = "dotnet"; 73 | p.StartInfo.Arguments = $"test --no-build {testLogger} {testProject}"; 74 | p.Start(); 75 | 76 | Console.WriteLine("dotnet arguments: " + p.StartInfo.Arguments); 77 | 78 | // To avoid deadlocks, always read the output stream first and then wait. 79 | string output = p.StandardOutput.ReadToEnd(); 80 | p.WaitForExit(); 81 | Console.WriteLine("dotnet output: " + output); 82 | Console.WriteLine("------------"); 83 | } 84 | } 85 | 86 | public static void Execute(string resultsFileName, string filePath) 87 | { 88 | var testProject = RootDirectory; 89 | var testLogger = $"--logger \"junit;LogFileName={resultsFileName}\" --results-directory \"{filePath}\""; 90 | 91 | // Log the contents of test output directory. Useful to verify if the logger is copied 92 | Console.WriteLine("------------"); 93 | Console.WriteLine("Contents of test output directory:"); 94 | foreach (var f in Directory.GetFiles(Path.Combine(testProject, $"bin/Debug/{DotnetVersion}"))) 95 | { 96 | Console.WriteLine(" " + f); 97 | } 98 | 99 | Console.WriteLine(); 100 | 101 | // Run dotnet test with logger 102 | using (var p = new Process()) 103 | { 104 | p.StartInfo.UseShellExecute = false; 105 | p.StartInfo.RedirectStandardOutput = true; 106 | p.StartInfo.FileName = "dotnet"; 107 | p.StartInfo.Arguments = $"test --no-build {testLogger} {testProject}"; 108 | p.Start(); 109 | 110 | Console.WriteLine("dotnet arguments: " + p.StartInfo.Arguments); 111 | 112 | // To avoid deadlocks, always read the output stream first and then wait. 113 | string output = p.StandardOutput.ReadToEnd(); 114 | p.WaitForExit(); 115 | Console.WriteLine("dotnet output: " + output); 116 | Console.WriteLine("------------"); 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > Development of v4.x and subsequent versions of the Junit logger is moved to the [testlogger](https://github.com/spekt/testlogger) repository. 3 | > Kindly report any new issues or contribute your patches in that repo. 4 | 5 | # JUnit Test Logger 6 | 7 | JUnit xml report extension for [Visual Studio Test Platform](https://github.com/microsoft/vstest). 8 | 9 | [![Build Status](https://travis-ci.com/spekt/junit.testlogger.svg?branch=master)](https://travis-ci.com/spekt/junit.testlogger) 10 | [![Build Status](https://ci.appveyor.com/api/projects/status/gsiaqo5g4gfk76kq?svg=true)](https://ci.appveyor.com/project/spekt/junit-testlogger) 11 | [![NuGet Downloads](https://img.shields.io/nuget/dt/JunitXml.TestLogger)](https://www.nuget.org/packages/JunitXml.TestLogger/) 12 | 13 | ## Packages 14 | 15 | | Logger | Stable Package | Pre-release Package | 16 | | ------ | -------------- | ------------------- | 17 | | JUnit | [![NuGet](https://img.shields.io/nuget/v/JUnitXml.TestLogger.svg)](https://www.nuget.org/packages/JUnitXml.TestLogger/) | [![MyGet Pre Release](https://img.shields.io/myget/spekt/vpre/junitxml.testlogger.svg)](https://www.myget.org/feed/spekt/package/nuget/JunitXml.TestLogger) | 18 | 19 | If you're looking for `Nunit`, `Xunit` or `appveyor` loggers, visit following repositories: 20 | 21 | - 22 | - 23 | - 24 | 25 | ## Usage 26 | 27 | The JUnit Test Logger generates xml reports in the [Ant Junit Format](https://github.com/windyroad/JUnit-Schema), which the JUnit 5 repository refers to as the de-facto standard. While the generated xml complies with that schema, it does not contain values in every case. For example, the logger currently does not log any `properties`. Please [refer to a sample file](docs/assets/TestResults.xml) to see an example. If you find that the format is missing data required by your CI/CD system, please open an issue or PR. 28 | 29 | To use the logger, follow these steps: 30 | 31 | 1. Add a reference to the [JUnit Logger](https://www.nuget.org/packages/JUnitXml.TestLogger) nuget package in test project 32 | 2. Use the following command line in tests 33 | 34 | ```none 35 | > dotnet test --logger:junit 36 | ``` 37 | 38 | 3. Test results are generated in the `TestResults` directory relative to the `test.csproj` 39 | 40 | A path for the report file can be specified as follows: 41 | 42 | ```none 43 | > dotnet test --logger:"junit;LogFilePath=test-result.xml" 44 | ``` 45 | 46 | `test-result.xml` will be generated in the same directory as `test.csproj`. 47 | 48 | **Note:** the arguments to `--logger` should be in quotes since `;` is treated as a command delimiter in shell. 49 | 50 | All common options to the logger are documented [in the wiki][config-wiki]. E.g. 51 | token expansion for `{assembly}` or `{framework}` in result file. If you are writing multiple 52 | files to the same directory or testing multiple frameworks, these options can prevent 53 | test logs from over-writing each other. 54 | 55 | [config-wiki]: https://github.com/spekt/testlogger/wiki/Logger-Configuration 56 | 57 | ### Customizing Junit XML Contents 58 | 59 | There are several options to customize how the junit xml is populated. These options exist to 60 | provide additional control over the xml file so that the logged test results can be optimized for different CI/CD systems. 61 | 62 | Platform Specific Recommendations: 63 | 64 | - [GitLab CI/CD Recommendation](/docs/gitlab-recommendation.md) 65 | - [Jenkins Recommendation](/docs/jenkins-recommendation.md) 66 | - [CircleCI Recommendation](/docs/circleci-recommendation.md) 67 | 68 | After the logger name, command line arguments are provided as key/value pairs with the following general format. **Note** the quotes are required and key names are case sensitive. 69 | 70 | ```none 71 | > dotnet test --test-adapter-path:. --logger:"junit;key1=value1;key2=value2" 72 | ``` 73 | 74 | #### MethodFormat 75 | 76 | This option alters the `testcase name` attribute. By default, this contains only the method. Class, will add the class to the name. Full, will add the assembly/namespace/class to the method. 77 | 78 | We recommend this option for [GitLab](/docs/gitlab-recommendation.md) users. 79 | 80 | ##### Allowed Values 81 | 82 | - MethodFormat=Default 83 | - MethodFormat=Class 84 | - MethodFormat=Full 85 | 86 | #### FailureBodyFormat 87 | 88 | When set to default, the body of a `failure` element will contain only the exception which is captured by vstest. Verbose will prepend the body with 'Expected X, Actual Y' similar to how it is displayed in the standard test output. 'Expected X, Actual Y' are normally only contained in the failure message. Additionally, Verbose will include standard output from the test in the failure message. 89 | 90 | We recommend this option for [GitLab](/docs/gitlab-recommendation.md) and [CircleCI](/docs/circleci-recommendation.md) users. 91 | 92 | ##### Allowed Values 93 | 94 | - FailureBodyFormat=Default 95 | - FailureBodyFormat=Verbose 96 | 97 | ## License 98 | 99 | MIT 100 | -------------------------------------------------------------------------------- /test/JUnit.Xml.TestLogger.AcceptanceTests/JUnitTestLoggerAcceptanceTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace JUnit.Xml.TestLogger.AcceptanceTests 5 | { 6 | using System; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Xml.Linq; 10 | using System.Xml.XPath; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | 13 | /// 14 | /// Acceptance tests evaluate the most recent output of the build.ps1 script, NOT the most 15 | /// recent build performed by visual studio or dotnet.build 16 | /// 17 | /// These acceptance tests look at the specific structure and contents of the produced Xml. 18 | /// 19 | [TestClass] 20 | public class JUnitTestLoggerAcceptanceTests 21 | { 22 | private readonly string resultsFile; 23 | private readonly XDocument resultsXml; 24 | 25 | public JUnitTestLoggerAcceptanceTests() 26 | { 27 | this.resultsFile = Path.Combine(DotnetTestFixture.RootDirectory, "test-results.xml"); 28 | this.resultsXml = XDocument.Load(this.resultsFile); 29 | } 30 | 31 | [ClassInitialize] 32 | public static void SuiteInitialize(TestContext context) 33 | { 34 | DotnetTestFixture.Execute("test-results.xml"); 35 | } 36 | 37 | [TestMethod] 38 | public void TestRunWithLoggerAndFilePathShouldCreateResultsFile() 39 | { 40 | Assert.IsTrue(File.Exists(this.resultsFile)); 41 | } 42 | 43 | [TestMethod] 44 | public void TestResultFileShouldContainTestSuitesInformation() 45 | { 46 | var node = this.resultsXml.XPathSelectElement("/testsuites"); 47 | 48 | Assert.IsNotNull(node); 49 | } 50 | 51 | [TestMethod] 52 | public void TestResultFileShouldContainTestSuiteInformation() 53 | { 54 | var node = this.resultsXml.XPathSelectElement("/testsuites/testsuite"); 55 | 56 | Assert.IsNotNull(node); 57 | Assert.AreEqual("JUnit.Xml.TestLogger.NetCore.Tests.dll", node.Attribute(XName.Get("name")).Value); 58 | Assert.AreEqual(Environment.MachineName, node.Attribute(XName.Get("hostname")).Value); 59 | 60 | Assert.AreEqual("52", node.Attribute(XName.Get("tests")).Value); 61 | Assert.AreEqual("14", node.Attribute(XName.Get("failures")).Value); 62 | Assert.AreEqual("8", node.Attribute(XName.Get("skipped")).Value); 63 | 64 | // Errors is zero becasue we don't get errors as a test outcome from .net 65 | Assert.AreEqual("0", node.Attribute(XName.Get("errors")).Value); 66 | 67 | Convert.ToDouble(node.Attribute(XName.Get("time")).Value); 68 | Convert.ToDateTime(node.Attribute(XName.Get("timestamp")).Value); 69 | } 70 | 71 | [TestMethod] 72 | public void TestResultFileShouldContainTestCases() 73 | { 74 | var node = this.resultsXml.XPathSelectElements("/testsuites/testsuite").Descendants(); 75 | var testcases = node.Where(x => x.Name.LocalName == "testcase").ToList(); 76 | 77 | // Check all test cases 78 | Assert.IsNotNull(node); 79 | Assert.AreEqual(52, testcases.Count()); 80 | Assert.IsTrue(testcases.All(x => double.TryParse(x.Attribute("time").Value, out _))); 81 | 82 | // Check failures 83 | var failures = testcases 84 | .Where(x => x.Descendants().Any(y => y.Name.LocalName == "failure")) 85 | .ToList(); 86 | 87 | Assert.AreEqual(14, failures.Count()); 88 | Assert.IsTrue(failures.All(x => x.Descendants().Count() == 1)); 89 | Assert.IsTrue(failures.All(x => x.Descendants().First().Attribute("type").Value == "failure")); 90 | 91 | // Check failures 92 | var skips = testcases 93 | .Where(x => x.Descendants().Any(y => y.Name.LocalName == "skipped")) 94 | .ToList(); 95 | 96 | Assert.AreEqual(8, skips.Count()); 97 | } 98 | 99 | [TestMethod] 100 | public void TestResultFileShouldContainStandardOut() 101 | { 102 | var node = this.resultsXml.XPathSelectElement("/testsuites/testsuite/system-out"); 103 | 104 | Assert.IsTrue(node.Value.Contains("{2010CAE3-7BC0-4841-A5A3-7D5F947BB9FB}")); 105 | Assert.IsTrue(node.Value.Contains("{998AC9EC-7429-42CD-AD55-72037E7AF3D8}")); 106 | Assert.IsTrue(node.Value.Contains("{EEEE1DA6-6296-4486-BDA5-A50A19672F0F}")); 107 | Assert.IsTrue(node.Value.Contains("{C33FF4B5-75E1-4882-B968-DF9608BFE7C2}")); 108 | } 109 | 110 | [TestMethod] 111 | public void TestResultFileShouldContainErrordOut() 112 | { 113 | var node = this.resultsXml.XPathSelectElement("/testsuites/testsuite/system-err"); 114 | 115 | Assert.IsTrue(node.Value.Contains("{D46DFA10-EEDD-49E5-804D-FE43051331A7}")); 116 | Assert.IsTrue(node.Value.Contains("{33F5FD22-6F40-499D-98E4-481D87FAEAA1}")); 117 | } 118 | 119 | [TestMethod] 120 | public void LoggedXmlValidatesAgainstXsdSchema() 121 | { 122 | var validator = new JunitXmlValidator(); 123 | var result = validator.IsValid(File.ReadAllText(this.resultsFile)); 124 | Assert.IsTrue(result); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /docs/circleci-recommendation.md: -------------------------------------------------------------------------------- 1 | # CircleCI Recommendation 2 | 3 | ## Summary 4 | 5 | ```yml 6 | - run: 7 | name: Run Tests 8 | command: | 9 | dotnet test ... --logger:"junit;LogFilePath=TestResults/test-result.xml;FailureBodyFormat=Verbose;MethodFormat=Class" 10 | - store_test_results: 11 | path: TestResults/ 12 | - store_artifacts: 13 | path: TestResults/ 14 | ``` 15 | 16 | See also: [this sample repository][circleci-windows-project-example]. 17 | 18 | ## Details 19 | 20 | CircleCI uses just a few pieces of the JUnit XML report to generate the displayed user interface. 21 | A basic example `junit.xml` file which can be parsed by CircleCI can be seen 22 | [here][circleci-junit-xml-report]. 23 | 24 | For each failed test (i.e. for each JUnit `` element containing a failure or an error), 25 | CircleCI only shows the testcase's failure/error message, 26 | any text within the failure/error element, 27 | plus any `` or `` text. 28 | It does not show the testcase's properties. 29 | It does not show data from the testsuite. 30 | 31 | By default, the logger records test console output in the JUnit testsuite, 32 | which CircleCI ignores; 33 | this can be improved upon by setting `FailureBodyFormat` when invoking the logger. 34 | Setting MethodFormat provides no additional information or functionality. 35 | 36 | ### FailureBodyFormat 37 | 38 | Setting `FailureBodyFormat=Verbose` when invoking the logger ensures that 39 | any test console output is visible in the CircleCI UI for (failed) tests. 40 | This can provide important/useful information, 41 | especially if the test failure message is insufficiently informative on its own. 42 | 43 | - `Default`: 44 | 45 | ![FailureBodyFormat Default](assets/circleci-test-expanded-with-failure-default.png) 46 | 47 | - `Verbose`: 48 | 49 | ![FailureBodyFormat Verbose](assets/circleci-test-expanded-with-failure-verbose.png) 50 | 51 | ### MethodFormat 52 | 53 | CircleCI shows the class names of each failed test so setting 54 | `MethodFormat=Class` or `MethodFormat=Full` 55 | provides no further information on the CircleCI UI; 56 | the effect is purely cosmetic and is a matter of personal preference. 57 | 58 | - `Default`: 59 | 60 | ![MethodFormat Default](assets/circleci-test-collapsed-with-methodname-default.png) 61 | 62 | - `Class` and `Full`: 63 | 64 | ![MethodFormat Class](assets/circleci-test-collapsed-with-methodname-class.png) 65 | 66 | ### Store Test Results 67 | 68 | To enable CircleCI to show test results on the job's page, 69 | to interpret generated test result files to be used for gathering 70 | [Test Insights][circleci-test-insights] 71 | and 72 | [Test Splitting][circleci-test-splitting], 73 | the files must be stored using the 74 | [`store_test_results` step][circleci-store-test-results-step]. 75 | 76 | ```yml 77 | - store_test_results: 78 | path: TestResults/ 79 | ``` 80 | 81 | ### Store Artifacts 82 | 83 | To enable further troubleshooting, 84 | the test results file should _also_ be stored as an artifact using the 85 | [`store_artifacts` step][circleci-store-artifacts-step]. 86 | This allows the user to view and download the contents of the raw `*.xml` file, 87 | which is useful where the user requires information from the file that CircleCI does not display. 88 | 89 | ```yml 90 | - store_artifacts: 91 | path: TestResults/ 92 | ``` 93 | 94 | ### Test Splitting 95 | 96 | CircleCI can enable a suite a tests to be split across multiple executors in parallel by using (timing) data logged by this junit logger. 97 | 98 | The simplified summary (above) does not include this functionality but this is recommended for non-trivial test workloads, 99 | especially where the test execution time significantly exceeds the time taken to set up ready for the first test. 100 | This technique reduces the total (elapsed-time) duration of the tests and thus provides a faster test feedback loop. 101 | 102 | For an example of a CircleCI Windows project that demonstrates parallel test execution, take a look at 103 | [this sample repository][circleci-windows-project-example]. 104 | 105 | ----- 106 | 107 | ## Footnote 108 | 109 | As of July 20 2023: 110 | 111 | - Only the data in a `` element is interpreted by CircleCI. 112 | - e.g. `` from within a `` is shown. 113 | - Data outside of a `` element is not interpreted by CircleCI. 114 | - The `` from a `` aren't interpreted by CircleCI. 115 | - By default, the logger only puts console text (`` and ``) in elements at the `` level (not into ``) so CircleCI does not interpret it. 116 | 117 | e.g. 118 | 119 | ```xml 120 | 121 | 122 | 123 | 124 | 125 | 126 | So can this 127 | This is also shown by CircleCI 128 | Same for system-err too 129 | 130 | CircleCI ignores data here 131 | Same for system-err too 132 | 133 | 134 | ``` 135 | 136 | [circleci-junit-xml-report]: https://circleci.com/docs/use-the-circleci-cli-to-split-tests/#junit-xml-reports 137 | [circleci-test-insights]: https://circleci.com/docs/insights-tests/ 138 | [circleci-test-splitting]: https://circleci.com/docs/use-the-circleci-cli-to-split-tests/#split-by-timing-data 139 | [circleci-store-test-results-step]: https://circleci.com/docs/configuration-reference/#storetestresults 140 | [circleci-store-artifacts-step]: https://circleci.com/docs/configuration-reference/#storeartifacts 141 | [circleci-windows-project-example]: https://github.com/jenny-miggin/circleci-demo-windows-test-splitting 142 | -------------------------------------------------------------------------------- /test/assets/JUnit.Xml.TestLogger.NetCore.Tests/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | 5 | namespace JUnit.Xml.TestLogger.NetFull.Tests 6 | { 7 | [TestFixture] 8 | public class UnitTest1 9 | { 10 | [Test] 11 | [Description("Passing test description")] 12 | public async Task PassTest11() 13 | { 14 | Console.WriteLine("{2010CAE3-7BC0-4841-A5A3-7D5F947BB9FB}"); 15 | Console.WriteLine("{998AC9EC-7429-42CD-AD55-72037E7AF3D8}"); 16 | await Task.Delay(TimeSpan.FromMilliseconds(400)); 17 | } 18 | 19 | [Test] 20 | public void FailTest11() 21 | { 22 | Console.WriteLine("{EEEE1DA6-6296-4486-BDA5-A50A19672F0F}"); 23 | Console.WriteLine("{C33FF4B5-75E1-4882-B968-DF9608BFE7C2}"); 24 | Console.Error.WriteLine("{D46DFA10-EEDD-49E5-804D-FE43051331A7}"); 25 | Console.Error.WriteLine("{33F5FD22-6F40-499D-98E4-481D87FAEAA1}"); 26 | 27 | Assert.False(true); 28 | } 29 | 30 | [Test] 31 | public void Inconclusive() 32 | { 33 | Assert.Inconclusive("test inconclusive"); 34 | } 35 | 36 | [Test] 37 | [Ignore("ignore reason")] 38 | public void Ignored() 39 | { 40 | } 41 | 42 | [Test] 43 | [Property("Property name", "Property value")] 44 | public void WithProperty() 45 | { 46 | } 47 | 48 | [Test] 49 | public void NoProperty() 50 | { 51 | } 52 | 53 | [Test] 54 | [Category("Junit Test Category")] 55 | public void WithCategory() 56 | { 57 | } 58 | 59 | [Test] 60 | [Category("Category2")] 61 | [Category("Category1")] 62 | public void MultipleCategories() 63 | { 64 | } 65 | 66 | [Test] 67 | [Category("JUnit Test Category")] 68 | [Property("Property name", "Property value")] 69 | public void WithCategoryAndProperty() 70 | { 71 | } 72 | 73 | [Test] 74 | [Property("Property name", "Property value 1")] 75 | [Property("Property name", "Property value 2")] 76 | public void WithProperties() 77 | { 78 | } 79 | } 80 | 81 | public class UnitTest2 82 | { 83 | [Test] 84 | [Category("passing category")] 85 | public void PassTest21() 86 | { 87 | Assert.That(2, Is.EqualTo(2)); 88 | } 89 | 90 | [Test] 91 | [Category("failing category")] 92 | public void FailTest22() 93 | { 94 | Assert.False(true); 95 | } 96 | 97 | [Test] 98 | public void Inconclusive() 99 | { 100 | Assert.Inconclusive(); 101 | } 102 | 103 | [Test] 104 | [Ignore("ignore reason")] 105 | public void IgnoredTest() 106 | { 107 | } 108 | 109 | [Test] 110 | public void WarningTest() 111 | { 112 | Assert.Warn("Warning"); 113 | } 114 | 115 | [Test] 116 | [Explicit] 117 | public void ExplicitTest() 118 | { 119 | } 120 | } 121 | 122 | [TestFixture] 123 | public class SuccessFixture 124 | { 125 | [Test] 126 | public void SuccessTest() 127 | { 128 | } 129 | } 130 | 131 | [TestFixture] 132 | public class SuccessAndInconclusiveFixture 133 | { 134 | [Test] 135 | public void SuccessTest() 136 | { 137 | } 138 | 139 | [Test] 140 | public void InconclusiveTest() 141 | { 142 | Assert.Inconclusive(); 143 | } 144 | } 145 | 146 | [TestFixture] 147 | public class FailingOneTimeSetUp 148 | { 149 | [OneTimeSetUp] 150 | public void OneTimeSetUp() 151 | { 152 | throw new InvalidOperationException(); 153 | } 154 | 155 | [Test] 156 | public void TestA() 157 | { 158 | } 159 | } 160 | 161 | [TestFixture] 162 | public class FailingTestSetup 163 | { 164 | [SetUp] 165 | public void SetUp() 166 | { 167 | throw new InvalidOperationException(); 168 | } 169 | 170 | [Test] 171 | public void TestB() 172 | { 173 | } 174 | } 175 | 176 | [TestFixture] 177 | public class FailingTearDown 178 | { 179 | [TearDown] 180 | public void TearDown() 181 | { 182 | throw new InvalidOperationException(); 183 | } 184 | 185 | [Test] 186 | public void TestC() 187 | { 188 | } 189 | } 190 | 191 | [TestFixture] 192 | public class FailingOneTimeTearDown 193 | { 194 | [OneTimeTearDown] 195 | public void OneTimeTearDown() 196 | { 197 | throw new InvalidOperationException(); 198 | } 199 | 200 | [Test] 201 | public void TestD() 202 | { 203 | } 204 | } 205 | 206 | [TestFixture] 207 | [TestFixtureSource("FixtureArgs")] 208 | public class ParametrizedFixture 209 | { 210 | public ParametrizedFixture(string word, int num) 211 | { 212 | } 213 | 214 | [Test] 215 | public void TestE() 216 | { 217 | } 218 | 219 | static object[] FixtureArgs = 220 | { 221 | new object[] {"Question", 1}, 222 | new object[] {"Answer", 42} 223 | }; 224 | } 225 | 226 | [TestFixture] 227 | public class ParametrizedTestCases 228 | { 229 | [Test] 230 | public void TestData([Values(1, 2)] int x, [Values("A", "B")] string s) 231 | { 232 | Assert.That(x, Is.Not.EqualTo(2), "failing for second case"); 233 | Assert.That(s, Is.Not.Null); 234 | } 235 | } 236 | } -------------------------------------------------------------------------------- /junit.testlogger.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2010 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{DB72843C-27A3-4646-BE18-164E33702212}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JUnit.Xml.TestLogger", "src\JUnit.Xml.TestLogger\JUnit.Xml.TestLogger.csproj", "{D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JUnit.Xml.TestLogger.TestAdapter", "src\JUnit.Xml.TestLogger.TestAdapter\JUnit.Xml.TestLogger.TestAdapter.csproj", "{72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "package", "src\package\package.csproj", "{B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D244BD05-A6C4-4E04-A392-A21091E4597F}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JUnit.Xml.TestLogger.AcceptanceTests", "test\JUnit.Xml.TestLogger.AcceptanceTests\JUnit.Xml.TestLogger.AcceptanceTests.csproj", "{A47EDBAD-CF86-4F2D-9CE6-02510954CE10}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JUnit.Xml.TestLogger.UnitTests", "test\JUnit.Xml.TestLogger.UnitTests\JUnit.Xml.TestLogger.UnitTests.csproj", "{FC0600B1-5E2E-4C19-AFD9-BF418A739E17}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Debug|x64 = Debug|x64 24 | Debug|x86 = Debug|x86 25 | Release|Any CPU = Release|Any CPU 26 | Release|x64 = Release|x64 27 | Release|x86 = Release|x86 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Debug|x64.ActiveCfg = Debug|Any CPU 33 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Debug|x64.Build.0 = Debug|Any CPU 34 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Debug|x86.ActiveCfg = Debug|Any CPU 35 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Debug|x86.Build.0 = Debug|Any CPU 36 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Release|x64.ActiveCfg = Release|Any CPU 39 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Release|x64.Build.0 = Release|Any CPU 40 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Release|x86.ActiveCfg = Release|Any CPU 41 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2}.Release|x86.Build.0 = Release|Any CPU 42 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Debug|x64.ActiveCfg = Debug|Any CPU 45 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Debug|x64.Build.0 = Debug|Any CPU 46 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Debug|x86.ActiveCfg = Debug|Any CPU 47 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Debug|x86.Build.0 = Debug|Any CPU 48 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Release|x64.ActiveCfg = Release|Any CPU 51 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Release|x64.Build.0 = Release|Any CPU 52 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Release|x86.ActiveCfg = Release|Any CPU 53 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D}.Release|x86.Build.0 = Release|Any CPU 54 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Debug|x64.ActiveCfg = Debug|Any CPU 57 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Debug|x64.Build.0 = Debug|Any CPU 58 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Debug|x86.ActiveCfg = Debug|Any CPU 59 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Debug|x86.Build.0 = Debug|Any CPU 60 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Release|x64.ActiveCfg = Release|Any CPU 63 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Release|x64.Build.0 = Release|Any CPU 64 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Release|x86.ActiveCfg = Release|Any CPU 65 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3}.Release|x86.Build.0 = Release|Any CPU 66 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Debug|x64.ActiveCfg = Debug|Any CPU 69 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Debug|x64.Build.0 = Debug|Any CPU 70 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Debug|x86.ActiveCfg = Debug|Any CPU 71 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Debug|x86.Build.0 = Debug|Any CPU 72 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Release|Any CPU.ActiveCfg = Release|Any CPU 73 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Release|Any CPU.Build.0 = Release|Any CPU 74 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Release|x64.ActiveCfg = Release|Any CPU 75 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Release|x64.Build.0 = Release|Any CPU 76 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Release|x86.ActiveCfg = Release|Any CPU 77 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10}.Release|x86.Build.0 = Release|Any CPU 78 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 79 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Debug|Any CPU.Build.0 = Debug|Any CPU 80 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Debug|x64.ActiveCfg = Debug|Any CPU 81 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Debug|x64.Build.0 = Debug|Any CPU 82 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Debug|x86.ActiveCfg = Debug|Any CPU 83 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Debug|x86.Build.0 = Debug|Any CPU 84 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Release|Any CPU.ActiveCfg = Release|Any CPU 85 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Release|Any CPU.Build.0 = Release|Any CPU 86 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Release|x64.ActiveCfg = Release|Any CPU 87 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Release|x64.Build.0 = Release|Any CPU 88 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Release|x86.ActiveCfg = Release|Any CPU 89 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17}.Release|x86.Build.0 = Release|Any CPU 90 | EndGlobalSection 91 | GlobalSection(SolutionProperties) = preSolution 92 | HideSolutionNode = FALSE 93 | EndGlobalSection 94 | GlobalSection(NestedProjects) = preSolution 95 | {D53B993F-BAA5-4DBA-AAE2-D44DC28D37B2} = {DB72843C-27A3-4646-BE18-164E33702212} 96 | {72D6CC14-E2CE-4271-8CEE-FBEF8E6EC31D} = {DB72843C-27A3-4646-BE18-164E33702212} 97 | {B07E01EB-F697-4D97-BBE2-C2C49B2EFBF3} = {DB72843C-27A3-4646-BE18-164E33702212} 98 | {A47EDBAD-CF86-4F2D-9CE6-02510954CE10} = {D244BD05-A6C4-4E04-A392-A21091E4597F} 99 | {FC0600B1-5E2E-4C19-AFD9-BF418A739E17} = {D244BD05-A6C4-4E04-A392-A21091E4597F} 100 | EndGlobalSection 101 | GlobalSection(ExtensibilityGlobals) = postSolution 102 | SolutionGuid = {3AD427EE-A607-4561-B08D-B30A09DE69BA} 103 | EndGlobalSection 104 | EndGlobal 105 | -------------------------------------------------------------------------------- /test/JUnit.Xml.TestLogger.AcceptanceTests/JUnitTestLoggerFormatOptionsAcceptanceTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace JUnit.Xml.TestLogger.AcceptanceTests 5 | { 6 | using System; 7 | using System.IO; 8 | using System.Linq; 9 | using System.Xml.Linq; 10 | using System.Xml.XPath; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | using Spekt.TestLogger.Core; 13 | 14 | /// 15 | /// Acceptance tests evaluate the most recent output of the build.ps1 script, NOT the most 16 | /// recent build performed by visual studio or dotnet.build 17 | /// 18 | /// These acceptance tests look at the specific places output is expected to change because of 19 | /// the format option specified. Accordingly, these tests cannot protect against other changes 20 | /// occurring due to the formatting option. 21 | /// 22 | [TestClass] 23 | public class JUnitTestLoggerFormatOptionsAcceptanceTests 24 | { 25 | [TestMethod] 26 | public void FailureBodyFormat_Default_ShouldntStartWithMessage() 27 | { 28 | DotnetTestFixture.Execute("failure-default-test-results.xml;FailureBodyFormat=Default"); 29 | string resultsFile = Path.Combine(DotnetTestFixture.RootDirectory, "failure-default-test-results.xml"); 30 | XDocument resultsXml = XDocument.Load(resultsFile); 31 | 32 | var failures = resultsXml.XPathSelectElements("/testsuites/testsuite") 33 | .Descendants() 34 | .Where(x => x.Name.LocalName == "failure") 35 | .ToList(); 36 | 37 | foreach (var failure in failures) 38 | { 39 | // Strip new line and carrige return. These may be inconsistent depending on 40 | // environment settings 41 | var message = failure.Attribute("message").Value.Replace("\r", string.Empty).Replace("\n", string.Empty); 42 | var body = failure.Value.Replace("\r", string.Empty).Replace("\n", string.Empty); 43 | 44 | Assert.IsFalse(body.StartsWith(message)); 45 | } 46 | 47 | Assert.IsTrue(new JunitXmlValidator().IsValid(resultsXml)); 48 | } 49 | 50 | [TestMethod] 51 | public void FailureBodyFormat_Verbose_ShouldNotContainConsoleOut() 52 | { 53 | DotnetTestFixture.Execute("failure-verbose-test-results.xml;FailureBodyFormat=Default"); 54 | string resultsFile = Path.Combine(DotnetTestFixture.RootDirectory, "failure-verbose-test-results.xml"); 55 | XDocument resultsXml = XDocument.Load(resultsFile); 56 | 57 | var failures = resultsXml.XPathSelectElements("/testsuites/testsuite") 58 | .Descendants() 59 | .Where(x => x.Name.LocalName == "failure") 60 | .ToList(); 61 | 62 | Assert.AreEqual(0, failures.Count(x => x.Value.Contains("{EEEE1DA6-6296-4486-BDA5-A50A19672F0F}"))); 63 | Assert.AreEqual(0, failures.Count(x => x.Value.Contains("{C33FF4B5-75E1-4882-B968-DF9608BFE7C2}"))); 64 | Assert.IsTrue(new JunitXmlValidator().IsValid(resultsXml)); 65 | } 66 | 67 | [TestMethod] 68 | public void FailureBodyFormat_Verbose_ShouldStartWithMessage() 69 | { 70 | DotnetTestFixture.Execute("failure-verbose-test-results.xml;FailureBodyFormat=Verbose"); 71 | string resultsFile = Path.Combine(DotnetTestFixture.RootDirectory, "failure-verbose-test-results.xml"); 72 | XDocument resultsXml = XDocument.Load(resultsFile); 73 | 74 | var failures = resultsXml.XPathSelectElements("/testsuites/testsuite") 75 | .Descendants() 76 | .Where(x => x.Name.LocalName == "failure") 77 | .ToList(); 78 | 79 | foreach (var failure in failures) 80 | { 81 | // Strip new line and carrige return. These may be inconsistent depending on 82 | // environment settings 83 | var message = failure.Attribute("message").Value.Replace("\r", string.Empty).Replace("\n", string.Empty); 84 | var body = failure.Value.Replace("\r", string.Empty).Replace("\n", string.Empty); 85 | 86 | Assert.IsTrue(body.Trim().StartsWith(message.Trim())); 87 | } 88 | 89 | Assert.IsTrue(new JunitXmlValidator().IsValid(resultsXml)); 90 | } 91 | 92 | [TestMethod] 93 | public void FailureBodyFormat_Verbose_ShouldContainConsoleOut() 94 | { 95 | DotnetTestFixture.Execute("failure-verbose-test-results.xml;FailureBodyFormat=Verbose"); 96 | string resultsFile = Path.Combine(DotnetTestFixture.RootDirectory, "failure-verbose-test-results.xml"); 97 | XDocument resultsXml = XDocument.Load(resultsFile); 98 | 99 | var failures = resultsXml.XPathSelectElements("/testsuites/testsuite") 100 | .Descendants() 101 | .Where(x => x.Name.LocalName == "failure") 102 | .ToList(); 103 | 104 | Assert.AreEqual(1, failures.Count(x => x.Value.Contains("{EEEE1DA6-6296-4486-BDA5-A50A19672F0F}"))); 105 | Assert.AreEqual(1, failures.Count(x => x.Value.Contains("{C33FF4B5-75E1-4882-B968-DF9608BFE7C2}"))); 106 | Assert.IsTrue(new JunitXmlValidator().IsValid(resultsXml)); 107 | } 108 | 109 | [TestMethod] 110 | public void MethodFormat_Default_ShouldBeOnlyTheMethod() 111 | { 112 | DotnetTestFixture.Execute("method-default-test-results.xml;MethodFormat=Default"); 113 | string resultsFile = Path.Combine(DotnetTestFixture.RootDirectory, "method-default-test-results.xml"); 114 | XDocument resultsXml = XDocument.Load(resultsFile); 115 | 116 | var testcases = resultsXml.XPathSelectElements("/testsuites/testsuite") 117 | .Descendants() 118 | .Where(x => x.Name.LocalName == "testcase") 119 | .ToList(); 120 | 121 | foreach (var testcase in testcases) 122 | { 123 | var parsedName = new TestCaseNameParser().Parse(testcase.Attribute("name").Value); 124 | 125 | // A method name only will not be parsable into two pieces 126 | Assert.AreEqual(parsedName.Type, TestCaseNameParser.TestCaseParserUnknownType); 127 | } 128 | 129 | Assert.IsTrue(new JunitXmlValidator().IsValid(resultsXml)); 130 | } 131 | 132 | [TestMethod] 133 | public void MethodFormat_Class_ShouldIncludeClass() 134 | { 135 | DotnetTestFixture.Execute("method-class-test-results.xml;MethodFormat=Class"); 136 | string resultsFile = Path.Combine(DotnetTestFixture.RootDirectory, "method-class-test-results.xml"); 137 | XDocument resultsXml = XDocument.Load(resultsFile); 138 | 139 | var testcases = resultsXml.XPathSelectElements("/testsuites/testsuite") 140 | .Descendants() 141 | .Where(x => x.Name.LocalName == "testcase") 142 | .ToList(); 143 | 144 | foreach (var testcase in testcases) 145 | { 146 | // Note the new parser can't handle the names with just class.method 147 | var parsedName = new LegacyTestCaseNameParser().Parse(testcase.Attribute("name").Value); 148 | 149 | // If the name is parsable into two pieces, then we have a two piece name and 150 | // consider that to be a passing result. 151 | Assert.AreNotEqual(parsedName.Type, TestCaseNameParser.TestCaseParserUnknownType); 152 | } 153 | 154 | Assert.IsTrue(new JunitXmlValidator().IsValid(resultsXml)); 155 | } 156 | 157 | [TestMethod] 158 | public void MethodFormat_Full_ShouldIncludeNamespaceAndClass() 159 | { 160 | DotnetTestFixture.Execute("method-full-test-results.xml;MethodFormat=Full"); 161 | string resultsFile = Path.Combine(DotnetTestFixture.RootDirectory, "method-full-test-results.xml"); 162 | XDocument resultsXml = XDocument.Load(resultsFile); 163 | 164 | var testcases = resultsXml.XPathSelectElements("/testsuites/testsuite") 165 | .Descendants() 166 | .Where(x => x.Name.LocalName == "testcase") 167 | .ToList(); 168 | 169 | foreach (var testcase in testcases) 170 | { 171 | var parsedName = new TestCaseNameParser().Parse(testcase.Attribute("name").Value); 172 | 173 | // We expect the full name would be the class name plus the parsed method 174 | var expectedFullName = parsedName.Namespace + "." + parsedName.Type + "." + parsedName.Method; 175 | 176 | // If the name is parsable into two pieces, then we have at least a two piece name 177 | Assert.AreNotEqual(parsedName.Type, TestCaseNameParser.TestCaseParserUnknownType); 178 | Assert.AreEqual(expectedFullName, testcase.Attribute("name").Value); 179 | } 180 | 181 | Assert.IsTrue(new JunitXmlValidator().IsValid(resultsXml)); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /test/assets/JUnit.xsd: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | JUnit test result schema for the Apache Ant JUnit and JUnitReport tasks 8 | Copyright © 2011, Windy Road Technology Pty. Limited 9 | The Apache Ant JUnit XML Schema is distributed under the terms of the Apache License Version 2.0 http://www.apache.org/licenses/ 10 | Permission to waive conditions of this license may be requested from Windy Road Support (http://windyroad.org/support). 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Contains an aggregation of testsuite results 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Derived from testsuite/@name in the non-aggregated documents 31 | 32 | 33 | 34 | 35 | Starts at '0' for the first testsuite and is incremented by 1 for each following testsuite 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Contains the results of exexuting a testsuite 48 | 49 | 50 | 51 | 52 | Properties (e.g., environment settings) set during test execution 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | Indicates that the test errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. Contains as a text node relevant data for the error, e.g., a stack trace 78 | 79 | 80 | 81 | 82 | 83 | 84 | The error message. e.g., if a java exception is thrown, the return value of getMessage() 85 | 86 | 87 | 88 | 89 | The type of error that occured. e.g., if a java execption is thrown the full class name of the exception. 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | Indicates that the test failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals. Contains as a text node relevant data for the failure, e.g., a stack trace 99 | 100 | 101 | 102 | 103 | 104 | 105 | The message specified in the assert 106 | 107 | 108 | 109 | 110 | The type of the assert. 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | Name of the test method 121 | 122 | 123 | 124 | 125 | Full class name for the class the test method is in. 126 | 127 | 128 | 129 | 130 | Time taken (in seconds) to execute the test 131 | 132 | 133 | 134 | 135 | 136 | 137 | Data that was written to standard out while the test was executed 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | Data that was written to standard error while the test was executed 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | Full class name of the test for non-aggregated testsuite documents. Class name without the package for aggregated testsuites documents 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | when the test was executed. Timezone may not be specified. 169 | 170 | 171 | 172 | 173 | Host on which the tests were executed. 'localhost' should be used if the hostname cannot be determined. 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | The total number of tests in the suite 184 | 185 | 186 | 187 | 188 | The total number of tests in the suite that failed. A failure is a test which the code has explicitly failed by using the mechanisms for that purpose. e.g., via an assertEquals 189 | 190 | 191 | 192 | 193 | The total number of tests in the suite that errored. An errored test is one that had an unanticipated problem. e.g., an unchecked throwable; or a problem with the implementation of the test. 194 | 195 | 196 | 197 | 198 | The total number of ignored or skipped tests in the suite. 199 | 200 | 201 | 202 | 203 | Time taken (in seconds) to execute the tests in the suite 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | -------------------------------------------------------------------------------- /docs/assets/TestResults.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | --TearDown 10 | at JUnit.Xml.TestLogger.NetFull.Tests.FailingTearDown.TearDown() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest1.cs:line 182 11 | 12 | 13 | at JUnit.Xml.TestLogger.NetFull.Tests.FailingTestSetup.SetUp() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest1.cs:line 167 14 | 15 | 16 | 17 | 18 | 19 | 20 | at JUnit.Xml.TestLogger.NetFull.Tests.ParametrizedTestCases.TestData(Int32 x, String s) in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest1.cs:line 232 21 | 22 | 23 | at JUnit.Xml.TestLogger.NetFull.Tests.ParametrizedTestCases.TestData(Int32 x, String s) in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest1.cs:line 232 24 | 25 | 26 | 27 | 28 | 29 | at JUnit.Xml.TestLogger.NetFull.Tests.UnitTest1.FailTest11() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest1.cs:line 27 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | at JUnit.Xml.TestLogger.NetFull.Tests.UnitTest2.FailTest22() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest1.cs:line 94 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | --TearDown 60 | at JUnit.Xml.TestLogger.Tests2.FailingTearDown.TearDown() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest2.cs:line 137 61 | 62 | 63 | at JUnit.Xml.TestLogger.Tests2.FailingTestSetup.SetUp() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest2.cs:line 122 64 | 65 | 66 | 67 | 68 | 69 | 70 | at JUnit.Xml.TestLogger.Tests2.ParametrizedTestCases.TestData(Int32 x, String s) in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest2.cs:line 187 71 | 72 | 73 | at JUnit.Xml.TestLogger.Tests2.ParametrizedTestCases.TestData(Int32 x, String s) in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest2.cs:line 187 74 | 75 | 76 | 77 | 78 | 79 | at JUnit.Xml.TestLogger.Tests2.UnitTest1.FailTest11() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest2.cs:line 20 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | at JUnit.Xml.TestLogger.Tests2.UnitTest2.FailTest22() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest2.cs:line 49 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | {EEEE1DA6-6296-4486-BDA5-A50A19672F0F} 99 | {C33FF4B5-75E1-4882-B968-DF9608BFE7C2} 100 | 101 | {2010CAE3-7BC0-4841-A5A3-7D5F947BB9FB} 102 | {998AC9EC-7429-42CD-AD55-72037E7AF3D8} 103 | 104 | 105 | Test Framework Informational Messages: 106 | NUnit Adapter 3.10.0.21: Test execution started 107 | Running all tests in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\bin\Debug\netcoreapp3.1\JUnit.Xml.TestLogger.NetCore.Tests.dll 108 | NUnit3TestExecutor converted 52 of 52 NUnit test cases 109 | NUnit Adapter 3.10.0.21: Test execution complete 110 | 111 | Warning - SetUp failed for test fixture JUnit.Xml.TestLogger.NetFull.Tests.FailingOneTimeSetUp 112 | Warning - System.InvalidOperationException : Operation is not valid due to the current state of the object. 113 | Warning - at JUnit.Xml.TestLogger.NetFull.Tests.FailingOneTimeSetUp.OneTimeSetUp() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest1.cs:line 152 114 | Warning - TearDown failed for test fixture JUnit.Xml.TestLogger.NetFull.Tests.FailingOneTimeTearDown 115 | Warning - TearDown : System.InvalidOperationException : Operation is not valid due to the current state of the object. 116 | Warning - --TearDown 117 | at JUnit.Xml.TestLogger.NetFull.Tests.FailingOneTimeTearDown.OneTimeTearDown() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest1.cs:line 197 118 | Warning - {D46DFA10-EEDD-49E5-804D-FE43051331A7} 119 | Warning - {33F5FD22-6F40-499D-98E4-481D87FAEAA1} 120 | Warning - SetUp failed for test fixture JUnit.Xml.TestLogger.Tests2.FailingOneTimeSetUp 121 | Warning - System.InvalidOperationException : Operation is not valid due to the current state of the object. 122 | Warning - at JUnit.Xml.TestLogger.Tests2.FailingOneTimeSetUp.OneTimeSetUp() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest2.cs:line 107 123 | Warning - TearDown failed for test fixture JUnit.Xml.TestLogger.Tests2.FailingOneTimeTearDown 124 | Warning - TearDown : System.InvalidOperationException : Operation is not valid due to the current state of the object. 125 | Warning - --TearDown 126 | at JUnit.Xml.TestLogger.Tests2.FailingOneTimeTearDown.OneTimeTearDown() in C:\Users\REDACTED\Documents\GitHub\junit.testlogger\test\assets\JUnit.Xml.TestLogger.NetCore.Tests\UnitTest2.cs:line 152 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/JUnit.Xml.TestLogger/JunitXmlSerializer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Spekt Contributors. All rights reserved. 2 | // Licensed under the MIT license. See LICENSE file in the project root for full license information. 3 | 4 | namespace Microsoft.VisualStudio.TestPlatform.Extension.Junit.Xml.TestLogger 5 | { 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Globalization; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Text; 12 | using System.Xml.Linq; 13 | using Microsoft.VisualStudio.TestPlatform.ObjectModel; 14 | using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging; 15 | using Spekt.TestLogger.Core; 16 | using Spekt.TestLogger.Utilities; 17 | 18 | public class JunitXmlSerializer : ITestResultSerializer 19 | { 20 | // Dicionary keys for command line arguments. 21 | public const string MethodFormatKey = "MethodFormat"; 22 | 23 | public const string FailureBodyFormatKey = "FailureBodyFormat"; 24 | 25 | private const string ResultStatusPassed = "Passed"; 26 | private const string ResultStatusFailed = "Failed"; 27 | 28 | public enum MethodFormat 29 | { 30 | /// 31 | /// The method format will be the method only (i.e. Class.Method()) 32 | /// 33 | Default, 34 | 35 | /// 36 | /// The method format will include the class and method name (i.e. Class.Method()) 37 | /// 38 | Class, 39 | 40 | /// 41 | /// The method format will include the namespace, class and method (i.e. Namespace.Class.Method()) 42 | /// 43 | Full, 44 | } 45 | 46 | public enum FailureBodyFormat 47 | { 48 | /// 49 | /// The failure body will incldue only the error stack trace. 50 | /// 51 | Default, 52 | 53 | /// 54 | /// The failure body will incldue the Expected/Actual messages. 55 | /// 56 | Verbose, 57 | } 58 | 59 | public IInputSanitizer InputSanitizer { get; } = new InputSanitizerXml(); 60 | 61 | public MethodFormat MethodFormatOption { get; private set; } = MethodFormat.Default; 62 | 63 | public FailureBodyFormat FailureBodyFormatOption { get; private set; } = FailureBodyFormat.Default; 64 | 65 | public static IEnumerable GroupTestSuites(IEnumerable suites) 66 | { 67 | var groups = suites; 68 | var roots = new List(); 69 | while (groups.Any()) 70 | { 71 | groups = groups.GroupBy(r => 72 | { 73 | var name = r.FullName.SubstringBeforeDot(); 74 | if (string.IsNullOrEmpty(name)) 75 | { 76 | roots.Add(r); 77 | } 78 | 79 | return name; 80 | }) 81 | .OrderBy(g => g.Key) 82 | .Where(g => !string.IsNullOrEmpty(g.Key)) 83 | .Select(g => AggregateTestSuites(g, "TestSuite", g.Key.SubstringAfterDot(), g.Key)) 84 | .ToList(); 85 | } 86 | 87 | return roots; 88 | } 89 | 90 | public string Serialize( 91 | LoggerConfiguration loggerConfiguration, 92 | TestRunConfiguration runConfiguration, 93 | List results, 94 | List messages) 95 | { 96 | this.Configure(loggerConfiguration); 97 | var doc = new XDocument(this.CreateTestSuitesElement(results, runConfiguration, messages)); 98 | return doc.ToString(); 99 | } 100 | 101 | private static TestSuite AggregateTestSuites( 102 | IEnumerable suites, 103 | string testSuiteType, 104 | string name, 105 | string fullName) 106 | { 107 | var element = new XElement("test-suite"); 108 | 109 | int total = 0; 110 | int passed = 0; 111 | int failed = 0; 112 | int skipped = 0; 113 | int inconclusive = 0; 114 | int error = 0; 115 | var time = TimeSpan.Zero; 116 | 117 | foreach (var result in suites) 118 | { 119 | total += result.Total; 120 | passed += result.Passed; 121 | failed += result.Failed; 122 | skipped += result.Skipped; 123 | inconclusive += result.Inconclusive; 124 | error += result.Error; 125 | time += result.Time; 126 | 127 | element.Add(result.Element); 128 | } 129 | 130 | element.SetAttributeValue("type", testSuiteType); 131 | element.SetAttributeValue("name", name); 132 | element.SetAttributeValue("fullname", fullName); 133 | element.SetAttributeValue("total", total); 134 | element.SetAttributeValue("passed", passed); 135 | element.SetAttributeValue("failed", failed); 136 | element.SetAttributeValue("inconclusive", inconclusive); 137 | element.SetAttributeValue("skipped", skipped); 138 | 139 | var resultString = failed > 0 ? ResultStatusFailed : ResultStatusPassed; 140 | element.SetAttributeValue("result", resultString); 141 | element.SetAttributeValue("duration", time.TotalSeconds); 142 | 143 | return new TestSuite 144 | { 145 | Element = element, 146 | Name = name, 147 | FullName = fullName, 148 | Total = total, 149 | Passed = passed, 150 | Failed = failed, 151 | Inconclusive = inconclusive, 152 | Skipped = skipped, 153 | Error = error, 154 | Time = time 155 | }; 156 | } 157 | 158 | /// 159 | /// Produces a consistently indented output, taking into account that incoming messages 160 | /// often have new lines within a message. 161 | /// 162 | private static string Indent(IReadOnlyCollection messages) 163 | { 164 | var indent = " "; 165 | 166 | // Splitting on any line feed or carrage return because a message may include new lines 167 | // that are inconsistent with the Environment.NewLine. We then remove all blank lines so 168 | // it shouldn't cause an issue that this generates extra line breaks. 169 | return 170 | indent + 171 | string.Join( 172 | $"{Environment.NewLine}{indent}", 173 | messages.SelectMany(m => 174 | m.Text.Split(new string[] { "\r", "\n" }, StringSplitOptions.None) 175 | .Where(x => !string.IsNullOrWhiteSpace(x)) 176 | .Select(x => x.Trim()))); 177 | } 178 | 179 | private XElement CreateTestSuitesElement( 180 | List results, 181 | TestRunConfiguration runConfiguration, 182 | List messages) 183 | { 184 | var assemblies = results.Select(x => x.AssemblyPath).Distinct().ToList(); 185 | var testsuiteElements = assemblies 186 | .Select(a => this.CreateTestSuiteElement( 187 | results.Where(x => x.AssemblyPath == a).ToList(), 188 | runConfiguration, 189 | messages)); 190 | 191 | return new XElement("testsuites", testsuiteElements); 192 | } 193 | 194 | private XElement CreateTestSuiteElement(List results, TestRunConfiguration runConfiguration, List messages) 195 | { 196 | var testCaseElements = results.Select(a => this.CreateTestCaseElement(a)); 197 | 198 | StringBuilder stdOut = new StringBuilder(); 199 | foreach (var m in results.SelectMany(x => x.Messages)) 200 | { 201 | if (TestResultMessage.StandardOutCategory.Equals(m.Category, StringComparison.OrdinalIgnoreCase)) 202 | { 203 | stdOut.AppendLine(m.Text); 204 | } 205 | } 206 | 207 | var frameworkInfo = messages.Where(x => x.Level == TestMessageLevel.Informational); 208 | if (frameworkInfo.Any()) 209 | { 210 | stdOut.AppendLine(string.Empty); 211 | stdOut.AppendLine("Test Framework Informational Messages:"); 212 | 213 | foreach (var m in frameworkInfo) 214 | { 215 | stdOut.AppendLine(m.Message); 216 | } 217 | } 218 | 219 | StringBuilder stdErr = new StringBuilder(); 220 | foreach (var m in messages.Where(x => x.Level != TestMessageLevel.Informational)) 221 | { 222 | stdErr.AppendLine($"{m.Level} - {m.Message}"); 223 | } 224 | 225 | // Adding required properties, system-out, and system-err elements in the correct 226 | // positions as required by the xsd. In system-out collapse consequtive newlines to a 227 | // single newline. 228 | var element = new XElement( 229 | "testsuite", 230 | new XElement("properties"), 231 | testCaseElements, 232 | new XElement("system-out", stdOut.ToString()), 233 | new XElement("system-err", stdErr.ToString())); 234 | 235 | element.SetAttributeValue("name", Path.GetFileName(results.First().AssemblyPath)); 236 | 237 | element.SetAttributeValue("tests", results.Count); 238 | element.SetAttributeValue("skipped", results.Where(x => x.Outcome == TestOutcome.Skipped).Count()); 239 | element.SetAttributeValue("failures", results.Where(x => x.Outcome == TestOutcome.Failed).Count()); 240 | element.SetAttributeValue("errors", 0); // looks like this isn't supported by .net? 241 | element.SetAttributeValue("time", results.Sum(x => x.Duration.TotalSeconds).ToString(CultureInfo.InvariantCulture)); 242 | element.SetAttributeValue("timestamp", runConfiguration.StartTime.ToString("s")); 243 | element.SetAttributeValue("hostname", Environment.MachineName); 244 | element.SetAttributeValue("id", 0); // we never output multiple, so this is always zero. 245 | element.SetAttributeValue("package", Path.GetFileName(results.First().AssemblyPath)); 246 | 247 | return element; 248 | } 249 | 250 | private XElement CreateTestCaseElement(TestResultInfo result) 251 | { 252 | var testcaseElement = new XElement("testcase"); 253 | 254 | var namespaceClass = result.Namespace + "." + result.Type; 255 | 256 | testcaseElement.SetAttributeValue("classname", namespaceClass); 257 | 258 | if (this.MethodFormatOption == MethodFormat.Full) 259 | { 260 | testcaseElement.SetAttributeValue("name", namespaceClass + "." + result.Method); 261 | } 262 | else if (this.MethodFormatOption == MethodFormat.Class) 263 | { 264 | testcaseElement.SetAttributeValue("name", result.Type + "." + result.Method); 265 | } 266 | else 267 | { 268 | testcaseElement.SetAttributeValue("name", result.Method); 269 | } 270 | 271 | // Ensure time value is never zero because gitlab treats 0 like its null. 0.1 micro 272 | // seconds should be low enough it won't interfere with anyone monitoring test duration. 273 | testcaseElement.SetAttributeValue( 274 | "time", 275 | Math.Max(0.0000001f, result.Duration.TotalSeconds).ToString("0.0000000", CultureInfo.InvariantCulture)); 276 | 277 | if (result.Outcome == TestOutcome.Failed) 278 | { 279 | var failureBodySB = new StringBuilder(); 280 | 281 | if (this.FailureBodyFormatOption == FailureBodyFormat.Verbose) 282 | { 283 | failureBodySB.AppendLine(result.ErrorMessage); 284 | 285 | // Stack trace label included to mimic the normal test output 286 | failureBodySB.AppendLine("Stack Trace:"); 287 | } 288 | 289 | failureBodySB.AppendLine(result.ErrorStackTrace); 290 | 291 | if (this.FailureBodyFormatOption == FailureBodyFormat.Verbose && 292 | result.Messages.Count > 0) 293 | { 294 | failureBodySB.AppendLine("Standard Output:"); 295 | 296 | failureBodySB.AppendLine(Indent(result.Messages)); 297 | } 298 | 299 | var failureElement = new XElement("failure", failureBodySB.ToString().Trim()); 300 | 301 | failureElement.SetAttributeValue("type", "failure"); // TODO are there failure types? 302 | failureElement.SetAttributeValue("message", result.ErrorMessage); 303 | 304 | testcaseElement.Add(failureElement); 305 | } 306 | else if (result.Outcome == TestOutcome.Skipped) 307 | { 308 | var skippedElement = new XElement("skipped"); 309 | 310 | testcaseElement.Add(skippedElement); 311 | } 312 | 313 | return testcaseElement; 314 | } 315 | 316 | /// 317 | /// Performs logger specific configuration based the user's CLI flags, which are provided 318 | /// through . 319 | /// 320 | private void Configure(LoggerConfiguration loggerConfiguration) 321 | { 322 | if (loggerConfiguration.Values.TryGetValue(MethodFormatKey, out string methodFormat)) 323 | { 324 | if (string.Equals(methodFormat.Trim(), "Class", StringComparison.OrdinalIgnoreCase)) 325 | { 326 | this.MethodFormatOption = MethodFormat.Class; 327 | } 328 | else if (string.Equals(methodFormat.Trim(), "Full", StringComparison.OrdinalIgnoreCase)) 329 | { 330 | this.MethodFormatOption = MethodFormat.Full; 331 | } 332 | else if (string.Equals(methodFormat.Trim(), "Default", StringComparison.OrdinalIgnoreCase)) 333 | { 334 | this.MethodFormatOption = MethodFormat.Default; 335 | } 336 | else 337 | { 338 | Console.WriteLine($"JunitXML Logger: The provided Method Format '{methodFormat}' is not a recognized option. Using default"); 339 | } 340 | } 341 | 342 | if (loggerConfiguration.Values.TryGetValue(FailureBodyFormatKey, out string failureFormat)) 343 | { 344 | if (string.Equals(failureFormat.Trim(), "Verbose", StringComparison.OrdinalIgnoreCase)) 345 | { 346 | this.FailureBodyFormatOption = FailureBodyFormat.Verbose; 347 | } 348 | else if (string.Equals(failureFormat.Trim(), "Default", StringComparison.OrdinalIgnoreCase)) 349 | { 350 | this.FailureBodyFormatOption = FailureBodyFormat.Default; 351 | } 352 | else 353 | { 354 | Console.WriteLine($"JunitXML Logger: The provided Failure Body Format '{failureFormat}' is not a recognized option. Using default"); 355 | } 356 | } 357 | } 358 | 359 | public class TestSuite 360 | { 361 | public XElement Element { get; set; } 362 | 363 | public string Name { get; set; } 364 | 365 | public string FullName { get; set; } 366 | 367 | public int Total { get; set; } 368 | 369 | public int Passed { get; set; } 370 | 371 | public int Failed { get; set; } 372 | 373 | public int Inconclusive { get; set; } 374 | 375 | public int Skipped { get; set; } 376 | 377 | public int Error { get; set; } 378 | 379 | public TimeSpan Time { get; set; } 380 | } 381 | } 382 | } 383 | --------------------------------------------------------------------------------