├── .config └── dotnet-tools.json ├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── 10_bug_report.yml │ ├── 20_feature_request.yml │ ├── 30_question.yml │ └── config.yml ├── PULL_REQUEST_TEMPLATE.md ├── actionlint-matcher.json ├── dependabot.yml └── workflows │ ├── build.yml │ ├── bump-version.yml │ ├── codeql.yml │ ├── dependency-review.yml │ ├── lint.yml │ ├── ossf-scorecard.yml │ └── release.yml ├── .gitignore ├── .markdownlint.json ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── .vsconfig ├── CODE_OF_CONDUCT.md ├── Directory.Build.props ├── Directory.Packages.props ├── LICENSE ├── Logging.XUnit.ruleset ├── Logging.XUnit.slnx ├── NuGet.config ├── README.md ├── SECURITY.md ├── build.ps1 ├── docs └── images │ ├── output-terminal.png │ ├── output-vs.png │ └── output-vscode.png ├── global.json ├── package-icon.png ├── package-readme.md ├── src ├── Logging.XUnit.v3 │ ├── MartinCostello.Logging.XUnit.v3.csproj │ └── PublicAPI │ │ ├── PublicAPI.Shipped.txt │ │ ├── PublicAPI.Unshipped.txt │ │ ├── net10.0 │ │ ├── PublicAPI.Shipped.txt │ │ └── PublicAPI.Unshipped.txt │ │ └── net8.0 │ │ ├── PublicAPI.Shipped.txt │ │ └── PublicAPI.Unshipped.txt ├── Logging.XUnit │ ├── MartinCostello.Logging.XUnit.csproj │ └── PublicAPI │ │ ├── PublicAPI.Shipped.txt │ │ ├── PublicAPI.Unshipped.txt │ │ ├── net10.0 │ │ ├── PublicAPI.Shipped.txt │ │ └── PublicAPI.Unshipped.txt │ │ └── net8.0 │ │ ├── PublicAPI.Shipped.txt │ │ └── PublicAPI.Unshipped.txt └── Shared │ ├── AmbientTestOutputHelperAccessor.cs │ ├── IMessageSinkAccessor.cs │ ├── IMessageSinkExtensions.cs │ ├── ITestOutputHelperAccessor.cs │ ├── ITestOutputHelperExtensions.cs │ ├── MessageSinkAccessor.cs │ ├── StringSyntaxAttribute.cs │ ├── TestOutputHelperAccessor.cs │ ├── XUnitLogScope.cs │ ├── XUnitLogger.IMessageSink.cs │ ├── XUnitLogger.ITestOutputHelper.cs │ ├── XUnitLogger.cs │ ├── XUnitLoggerExtensions.IMessageSink.cs │ ├── XUnitLoggerExtensions.ITestOutputHelper.cs │ ├── XUnitLoggerOptions.cs │ ├── XUnitLoggerProvider.IMessageSink.cs │ ├── XUnitLoggerProvider.ITestOutputHelper.cs │ └── XUnitLoggerProvider.cs ├── stylecop.json └── tests ├── Logging.XUnit.Tests └── MartinCostello.Logging.XUnit.Tests.csproj ├── Logging.XUnit.v3.Tests └── MartinCostello.Logging.XUnit.v3.Tests.csproj ├── SampleApp ├── Program.cs ├── Properties │ └── launchSettings.json ├── SampleApp.csproj ├── appsettings.Development.json └── appsettings.json └── Shared ├── AssemblyTests.cs ├── Constructor.cs ├── Examples.cs ├── Integration ├── DatabaseFixture.cs ├── DatabaseTests.cs ├── HttpApplicationTests.cs ├── HttpServerCollection.cs ├── HttpServerFixture.cs └── PrintableDiagnosticMessage.cs ├── IntegrationTests.cs ├── XUnitLoggerExtensionsTests.cs ├── XUnitLoggerProviderTests.cs ├── XUnitLoggerTests.cs └── xunit.runner.json /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "dotnet-validate": { 6 | "version": "0.0.1-preview.537", 7 | "commands": [ 8 | "dotnet-validate" 9 | ] 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "C# (.NET)", 3 | "customizations": { 4 | "vscode": { 5 | "extensions": [ 6 | "editorconfig.editorconfig", 7 | "ms-dotnettools.csharp", 8 | "ms-vscode.PowerShell" 9 | ] 10 | } 11 | }, 12 | "postCreateCommand": "./build.ps1 -SkipTests", 13 | "remoteEnv": { 14 | "PATH": "/root/.dotnet/tools:${containerWorkspaceFolder}/.dotnet:${containerEnv:PATH}" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = crlf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{config,csproj,json,props,ruleset,targets,vsconfig,yml}] 12 | indent_size = 2 13 | 14 | # Code files 15 | [*.{cs,csx,vb,vbx}] 16 | file_header_template = Copyright (c) Martin Costello, 2018. All rights reserved.\nLicensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 17 | indent_size = 4 18 | insert_final_newline = true 19 | charset = utf-8-bom 20 | 21 | ############################### 22 | # .NET Coding Conventions # 23 | ############################### 24 | [*.{cs,vb}] 25 | 26 | # Enable style analyzers 27 | dotnet_analyzer_diagnostic.category-Style.severity = warning 28 | 29 | dotnet_diagnostic.IDE0005.severity = silent 30 | dotnet_diagnostic.IDE0045.severity = silent 31 | dotnet_diagnostic.IDE0046.severity = silent 32 | dotnet_diagnostic.IDE0058.severity = silent 33 | dotnet_diagnostic.IDE0072.severity = silent 34 | dotnet_diagnostic.IDE0079.severity = silent 35 | 36 | # Organize usings 37 | dotnet_sort_system_directives_first = true 38 | 39 | # this. preferences 40 | dotnet_style_qualification_for_field = false:none 41 | dotnet_style_qualification_for_property = false:none 42 | dotnet_style_qualification_for_method = false:none 43 | dotnet_style_qualification_for_event = false:none 44 | 45 | # Language keywords vs BCL types preferences 46 | dotnet_style_predefined_type_for_locals_parameters_members = true:none 47 | dotnet_style_predefined_type_for_member_access = true:none 48 | 49 | # Modifier preferences 50 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:none 51 | dotnet_style_readonly_field = true:suggestion 52 | 53 | # Expression-level preferences 54 | dotnet_style_object_initializer = true:suggestion 55 | dotnet_style_collection_initializer = true:suggestion 56 | dotnet_style_explicit_tuple_names = true:suggestion 57 | dotnet_style_null_propagation = true:suggestion 58 | dotnet_style_coalesce_expression = true:suggestion 59 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:none 60 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 61 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 62 | dotnet_style_prefer_auto_properties = true:none 63 | 64 | ############################### 65 | # Naming Conventions # 66 | ############################### 67 | 68 | # Style Definitions 69 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 70 | 71 | # Use PascalCase for constant fields 72 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 73 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 74 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 75 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 76 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 77 | dotnet_naming_symbols.constant_fields.required_modifiers = const 78 | 79 | ############################### 80 | # C# Coding Conventions # 81 | ############################### 82 | [*.cs] 83 | # var preferences 84 | csharp_style_var_for_built_in_types = true:none 85 | csharp_style_var_when_type_is_apparent = true:none 86 | csharp_style_var_elsewhere = true:none 87 | 88 | # Expression-bodied members 89 | csharp_style_expression_bodied_methods = false:none 90 | csharp_style_expression_bodied_constructors = false:none 91 | csharp_style_expression_bodied_operators = false:none 92 | csharp_style_expression_bodied_properties = true:none 93 | csharp_style_expression_bodied_indexers = true:none 94 | csharp_style_expression_bodied_accessors = true:none 95 | csharp_style_expression_bodied_local_functions = when_on_single_line 96 | 97 | # Pattern matching preferences 98 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 99 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 100 | 101 | # Null-checking preferences 102 | csharp_style_throw_expression = true:suggestion 103 | csharp_style_conditional_delegate_call = true:suggestion 104 | 105 | # Modifier preferences 106 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 107 | 108 | # Expression-level preferences 109 | csharp_prefer_braces = true:none 110 | csharp_style_deconstructed_variable_declaration = true:suggestion 111 | csharp_prefer_simple_default_expression = true:suggestion 112 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 113 | csharp_style_inlined_variable_declaration = true:suggestion 114 | 115 | # Namespace preferences 116 | csharp_style_namespace_declarations = file_scoped 117 | 118 | ############################### 119 | # C# Formatting Rules # 120 | ############################### 121 | # New line preferences 122 | csharp_new_line_before_open_brace = all 123 | csharp_new_line_before_else = true 124 | csharp_new_line_before_catch = true 125 | csharp_new_line_before_finally = true 126 | csharp_new_line_before_members_in_object_initializers = true 127 | csharp_new_line_before_members_in_anonymous_types = true 128 | csharp_new_line_between_query_expression_clauses = true 129 | 130 | # Indentation preferences 131 | csharp_indent_case_contents = true 132 | csharp_indent_switch_labels = true 133 | csharp_indent_labels = flush_left 134 | 135 | # Space preferences 136 | csharp_space_after_cast = false 137 | csharp_space_after_keywords_in_control_flow_statements = true 138 | csharp_space_between_method_call_parameter_list_parentheses = false 139 | csharp_space_between_method_declaration_parameter_list_parentheses = false 140 | csharp_space_between_parentheses = false 141 | csharp_space_before_colon_in_inheritance_clause = true 142 | csharp_space_after_colon_in_inheritance_clause = true 143 | csharp_space_around_binary_operators = before_and_after 144 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 145 | csharp_space_between_method_call_name_and_opening_parenthesis = false 146 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 147 | 148 | # Wrapping preferences 149 | csharp_preserve_single_line_statements = true 150 | csharp_preserve_single_line_blocks = true 151 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sh eol=lf 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @martincostello 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | To contribute changes (source code, scripts, configuration) to this repository please follow the steps below. These steps are a guideline for contributing and do not necessarily need to be followed for all changes. 4 | 5 | 1. If you intend to fix a bug please create an issue before forking the repository. 6 | 1. Fork the `main` branch of this repository from the latest commit. 7 | 1. Create a branch from your fork's `main` branch to help isolate your changes from any further work on `main`. If fixing an issue try to reference its name in your branch name (e.g. `issue-42`) to make changes easier to track the changes. 8 | 1. Work on your proposed changes on your fork. If you are fixing an issue include at least one unit test that reproduces it if the code changes to fix it have not been applied; if you are adding new functionality please include unit tests appropriate to the changes you are making. The [code coverage figure](https://codecov.io/gh/martincostello/xunit-logging) should be maintained where possible. 9 | 1. When you think your changes are complete, test that the code builds cleanly using `build.ps1`. There should be no compiler warnings and all tests should pass. 10 | 1. Once your changes build cleanly locally submit a Pull Request back to the `main` branch from your fork's branch. Ideally commits to your branch should be squashed before creating the Pull Request. If the Pull Request fixes an issue please reference it in the title and/or description. Please keep changes focused around a specific topic rather than include multiple types of changes in a single Pull Request. 11 | 1. After your Pull Request is created it will build against the repository's continuous integrations. 12 | 1. Once the Pull Request has been reviewed by the project's [contributors](https://github.com/martincostello/xunit-logging/graphs/contributors) and the status checks pass your Pull Request will be merged back to the `main` branch, assuming that the changes are deemed appropriate. 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [martincostello] 2 | buy_me_a_coffee: martincostello 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: '' 3 | --- 4 | 5 | ### Expected behaviour 6 | 7 | 8 | 9 | ### Actual behaviour 10 | 11 | 12 | 13 | ### Steps to reproduce 14 | 15 | 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/10_bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug report 2 | description: Something not behaving as expected? 3 | title: '[Bug]: ' 4 | labels: ['bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please check for an existing issue and the [README](https://github.com/martincostello/xunit-logging/blob/main/README.md) before submitting a bug report. 10 | 11 | If you're not using the latest release, please try upgrading to the latest version first to see if the issue resolves itself. 12 | - type: dropdown 13 | id: version 14 | attributes: 15 | label: NuGet Package(s) 16 | multiple: true 17 | description: What NuGet packages(s) are you using? 18 | options: 19 | - MartinCostello.Logging.XUnit 20 | - MartinCostello.Logging.XUnit.v3 21 | validations: 22 | required: true 23 | - type: input 24 | attributes: 25 | label: Version 26 | description: Which version of the library are you experiencing the issue with? 27 | placeholder: 0.5.1 28 | validations: 29 | required: true 30 | - type: textarea 31 | attributes: 32 | label: Describe the bug 33 | description: A clear and concise description of what the bug is. 34 | validations: 35 | required: true 36 | - type: textarea 37 | attributes: 38 | label: Expected behaviour 39 | description: A clear and concise description of what you expected to happen. 40 | validations: 41 | required: false 42 | - type: textarea 43 | attributes: 44 | label: Actual behaviour 45 | description: What actually happens. 46 | validations: 47 | required: false 48 | - type: textarea 49 | attributes: 50 | label: Steps to reproduce 51 | description: | 52 | Provide a link to a [minimalistic project which reproduces this issue (repro)](https://stackoverflow.com/help/mcve) hosted in a **public** GitHub repository. 53 | Code snippets, such as a failing unit test or small console app, which demonstrate the issue wrapped in a [fenced code block](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) are also acceptable. 54 | 55 | This issue will be closed if: 56 | - The behaviour you're reporting cannot be easily reproduced. 57 | - The issue is a duplicate of an existing issue. 58 | - The behaviour you're reporting is by design. 59 | validations: 60 | required: false 61 | - type: textarea 62 | attributes: 63 | label: Exception(s) (if any) 64 | description: Include any exception(s) and/or stack trace(s) you get when facing this issue. 65 | render: text 66 | validations: 67 | required: false 68 | - type: input 69 | attributes: 70 | label: .NET Version 71 | description: | 72 | Run `dotnet --version` to get the .NET SDK version you're using. 73 | Alternatively, which target framework(s) (e.g. `net8.0`) does the project you're using the package with target? 74 | placeholder: 9.0.100 75 | validations: 76 | required: false 77 | - type: textarea 78 | attributes: 79 | label: Anything else? 80 | description: | 81 | Links? References? Anything that will give us more context about the issue you are encountering is useful. 82 | 83 | 💡Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. 84 | validations: 85 | required: false 86 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/20_feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature request 2 | description: Suggest a feature request or improvement 3 | title: '[Feature request]: ' 4 | labels: ['feature-request'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please check for an existing issue and the [README](https://github.com/martincostello/xunit-logging/blob/main/README.md) before submitting a feature request. 10 | - type: textarea 11 | attributes: 12 | label: Is your feature request related to a specific problem? Or an existing feature? 13 | description: A clear and concise description of what the problem is. Motivating examples help prioritise things. 14 | placeholder: I am trying to [...] but [...] 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: Describe the solution you'd like 20 | description: | 21 | A clear and concise description of what you want to happen. Include any alternative solutions you've considered. 22 | validations: 23 | required: true 24 | - type: textarea 25 | attributes: 26 | label: Additional context 27 | description: | 28 | Add any other context or screenshots about the feature request here. 29 | validations: 30 | required: false 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/30_question.yml: -------------------------------------------------------------------------------- 1 | name: 🤔 Question? 2 | description: You have something specific to achieve and the existing documentation hasn't covered how. 3 | title: '[Question]: ' 4 | labels: ['question'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Please check for an existing issue and the [README](https://github.com/martincostello/xunit-logging/blob/main/README.md) before asking a question. 10 | - type: textarea 11 | attributes: 12 | label: What do you want to achieve? 13 | description: A clear and concise description of what you're trying to do. 14 | placeholder: I am trying to [...] but [...] 15 | validations: 16 | required: true 17 | - type: textarea 18 | attributes: 19 | label: What code or approach do you have so far? 20 | description: | 21 | Provide a [minimalistic project which shows what you have so far](https://stackoverflow.com/help/mcve) hosted in a **public** GitHub repository. 22 | Code snippets wrapped in a [fenced code block](https://docs.github.com/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks) are also acceptable. 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Additional context 28 | description: | 29 | Add any other context or screenshots related to your question here. 30 | validations: 31 | required: false 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: Contact me 4 | url: https://martincostello.com/bluesky 5 | about: You can also contact me on Bluesky. 6 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /.github/actionlint-matcher.json: -------------------------------------------------------------------------------- 1 | { 2 | "problemMatcher": [ 3 | { 4 | "owner": "actionlint", 5 | "pattern": [ 6 | { 7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$", 8 | "file": 1, 9 | "line": 2, 10 | "column": 3, 11 | "message": 4, 12 | "code": 5 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "05:30" 8 | timezone: Europe/London 9 | - package-ecosystem: nuget 10 | directory: "/" 11 | groups: 12 | xunit: 13 | patterns: 14 | - xunit.runner.visualstudio 15 | - xunit.v3 16 | schedule: 17 | interval: daily 18 | time: "05:30" 19 | timezone: Europe/London 20 | open-pull-requests-limit: 99 21 | ignore: 22 | - dependency-name: Microsoft.Extensions.Logging 23 | - dependency-name: xunit.abstractions 24 | - dependency-name: xunit.extensibility.execution 25 | - dependency-name: xunit.v3.extensibility.core 26 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: [ v* ] 7 | pull_request: 8 | branches: 9 | - main 10 | - dotnet-vnext 11 | - dotnet-nightly 12 | workflow_dispatch: 13 | 14 | env: 15 | DOTNET_CLI_TELEMETRY_OPTOUT: true 16 | DOTNET_NOLOGO: true 17 | DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION: 1 18 | FORCE_COLOR: 3 19 | NUGET_XMLDOC_MODE: skip 20 | TERM: xterm 21 | 22 | permissions: 23 | contents: read 24 | 25 | jobs: 26 | build: 27 | name: ${{ matrix.os-name }} 28 | runs-on: ${{ matrix.runner }} 29 | timeout-minutes: 20 30 | 31 | outputs: 32 | dotnet-sdk-version: ${{ steps.setup-dotnet.outputs.dotnet-version }} 33 | dotnet-validate-version: ${{ steps.get-dotnet-validate-version.outputs.dotnet-validate-version }} 34 | package-names: ${{ steps.build.outputs.package-names }} 35 | package-version: ${{ steps.build.outputs.package-version }} 36 | 37 | permissions: 38 | attestations: write 39 | contents: write 40 | id-token: write 41 | 42 | strategy: 43 | fail-fast: false 44 | matrix: 45 | include: 46 | - os-name: macos 47 | runner: macos-latest 48 | - os-name: linux 49 | runner: ubuntu-latest 50 | - os-name: windows 51 | runner: windows-latest 52 | 53 | steps: 54 | 55 | - name: Checkout code 56 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 57 | with: 58 | filter: 'tree:0' 59 | show-progress: false 60 | 61 | - name: Setup .NET SDK 62 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 63 | id: setup-dotnet 64 | 65 | - name: Build, Test and Package 66 | id: build 67 | shell: pwsh 68 | run: ./build.ps1 69 | 70 | - name: Upload coverage to Codecov 71 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 72 | with: 73 | flags: ${{ matrix.os-name }} 74 | token: ${{ secrets.CODECOV_TOKEN }} 75 | 76 | - name: Upload test results to Codecov 77 | uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1 78 | if: ${{ !cancelled() }} 79 | with: 80 | flags: ${{ matrix.os-name }} 81 | token: ${{ secrets.CODECOV_TOKEN }} 82 | 83 | - name: Generate SBOM 84 | uses: anchore/sbom-action@e11c554f704a0b820cbf8c51673f6945e0731532 # v0.20.0 85 | with: 86 | artifact-name: build-${{ matrix.os-name }}.spdx.json 87 | output-file: ./artifacts/build.spdx.json 88 | path: ./artifacts/bin 89 | upload-release-assets: ${{ runner.os == 'Windows' }} 90 | 91 | - name: Attest artifacts 92 | uses: actions/attest-build-provenance@db473fddc028af60658334401dc6fa3ffd8669fd # v2.3.0 93 | if: | 94 | runner.os == 'Windows' && 95 | github.event.repository.fork == false && 96 | (github.ref_name == github.event.repository.default_branch || startsWith(github.ref, 'refs/tags/v')) 97 | with: 98 | subject-path: | 99 | ./artifacts/bin/MartinCostello.Logging.XUnit/release*/*.dll 100 | ./artifacts/package/release/* 101 | 102 | - name: Publish artifacts 103 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 104 | with: 105 | name: artifacts-${{ matrix.os-name }} 106 | path: ./artifacts 107 | 108 | - name: Publish NuGet packages 109 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 110 | with: 111 | name: packages-${{ matrix.os-name }} 112 | path: ./artifacts/package/release 113 | if-no-files-found: error 114 | 115 | - name: Get dotnet-validate version 116 | id: get-dotnet-validate-version 117 | shell: pwsh 118 | run: | 119 | $dotnetValidateVersion = (Get-Content "./.config/dotnet-tools.json" | Out-String | ConvertFrom-Json).tools.'dotnet-validate'.version 120 | "dotnet-validate-version=${dotnetValidateVersion}" >> $env:GITHUB_OUTPUT 121 | 122 | validate-packages: 123 | needs: build 124 | runs-on: ubuntu-latest 125 | steps: 126 | 127 | - name: Download packages 128 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 129 | with: 130 | name: packages-windows 131 | 132 | - name: Setup .NET SDK 133 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 134 | with: 135 | dotnet-version: ${{ needs.build.outputs.dotnet-sdk-version }} 136 | 137 | - name: Validate NuGet packages 138 | shell: pwsh 139 | env: 140 | DOTNET_VALIDATE_VERSION: ${{ needs.build.outputs.dotnet-validate-version }} 141 | run: | 142 | dotnet tool install --global dotnet-validate --version ${env:DOTNET_VALIDATE_VERSION} --allow-roll-forward 143 | $packages = Get-ChildItem -Filter "*.nupkg" | ForEach-Object { $_.FullName } 144 | $invalidPackages = 0 145 | foreach ($package in $packages) { 146 | dotnet validate package local $package 147 | if ($LASTEXITCODE -ne 0) { 148 | $invalidPackages++ 149 | } 150 | } 151 | if ($invalidPackages -gt 0) { 152 | Write-Output "::error::$invalidPackages NuGet package(s) failed validation." 153 | exit 1 154 | } 155 | 156 | publish-feedz-io: 157 | needs: [ build, validate-packages ] 158 | runs-on: ubuntu-latest 159 | if: | 160 | github.event.repository.fork == false && 161 | (github.ref_name == github.event.repository.default_branch || 162 | startsWith(github.ref, 'refs/tags/v')) 163 | 164 | environment: 165 | name: feedz.io 166 | url: https://feedz.io/org/${{ github.repository_owner }}/repository/xunit-logging/packages/MartinCostello.Logging.XUnit 167 | 168 | steps: 169 | 170 | - name: Download packages 171 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 172 | with: 173 | name: packages-windows 174 | 175 | - name: Setup .NET SDK 176 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 177 | with: 178 | dotnet-version: ${{ needs.build.outputs.dotnet-sdk-version }} 179 | 180 | - name: Push NuGet packages to feedz.io 181 | shell: bash 182 | env: 183 | API_KEY: ${{ secrets.FEEDZ_IO_TOKEN }} 184 | PACKAGE_VERSION: ${{ needs.build.outputs.package-version }} 185 | SOURCE: "https://f.feedz.io/${{ github.repository }}/nuget/index.json" 186 | run: dotnet nuget push "*.nupkg" --api-key "${API_KEY}" --skip-duplicate --source "${SOURCE}" && echo "::notice title=feedz.io::Published version ${PACKAGE_VERSION} to feedz.io." 187 | 188 | publish-nuget: 189 | needs: [ build, validate-packages ] 190 | runs-on: ubuntu-latest 191 | if: | 192 | github.event.repository.fork == false && 193 | startsWith(github.ref, 'refs/tags/v') 194 | 195 | environment: 196 | name: NuGet.org 197 | url: https://www.nuget.org/packages/MartinCostello.Logging.XUnit 198 | 199 | steps: 200 | 201 | - name: Download packages 202 | uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 203 | with: 204 | name: packages-windows 205 | 206 | - name: Setup .NET SDK 207 | uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 208 | with: 209 | dotnet-version: ${{ needs.build.outputs.dotnet-sdk-version }} 210 | 211 | - name: Push NuGet packages to NuGet.org 212 | shell: bash 213 | env: 214 | API_KEY: ${{ secrets.NUGET_TOKEN }} 215 | PACKAGE_VERSION: ${{ needs.build.outputs.package-version }} 216 | SOURCE: https://api.nuget.org/v3/index.json 217 | run: dotnet nuget push "*.nupkg" --api-key "${API_KEY}" --skip-duplicate --source "${SOURCE}" && echo "::notice title=nuget.org::Published version ${PACKAGE_VERSION} to NuGet.org." 218 | 219 | - name: Publish nuget_packages_published 220 | uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 221 | with: 222 | event-type: nuget_packages_published 223 | repository: ${{ github.repository_owner }}/github-automation 224 | token: ${{ secrets.COSTELLOBOT_TOKEN }} 225 | client-payload: |- 226 | { 227 | "repository": "${{ github.repository }}", 228 | "packages": "${{ needs.build.outputs.package-names }}", 229 | "version": "${{ needs.build.outputs.package-version }}" 230 | } 231 | -------------------------------------------------------------------------------- /.github/workflows/bump-version.yml: -------------------------------------------------------------------------------- 1 | name: bump-version 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | workflow_dispatch: 7 | inputs: 8 | version: 9 | description: 'The optional version string for the next release.' 10 | required: false 11 | type: string 12 | default: '' 13 | 14 | permissions: {} 15 | 16 | jobs: 17 | bump-version: 18 | runs-on: [ ubuntu-latest ] 19 | 20 | concurrency: 21 | group: '${{ github.workflow }}-bump' 22 | cancel-in-progress: false 23 | 24 | steps: 25 | 26 | - name: Checkout code 27 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 28 | with: 29 | filter: 'tree:0' 30 | show-progress: false 31 | token: ${{ secrets.COSTELLOBOT_TOKEN }} 32 | 33 | - name: Bump version 34 | id: bump-version 35 | shell: pwsh 36 | env: 37 | NEXT_VERSION: ${{ inputs.version }} 38 | run: | 39 | $properties = Join-Path "." "Directory.Build.props" 40 | 41 | $xml = [xml](Get-Content $properties) 42 | $versionPrefix = $xml.SelectSingleNode('Project/PropertyGroup/VersionPrefix') 43 | $publishedVersion = $versionPrefix.InnerText 44 | 45 | if (-Not [string]::IsNullOrEmpty(${env:NEXT_VERSION})) { 46 | $version = [System.Version]::new(${env:NEXT_VERSION}) 47 | $assemblyVersionProperty = $xml.SelectSingleNode('Project/PropertyGroup/AssemblyVersion') 48 | $assemblyVersion = [System.Version]::new($version.Major, ($version.Major -eq 0 ? $version.Minor : 0), 0, 0) 49 | $assemblyVersionProperty.InnerText = $assemblyVersion.ToString() 50 | } else { 51 | $version = [System.Version]::new($publishedVersion) 52 | $version = [System.Version]::new($version.Major, $version.Minor, $version.Build + 1) 53 | } 54 | 55 | $updatedVersion = $version.ToString() 56 | $versionPrefix.InnerText = $updatedVersion 57 | 58 | $packageValidationBaselineVersion = $xml.SelectSingleNode('Project/PropertyGroup/PackageValidationBaselineVersion') 59 | $packageValidationBaselineVersion.InnerText = $publishedVersion 60 | 61 | $settings = New-Object System.Xml.XmlWriterSettings 62 | $settings.Encoding = New-Object System.Text.UTF8Encoding($false) 63 | $settings.Indent = $true 64 | $settings.OmitXmlDeclaration = $true 65 | 66 | $writer = [System.Xml.XmlWriter]::Create($properties, $settings) 67 | 68 | $xml.Save($writer) 69 | 70 | $writer.Flush() 71 | $writer.Close() 72 | $writer = $null 73 | 74 | "" >> $properties 75 | 76 | "version=${updatedVersion}" >> $env:GITHUB_OUTPUT 77 | 78 | - name: Push changes to GitHub 79 | id: push-changes 80 | shell: pwsh 81 | env: 82 | GIT_COMMIT_USER_EMAIL: ${{ vars.GIT_COMMIT_USER_EMAIL }} 83 | GIT_COMMIT_USER_NAME: ${{ vars.GIT_COMMIT_USER_NAME }} 84 | NEXT_VERSION: ${{ steps.bump-version.outputs.version }} 85 | run: | 86 | $gitStatus = (git status --porcelain) 87 | 88 | if ([string]::IsNullOrEmpty($gitStatus)) { 89 | throw "No changes to commit." 90 | } 91 | 92 | git config color.diff always 93 | git --no-pager diff 94 | 95 | $branchName = "bump-version-${env:NEXT_VERSION}" 96 | 97 | git config user.email ${env:GIT_COMMIT_USER_EMAIL} | Out-Null 98 | git config user.name ${env:GIT_COMMIT_USER_NAME} | Out-Null 99 | git fetch origin --no-tags | Out-Null 100 | git rev-parse --verify --quiet "remotes/origin/${branchName}" | Out-Null 101 | 102 | if ($LASTEXITCODE -eq 0) { 103 | Write-Output "Branch ${branchName} already exists." 104 | exit 0 105 | } 106 | 107 | git checkout -b $branchName 108 | git add . 109 | git commit -m "Bump version`n`nBump version to ${env:NEXT_VERSION} for the next release." 110 | git push -u origin $branchName 111 | 112 | "branch-name=${branchName}" >> $env:GITHUB_OUTPUT 113 | "updated-version=true" >> $env:GITHUB_OUTPUT 114 | "version=${env:NEXT_VERSION}" >> $env:GITHUB_OUTPUT 115 | 116 | - name: Create pull request 117 | if: steps.push-changes.outputs.updated-version == 'true' 118 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 119 | env: 120 | BASE_BRANCH: ${{ github.event.repository.default_branch }} 121 | HEAD_BRANCH: ${{ steps.push-changes.outputs.branch-name }} 122 | NEXT_VERSION: ${{ steps.push-changes.outputs.version }} 123 | with: 124 | github-token: ${{ secrets.COSTELLOBOT_TOKEN }} 125 | script: | 126 | const nextVersion = process.env.NEXT_VERSION; 127 | const { repo, owner } = context.repo; 128 | const workflowUrl = `${process.env.GITHUB_SERVER_URL}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; 129 | 130 | const { data: pr } = await github.rest.pulls.create({ 131 | title: 'Bump version', 132 | owner, 133 | repo, 134 | head: process.env.HEAD_BRANCH, 135 | base: process.env.BASE_BRANCH, 136 | draft: true, 137 | body: [ 138 | `Bump version to \`${nextVersion}\` for the next release.`, 139 | '', 140 | `This pull request was generated by [GitHub Actions](${workflowUrl}).` 141 | ].join('\n') 142 | }); 143 | 144 | core.notice(`Created pull request ${owner}/${repo}#${pr.number}: ${pr.html_url}`); 145 | 146 | try { 147 | const { data: milestones } = await github.rest.issues.listMilestones({ 148 | owner, 149 | repo, 150 | state: 'open', 151 | }); 152 | 153 | const title = `v${nextVersion}`; 154 | let milestone = milestones.find((p) => p.title === title); 155 | 156 | if (!milestone) { 157 | const created = await github.rest.issues.createMilestone({ 158 | owner, 159 | repo, 160 | title, 161 | }); 162 | milestone = created.data; 163 | } 164 | 165 | await github.rest.issues.update({ 166 | owner, 167 | repo, 168 | issue_number: pr.number, 169 | milestone: milestone.number 170 | }); 171 | } catch (error) { 172 | // Ignore 173 | } 174 | 175 | close-milestone: 176 | runs-on: [ ubuntu-latest ] 177 | if: github.event_name == 'release' 178 | 179 | concurrency: 180 | group: '${{ github.workflow }}-milestone' 181 | cancel-in-progress: false 182 | 183 | steps: 184 | 185 | - name: Close milestone 186 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 187 | env: 188 | RELEASE_DATE: ${{ github.event.release.published_at }} 189 | RELEASE_VERSION: ${{ github.event.release.tag_name }} 190 | with: 191 | github-token: ${{ secrets.COSTELLOBOT_TOKEN }} 192 | script: | 193 | const { repo, owner } = context.repo; 194 | 195 | const { data: milestones } = await github.rest.issues.listMilestones({ 196 | owner, 197 | repo, 198 | state: 'open', 199 | }); 200 | 201 | const milestone = milestones.find((p) => p.title === process.env.RELEASE_VERSION); 202 | 203 | if (!milestone) { 204 | return; 205 | } 206 | 207 | try { 208 | await github.rest.issues.updateMilestone({ 209 | owner, 210 | repo, 211 | milestone_number: milestone.number, 212 | state: 'closed', 213 | due_on: process.env.RELEASE_DATE, 214 | }); 215 | } catch (error) { 216 | // Ignore 217 | } 218 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: codeql 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: 8 | - main 9 | - dotnet-vnext 10 | - dotnet-nightly 11 | schedule: 12 | - cron: '0 6 * * MON' 13 | workflow_dispatch: 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | analysis: 19 | runs-on: ubuntu-latest 20 | 21 | permissions: 22 | actions: read 23 | contents: read 24 | security-events: write 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | language: [ 'actions', 'csharp' ] 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | with: 35 | filter: 'tree:0' 36 | show-progress: false 37 | 38 | - name: Initialize CodeQL 39 | uses: github/codeql-action/init@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 40 | with: 41 | build-mode: none 42 | languages: ${{ matrix.language }} 43 | queries: security-and-quality 44 | 45 | - name: Perform CodeQL Analysis 46 | uses: github/codeql-action/analyze@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 47 | with: 48 | category: '/language:${{ matrix.language }}' 49 | 50 | codeql: 51 | if: ${{ !cancelled() }} 52 | needs: [ analysis ] 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - name: Report status 57 | shell: bash 58 | env: 59 | SCAN_SUCCESS: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} 60 | run: | 61 | if [ "${SCAN_SUCCESS}" == "true" ] 62 | then 63 | echo 'CodeQL analysis successful ✅' 64 | else 65 | echo 'CodeQL analysis failed ❌' 66 | exit 1 67 | fi 68 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | name: dependency-review 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | - dotnet-vnext 8 | - dotnet-nightly 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | 19 | - name: Checkout code 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 21 | with: 22 | filter: 'tree:0' 23 | show-progress: false 24 | 25 | - name: Review dependencies 26 | uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - '**/*.gitattributes' 8 | - '**/*.gitignore' 9 | - '**/*.md' 10 | pull_request: 11 | branches: 12 | - main 13 | - dotnet-vnext 14 | - dotnet-nightly 15 | workflow_dispatch: 16 | 17 | permissions: 18 | contents: read 19 | 20 | env: 21 | FORCE_COLOR: 3 22 | POWERSHELL_YAML_VERSION: '0.4.12' 23 | PSSCRIPTANALYZER_VERSION: '1.24.0' 24 | TERM: xterm 25 | 26 | jobs: 27 | lint: 28 | runs-on: ubuntu-latest 29 | 30 | steps: 31 | 32 | - name: Checkout code 33 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | with: 35 | filter: 'tree:0' 36 | show-progress: false 37 | 38 | - name: Add actionlint problem matcher 39 | run: echo "::add-matcher::.github/actionlint-matcher.json" 40 | 41 | - name: Lint workflows 42 | uses: docker://rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 # v1.7.7 43 | with: 44 | args: -color 45 | 46 | - name: Lint markdown 47 | uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0 48 | with: 49 | config: '.markdownlint.json' 50 | globs: | 51 | **/*.md 52 | 53 | - name: Lint PowerShell in workflows 54 | uses: martincostello/lint-actions-powershell@5942e3350ee5bd8f8933cec4e1185d13f0ea688f # v1.0.0 55 | with: 56 | powershell-yaml-version: ${{ env.POWERSHELL_YAML_VERSION }} 57 | psscriptanalyzer-version: ${{ env.PSSCRIPTANALYZER_VERSION }} 58 | treat-warnings-as-errors: true 59 | 60 | - name: Lint PowerShell scripts 61 | shell: pwsh 62 | run: | 63 | $settings = @{ 64 | IncludeDefaultRules = $true 65 | Severity = @("Error", "Warning") 66 | } 67 | $issues = Invoke-ScriptAnalyzer -Path ${env:GITHUB_WORKSPACE} -Recurse -ReportSummary -Settings $settings 68 | foreach ($issue in $issues) { 69 | $severity = $issue.Severity.ToString() 70 | $level = $severity.Contains("Error") ? "error" : $severity.Contains("Warning") ? "warning" : "notice" 71 | Write-Output "::${level} file=$($issue.ScriptName),line=$($issue.Line),title=PSScriptAnalyzer::$($issue.Message)" 72 | } 73 | if ($issues.Count -gt 0) { 74 | exit 1 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/ossf-scorecard.yml: -------------------------------------------------------------------------------- 1 | name: ossf-scorecard 2 | 3 | on: 4 | branch_protection_rule: 5 | push: 6 | branches: [ main ] 7 | schedule: 8 | - cron: '0 5 * * MON' 9 | workflow_dispatch: 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | analysis: 15 | name: analysis 16 | runs-on: ubuntu-latest 17 | 18 | permissions: 19 | id-token: write 20 | security-events: write 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 25 | with: 26 | filter: 'tree:0' 27 | persist-credentials: false 28 | show-progress: false 29 | 30 | - name: Run analysis 31 | uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 32 | with: 33 | publish_results: true 34 | results_file: results.sarif 35 | results_format: sarif 36 | 37 | - name: Upload artifact 38 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 39 | with: 40 | name: SARIF 41 | path: results.sarif 42 | retention-days: 5 43 | 44 | - name: Upload to code-scanning 45 | uses: github/codeql-action/upload-sarif@fca7ace96b7d713c7035871441bd52efbe39e27e # v3.28.19 46 | with: 47 | sarif_file: results.sarif 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | publish: 7 | description: 'If true, does not create the release as a draft.' 8 | required: false 9 | type: boolean 10 | default: false 11 | 12 | permissions: {} 13 | 14 | jobs: 15 | release: 16 | runs-on: [ ubuntu-latest ] 17 | 18 | concurrency: 19 | group: ${{ github.workflow }} 20 | cancel-in-progress: false 21 | 22 | steps: 23 | 24 | - name: Checkout code 25 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 26 | with: 27 | filter: 'tree:0' 28 | show-progress: false 29 | token: ${{ secrets.COSTELLOBOT_TOKEN }} 30 | 31 | - name: Get version 32 | id: get-version 33 | shell: pwsh 34 | run: | 35 | $properties = Join-Path "." "Directory.Build.props" 36 | $xml = [xml](Get-Content $properties) 37 | $version = $xml.SelectSingleNode('Project/PropertyGroup/VersionPrefix').InnerText 38 | "version=${version}" >> $env:GITHUB_OUTPUT 39 | 40 | - name: Create release 41 | uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 42 | env: 43 | DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} 44 | DRAFT: ${{ inputs.publish != true }} 45 | VERSION: ${{ steps.get-version.outputs.version }} 46 | with: 47 | github-token: ${{ secrets.COSTELLOBOT_TOKEN }} 48 | script: | 49 | const { repo, owner } = context.repo; 50 | const draft = process.env.DRAFT === 'true'; 51 | const version = process.env.VERSION; 52 | const tag_name = `v${version}`; 53 | const name = tag_name; 54 | 55 | const { data: notes } = await github.rest.repos.generateReleaseNotes({ 56 | owner, 57 | repo, 58 | tag_name, 59 | target_commitish: process.env.DEFAULT_BRANCH, 60 | }); 61 | 62 | const body = notes.body 63 | .split('\n') 64 | .filter((line) => !line.includes(' @costellobot ')) 65 | .filter((line) => !line.includes(' @dependabot ')) 66 | .filter((line) => !line.includes(' @github-actions ')) 67 | .join('\n'); 68 | 69 | const { data: release } = await github.rest.repos.createRelease({ 70 | owner, 71 | repo, 72 | tag_name, 73 | name, 74 | body, 75 | draft, 76 | }); 77 | 78 | core.notice(`Created release ${release.name}: ${release.html_url}`); 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dotnet 3 | .idea 4 | .metadata 5 | .settings 6 | .vs 7 | _ReSharper* 8 | _reports 9 | _UpgradeReport_Files/ 10 | artifacts/ 11 | Backup*/ 12 | bin 13 | Bin 14 | coverage 15 | coverage.* 16 | junit.xml 17 | MSBuild_Logs/ 18 | obj 19 | packages 20 | TestResults 21 | UpgradeLog*.htm 22 | UpgradeLog*.XML 23 | *.binlog 24 | *.coverage 25 | *.DotSettings 26 | *.GhostDoc.xml 27 | *.log 28 | *.nupkg 29 | !.packages/*.nupkg 30 | *.opensdf 31 | *.[Pp]ublish.xml 32 | *.publishproj 33 | *.pubxml 34 | *.sdf 35 | *.sln.cache 36 | *.sln.docstates 37 | *.sln.ide 38 | *.suo 39 | *.user 40 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, 3 | "MD040": false 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "ms-dotnettools.csharp", 5 | "ms-vscode.PowerShell" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Run tests", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "preLaunchTask": "build", 9 | "program": "dotnet", 10 | "args": [ 11 | "test" 12 | ], 13 | "cwd": "${workspaceFolder}/tests/Logging.XUnit.Tests", 14 | "console": "internalConsole", 15 | "stopAtEntry": false, 16 | "internalConsoleOptions": "openOnSessionStart" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet build", 7 | "type": "shell", 8 | "group": "build", 9 | "presentation": { 10 | "reveal": "silent" 11 | }, 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.vsconfig: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0", 3 | "components": [ 4 | "Microsoft.VisualStudio.Component.CoreEditor", 5 | "Microsoft.VisualStudio.Workload.CoreEditor", 6 | "Microsoft.NetCore.Component.Runtime.9.0", 7 | "Microsoft.NetCore.Component.SDK", 8 | "Microsoft.VisualStudio.Component.Roslyn.Compiler", 9 | "Microsoft.VisualStudio.Component.Roslyn.LanguageServices" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team [through a GitHub issue](https://github.com/martincostello/xunit-logging/issues). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | $(MSBuildThisFileDirectory)Logging.XUnit.ruleset 5 | true 6 | direct 7 | xunit;logging 8 | true 9 | true 10 | 0.6.0.0 11 | 0.6.0 12 | 0.6.1 13 | 14 | 15 | true 16 | $(NoWarn);419;1570;1573;1574;1584;1591;SA0001;SA1602 17 | 18 | 19 | true 20 | cobertura,json 21 | [SampleApp]*,[xunit.*]* 22 | 93 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2018 Martin Costello 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /Logging.XUnit.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Logging.XUnit.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xunit Logging 2 | 3 | [![Build status][build-badge]][build-status] 4 | [![codecov][coverage-badge]][coverage-report] 5 | [![OpenSSF Scorecard][scorecard-badge]][scorecard-report] 6 | 7 | | **xunit version** | **Package** | **NuGet Version** | 8 | |:------------------|:------------|:------------------| 9 | | xunit v2 | [MartinCostello.Logging.XUnit][package-download-v2] | [![NuGet][package-badge-version-v2]][package-download-v2] [![NuGet Downloads][package-badge-downloads-v2]][package-download-v2] | 10 | | xunit v3 | [MartinCostello.Logging.XUnit.v3][package-download-v3] | [![NuGet][package-badge-version-v3]][package-download-v3] [![NuGet Downloads][package-badge-downloads-v3]][package-download-v3] | 11 | 12 | ## Introduction 13 | 14 | `MartinCostello.Logging.XUnit` and `MartinCostello.Logging.XUnit.v3` provide extensions to hook into 15 | the `ILogger` infrastructure to output logs from your xunit tests to the test output. 16 | 17 | Projects using xunit v2 should use the `MartinCostello.Logging.XUnit` package, while projects using 18 | xunit v3 should use the `MartinCostello.Logging.XUnit.v3` package. 19 | 20 | > [!NOTE] 21 | > This library is designed for the Microsoft logging implementation of `ILoggerFactory`. 22 | > For other logging implementations, such as [Serilog][serilog], consider using packages such as [Serilog.Sinks.XUnit][serilog-sinks-xunit] instead. 23 | 24 | ### Installation 25 | 26 | To install the library from NuGet using the .NET SDK run one of the following commands. 27 | 28 | #### For xunit v2 29 | 30 | ```console 31 | dotnet add package MartinCostello.Logging.XUnit 32 | ``` 33 | 34 | #### For xunit v3 35 | 36 | ```console 37 | dotnet add package MartinCostello.Logging.XUnit.v3 38 | ``` 39 | 40 | ### Usage 41 | 42 | #### Dependency Injection 43 | 44 | ```csharp 45 | using Microsoft.Extensions.DependencyInjection; 46 | using Microsoft.Extensions.Logging; 47 | using Xunit; 48 | using Xunit.Abstractions; // For xunit v2 - not required for xunit v3 49 | 50 | namespace MyApp.Calculator; 51 | 52 | public class CalculatorTests(ITestOutputHelper outputHelper) 53 | { 54 | [Fact] 55 | public void Calculator_Sums_Two_Integers() 56 | { 57 | // Arrange 58 | using var serviceProvider = new ServiceCollection() 59 | .AddLogging((builder) => builder.AddXUnit(outputHelper)) 60 | .AddSingleton() 61 | .BuildServiceProvider(); 62 | 63 | var calculator = services.GetRequiredService(); 64 | 65 | // Act 66 | int actual = calculator.Sum(1, 2); 67 | 68 | // Assert 69 | Assert.AreEqual(3, actual); 70 | } 71 | } 72 | 73 | public sealed class Calculator(ILogger logger) 74 | { 75 | public int Sum(int x, int y) 76 | { 77 | int sum = x + y; 78 | 79 | logger.LogInformation("The sum of {x} and {y} is {sum}.", x, y, sum); 80 | 81 | return sum; 82 | } 83 | } 84 | ``` 85 | 86 | #### Standalone Logging Components 87 | 88 | ```csharp 89 | using Microsoft.Extensions.DependencyInjection; 90 | using Microsoft.Extensions.Logging; 91 | using Xunit; 92 | using Xunit.Abstractions; // For xunit v2 - not required for xunit v3 93 | 94 | namespace MyApp.Calculator; 95 | 96 | public class CalculatorTests(ITestOutputHelper outputHelper) 97 | { 98 | [Fact] 99 | public void Calculator_Sums_Two_Integers() 100 | { 101 | // Arrange 102 | var loggerFactory = LoggerFactory.Create(builder => builder 103 | .AddProvider(new XUnitLoggerProvider(outputHelper, xunitLoggerOptions)) 104 | .SetMinimumLevel(LogLevel.Trace)); 105 | 106 | var logger = loggerFactory.CreateLogger(); 107 | 108 | var calculator = new Calculator(logger); 109 | 110 | // Act 111 | int actual = calculator.Sum(1, 2); 112 | 113 | // Assert 114 | Assert.AreEqual(3, actual); 115 | } 116 | } 117 | 118 | public sealed class Calculator(ILogger logger) 119 | { 120 | public int Sum(int x, int y) 121 | { 122 | int sum = x + y; 123 | 124 | logger.LogInformation("The sum of {x} and {y} is {sum}.", x, y, sum); 125 | 126 | return sum; 127 | } 128 | } 129 | ``` 130 | 131 | See below for links to more examples: 132 | 133 | - [Unit tests][example-unit-tests] 134 | - [Integration tests for an ASP.NET Core HTTP application][example-integration-tests] 135 | 136 | ## Example Output 137 | 138 | If your tests (and the system under test) are correctly configured, then you should see output from the 139 | tests in the same places you would expected to see [output from xunit tests][xunit-output]. 140 | 141 | See below for some examples from the [`Http_Get_Many`][example-test] test in this repository. 142 | 143 | 144 | 145 | ### Visual Studio 146 | 147 |
148 | Click to expand 149 | 150 | ![Visual Studio Output][output-vs] 151 | 152 |
153 | 154 | ### Visual Studio Code 155 | 156 |
157 | Click to expand 158 | 159 | ![Visual Studio Code Output][output-vscode] 160 | 161 |
162 | 163 | ### Windows Terminal and .NET CLI 164 | 165 |
166 | Click to expand 167 | 168 | ![Windows Terminal Output][output-windows-terminal] 169 | 170 |
171 | 172 | ## Migrating to xunit v3 173 | 174 | [Xunit v3][xunit-v3-whats-new] contains many major architectural changes which means the same package 175 | that supports logging for xunit v2 cannot be used with xunit v3. The equivalent NuGet package to support 176 | logging for xunit v3 is the new [MartinCostello.Logging.XUnit.v3][package-download-v3] package. 177 | 178 | To migrate usage of `MartinCostello.Logging.XUnit` to `MartinCostello.Logging.XUnit.v3` for xunit v3: 179 | 180 | 1. Follow the relevant steps to migrate any test projects from [xunit v2 to v3][xunit-v3-migration]. 181 | - The most relevant change in xunit v3 is that the `ITestOutputHelper` type has moved from the `Xunit.Abstractions` namespace to `Xunit`. 182 | 1. Change any package references from `MartinCostello.Logging.XUnit` to `MartinCostello.Logging.XUnit.v3`. 183 | 184 | ```diff 185 | - 186 | + 187 | ``` 188 | 189 | ## Feedback 190 | 191 | Any feedback or issues can be added to the issues for this project in [GitHub][issues]. 192 | 193 | ## Repository 194 | 195 | The repository is hosted in [GitHub][repo]: 196 | 197 | ## License 198 | 199 | This project is licensed under the [Apache 2.0][license] license. 200 | 201 | ## Building and Testing 202 | 203 | Compiling the solution yourself requires Git and the [.NET SDK][dotnet-sdk] to be installed (version `9.0.100` or later). 204 | 205 | To build and test the solution locally from a terminal/command-line, run the following set of commands: 206 | 207 | ```powershell 208 | git clone https://github.com/martincostello/xunit-logging.git 209 | cd xunit-logging 210 | ./build.ps1 211 | ``` 212 | 213 | [build-badge]: https://github.com/martincostello/xunit-logging/actions/workflows/build.yml/badge.svg?branch=main&event=push 214 | [build-status]: https://github.com/martincostello/xunit-logging/actions?query=workflow%3Abuild+branch%3Amain+event%3Apush "Continuous Integration for this project" 215 | [coverage-badge]: https://codecov.io/gh/martincostello/xunit-logging/branch/main/graph/badge.svg 216 | [coverage-report]: https://codecov.io/gh/martincostello/xunit-logging "Code coverage report for this project" 217 | [scorecard-badge]: https://api.securityscorecards.dev/projects/github.com/martincostello/xunit-logging/badge 218 | [scorecard-report]: https://securityscorecards.dev/viewer/?uri=github.com/martincostello/xunit-logging "OpenSSF Scorecard for this project" 219 | [dotnet-sdk]: https://dot.net/download "Download the .NET SDK" 220 | [example-integration-tests]: https://github.com/martincostello/xunit-logging/blob/main/tests/Shared/Integration/HttpApplicationTests.cs "Integration test examples" 221 | [example-test]: https://github.com/martincostello/xunit-logging/blob/8951660a4667cfa5b07e2558b741b335a31d1e59/tests/Shared/Integration/HttpApplicationTests.cs#L26-L58 "Http_Get_Many" 222 | [example-unit-tests]: https://github.com/martincostello/xunit-logging/blob/main/tests/Shared/Examples.cs "Unit test examples" 223 | [issues]: https://github.com/martincostello/xunit-logging/issues "Issues for this project on GitHub.com" 224 | [license]: https://www.apache.org/licenses/LICENSE-2.0.txt "The Apache 2.0 license" 225 | [output-vs]: ./docs/images/output-vs.png "xunit Output in Visual Studio" 226 | [output-vscode]: ./docs/images/output-vscode.png "xunit Output in Visual Studio Code" 227 | [output-windows-terminal]: ./docs/images/output-terminal.png "xunit Output in Windows Terminal with the .NET CLI" 228 | [package-badge-downloads-v2]: https://img.shields.io/nuget/dt/MartinCostello.Logging.XUnit?logo=nuget&label=Downloads&color=blue 229 | [package-badge-downloads-v3]: https://img.shields.io/nuget/dt/MartinCostello.Logging.XUnit.v3?logo=nuget&label=Downloads&color=blue 230 | [package-badge-version-v2]: https://img.shields.io/nuget/v/MartinCostello.Logging.XUnit?logo=nuget&label=Latest&color=blue 231 | [package-badge-version-v3]: https://img.shields.io/nuget/v/MartinCostello.Logging.XUnit.v3?logo=nuget&label=Latest&color=blue 232 | [package-download-v2]: https://www.nuget.org/packages/MartinCostello.Logging.XUnit "Download MartinCostello.Logging.XUnit from NuGet" 233 | [package-download-v3]: https://www.nuget.org/packages/MartinCostello.Logging.XUnit.v3 "Download MartinCostello.Logging.XUnit.v3 from NuGet" 234 | [repo]: https://github.com/martincostello/xunit-logging "This project on GitHub.com" 235 | [serilog]: https://serilog.net/ "Serilog website" 236 | [serilog-sinks-xunit]: https://github.com/trbenning/serilog-sinks-xunit "Serilog.Sinks.XUnit on GitHub" 237 | [xunit-output]: https://xunit.net/docs/capturing-output "Capturing Output - xUnit.net Documentation" 238 | [xunit-v3-migration]: https://xunit.net/docs/getting-started/v3/migration "Migrating from xunit v2 to v3" 239 | [xunit-v3-whats-new]: https://xunit.net/docs/getting-started/v3/whats-new "What's New in v3" 240 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | To privately report a security vulnerability, please create a security advisory in the [repository's Security tab](https://github.com/martincostello/xunit-logging/security/advisories). 6 | 7 | Further details can be found in the [GitHub documentation](https://docs.github.com/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability). 8 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env pwsh 2 | 3 | #Requires -PSEdition Core 4 | #Requires -Version 7 5 | 6 | param( 7 | [Parameter(Mandatory = $false)][switch] $SkipTests 8 | ) 9 | 10 | $ErrorActionPreference = "Stop" 11 | $InformationPreference = "Continue" 12 | $ProgressPreference = "SilentlyContinue" 13 | 14 | $solutionPath = $PSScriptRoot 15 | $sdkFile = Join-Path $solutionPath "global.json" 16 | 17 | $libraryProjects = @( 18 | (Join-Path $solutionPath "src" "Logging.XUnit" "MartinCostello.Logging.XUnit.csproj") 19 | (Join-Path $solutionPath "src" "Logging.XUnit.v3" "MartinCostello.Logging.XUnit.v3.csproj") 20 | ) 21 | 22 | $dotnetVersion = (Get-Content $sdkFile | Out-String | ConvertFrom-Json).sdk.version 23 | 24 | $installDotNetSdk = $false; 25 | 26 | if (($null -eq (Get-Command "dotnet" -ErrorAction SilentlyContinue)) -and ($null -eq (Get-Command "dotnet.exe" -ErrorAction SilentlyContinue))) { 27 | Write-Information "The .NET SDK is not installed." 28 | $installDotNetSdk = $true 29 | } 30 | else { 31 | Try { 32 | $installedDotNetVersion = (dotnet --version 2>&1 | Out-String).Trim() 33 | } 34 | Catch { 35 | $installedDotNetVersion = "?" 36 | } 37 | 38 | if ($installedDotNetVersion -ne $dotnetVersion) { 39 | Write-Information "The required version of the .NET SDK is not installed. Expected $dotnetVersion." 40 | $installDotNetSdk = $true 41 | } 42 | } 43 | 44 | if ($installDotNetSdk) { 45 | 46 | ${env:DOTNET_INSTALL_DIR} = Join-Path $solutionPath ".dotnet" 47 | $sdkPath = Join-Path ${env:DOTNET_INSTALL_DIR} "sdk" $dotnetVersion 48 | 49 | if (!(Test-Path $sdkPath)) { 50 | if (!(Test-Path ${env:DOTNET_INSTALL_DIR})) { 51 | mkdir ${env:DOTNET_INSTALL_DIR} | Out-Null 52 | } 53 | [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor "Tls12" 54 | if (($PSVersionTable.PSVersion.Major -ge 6) -And !$IsWindows) { 55 | $installScript = Join-Path ${env:DOTNET_INSTALL_DIR} "install.sh" 56 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.sh" -OutFile $installScript -UseBasicParsing 57 | chmod +x $installScript 58 | & $installScript --jsonfile $sdkFile --install-dir ${env:DOTNET_INSTALL_DIR} --no-path --skip-non-versioned-files 59 | } 60 | else { 61 | $installScript = Join-Path ${env:DOTNET_INSTALL_DIR} "install.ps1" 62 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile $installScript -UseBasicParsing 63 | & $installScript -JsonFile $sdkFile -InstallDir ${env:DOTNET_INSTALL_DIR} -NoPath -SkipNonVersionedFiles 64 | } 65 | } 66 | } 67 | else { 68 | ${env:DOTNET_INSTALL_DIR} = Split-Path -Path (Get-Command dotnet).Path 69 | } 70 | 71 | $dotnet = Join-Path ${env:DOTNET_INSTALL_DIR} "dotnet" 72 | 73 | if ($installDotNetSdk) { 74 | ${env:PATH} = "${env:DOTNET_INSTALL_DIR};${env:PATH}" 75 | } 76 | 77 | function DotNetPack { 78 | param([string]$Project) 79 | 80 | & $dotnet pack $Project --include-symbols --include-source 81 | 82 | if ($LASTEXITCODE -ne 0) { 83 | throw "dotnet pack failed with exit code $LASTEXITCODE" 84 | } 85 | } 86 | 87 | function DotNetTest { 88 | param() 89 | 90 | $additionalArgs = @() 91 | 92 | if (-Not [string]::IsNullOrEmpty($env:GITHUB_SHA)) { 93 | $additionalArgs += "--logger:GitHubActions;report-warnings=false" 94 | $additionalArgs += "--logger:junit;LogFilePath=junit.xml" 95 | } 96 | 97 | & $dotnet test --configuration "Release" $additionalArgs 98 | 99 | if ($LASTEXITCODE -ne 0) { 100 | throw "dotnet test failed with exit code $LASTEXITCODE" 101 | } 102 | } 103 | 104 | Write-Information "Packaging libraries..." 105 | ForEach ($libraryProject in $libraryProjects) { 106 | DotNetPack $libraryProject 107 | } 108 | 109 | if (-Not $SkipTests) { 110 | Write-Information "Running tests..." 111 | DotNetTest 112 | } 113 | -------------------------------------------------------------------------------- /docs/images/output-terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martincostello/xunit-logging/36464418f76d01b76e7550818529ba46a868a715/docs/images/output-terminal.png -------------------------------------------------------------------------------- /docs/images/output-vs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martincostello/xunit-logging/36464418f76d01b76e7550818529ba46a868a715/docs/images/output-vs.png -------------------------------------------------------------------------------- /docs/images/output-vscode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martincostello/xunit-logging/36464418f76d01b76e7550818529ba46a868a715/docs/images/output-vscode.png -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.300", 4 | "allowPrerelease": false, 5 | "rollForward": "latestMajor", 6 | "paths": [ ".dotnet", "$host$" ], 7 | "errorMessage": "The required version of the .NET SDK could not be found. Please run ./build.ps1 to bootstrap the .NET SDK." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martincostello/xunit-logging/36464418f76d01b76e7550818529ba46a868a715/package-icon.png -------------------------------------------------------------------------------- /package-readme.md: -------------------------------------------------------------------------------- 1 | # xunit Logging 2 | 3 | ## Introduction 4 | 5 | `MartinCostello.Logging.XUnit` and `MartinCostello.Logging.XUnit.v3` provide extensions to hook 6 | into the `ILogger` infrastructure to output logs from your xunit tests to the test output. 7 | 8 | ### Usage 9 | 10 | ```csharp 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Logging; 13 | using Xunit; 14 | using Xunit.Abstractions; // For xunit v2 - not required for xunit v3 15 | 16 | namespace MyApp.Calculator; 17 | 18 | public class CalculatorTests(ITestOutputHelper outputHelper) 19 | { 20 | [Fact] 21 | public void Calculator_Sums_Two_Integers() 22 | { 23 | // Arrange 24 | using var serviceProvider = new ServiceCollection() 25 | .AddLogging((builder) => builder.AddXUnit(outputHelper)) 26 | .AddSingleton() 27 | .BuildServiceProvider(); 28 | 29 | var calculator = services.GetRequiredService(); 30 | 31 | // Act 32 | int actual = calculator.Sum(1, 2); 33 | 34 | // Assert 35 | Assert.AreEqual(3, actual); 36 | } 37 | } 38 | 39 | public sealed class Calculator(ILogger logger) 40 | { 41 | public int Sum(int x, int y) 42 | { 43 | int sum = x + y; 44 | 45 | logger.LogInformation("The sum of {x} and {y} is {sum}.", x, y, sum); 46 | 47 | return sum; 48 | } 49 | } 50 | ``` 51 | 52 | ## Feedback 53 | 54 | Any feedback or issues can be added to the issues for this project in [GitHub][issues]. 55 | 56 | ## License 57 | 58 | This project is licensed under the [Apache 2.0][license] license. 59 | 60 | [issues]: https://github.com/martincostello/xunit-logging/issues "Issues for this package on GitHub.com" 61 | [license]: https://www.apache.org/licenses/LICENSE-2.0.txt "The Apache 2.0 license" 62 | -------------------------------------------------------------------------------- /src/Logging.XUnit.v3/MartinCostello.Logging.XUnit.v3.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Logging Extensions for xunit v3 4 | $(DefineConstants);XUNIT_V3 5 | Extensions for Microsoft.Extensions.Logging for xunit v3. 6 | true 7 | true 8 | true 9 | Library 10 | MartinCostello.Logging.XUnit.v3 11 | MartinCostello.Logging.XUnit 12 | net8.0;net472 13 | xunit v3 Logging Extensions 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <_Parameter1>ed8d1c5e-3ee7-45fe-8d1d-94257a71f02a 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Logging.XUnit.v3/PublicAPI/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | MartinCostello.Logging.XUnit.IMessageSinkAccessor 3 | MartinCostello.Logging.XUnit.IMessageSinkAccessor.MessageSink.get -> Xunit.Sdk.IMessageSink? 4 | MartinCostello.Logging.XUnit.IMessageSinkAccessor.MessageSink.set -> void 5 | MartinCostello.Logging.XUnit.ITestOutputHelperAccessor 6 | MartinCostello.Logging.XUnit.ITestOutputHelperAccessor.OutputHelper.get -> Xunit.ITestOutputHelper? 7 | MartinCostello.Logging.XUnit.ITestOutputHelperAccessor.OutputHelper.set -> void 8 | MartinCostello.Logging.XUnit.XUnitLogger 9 | MartinCostello.Logging.XUnit.XUnitLogger.BeginScope(TState state) -> System.IDisposable? 10 | MartinCostello.Logging.XUnit.XUnitLogger.Filter.get -> System.Func! 11 | MartinCostello.Logging.XUnit.XUnitLogger.Filter.set -> void 12 | MartinCostello.Logging.XUnit.XUnitLogger.IncludeScopes.get -> bool 13 | MartinCostello.Logging.XUnit.XUnitLogger.IncludeScopes.set -> void 14 | MartinCostello.Logging.XUnit.XUnitLogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) -> bool 15 | MartinCostello.Logging.XUnit.XUnitLogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func! formatter) -> void 16 | MartinCostello.Logging.XUnit.XUnitLogger.MessageSinkMessageFactory.get -> System.Func! 17 | MartinCostello.Logging.XUnit.XUnitLogger.MessageSinkMessageFactory.set -> void 18 | MartinCostello.Logging.XUnit.XUnitLogger.Name.get -> string! 19 | MartinCostello.Logging.XUnit.XUnitLogger.XUnitLogger(string! name, MartinCostello.Logging.XUnit.IMessageSinkAccessor! accessor, MartinCostello.Logging.XUnit.XUnitLoggerOptions? options) -> void 20 | MartinCostello.Logging.XUnit.XUnitLogger.XUnitLogger(string! name, MartinCostello.Logging.XUnit.ITestOutputHelperAccessor! accessor, MartinCostello.Logging.XUnit.XUnitLoggerOptions? options) -> void 21 | MartinCostello.Logging.XUnit.XUnitLogger.XUnitLogger(string! name, Xunit.ITestOutputHelper! outputHelper, MartinCostello.Logging.XUnit.XUnitLoggerOptions? options) -> void 22 | MartinCostello.Logging.XUnit.XUnitLogger.XUnitLogger(string! name, Xunit.Sdk.IMessageSink! messageSink, MartinCostello.Logging.XUnit.XUnitLoggerOptions? options) -> void 23 | MartinCostello.Logging.XUnit.XUnitLoggerOptions 24 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.Filter.get -> System.Func! 25 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.Filter.set -> void 26 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.IncludeScopes.get -> bool 27 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.IncludeScopes.set -> void 28 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.MessageSinkMessageFactory.get -> System.Func! 29 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.MessageSinkMessageFactory.set -> void 30 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimestampFormat.get -> string? 31 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimestampFormat.set -> void 32 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.XUnitLoggerOptions() -> void 33 | MartinCostello.Logging.XUnit.XUnitLoggerProvider 34 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.Dispose() -> void 35 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.XUnitLoggerProvider(MartinCostello.Logging.XUnit.IMessageSinkAccessor! accessor, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> void 36 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.XUnitLoggerProvider(MartinCostello.Logging.XUnit.ITestOutputHelperAccessor! accessor, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> void 37 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.XUnitLoggerProvider(Xunit.ITestOutputHelper! outputHelper, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> void 38 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.XUnitLoggerProvider(Xunit.Sdk.IMessageSink! messageSink, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> void 39 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.~XUnitLoggerProvider() -> void 40 | Microsoft.Extensions.Logging.XUnitLoggerExtensions 41 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.ITestOutputHelper! outputHelper) -> Microsoft.Extensions.Logging.ILoggerFactory! 42 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.ITestOutputHelper! outputHelper, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> Microsoft.Extensions.Logging.ILoggerFactory! 43 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.ITestOutputHelper! outputHelper, Microsoft.Extensions.Logging.LogLevel minLevel) -> Microsoft.Extensions.Logging.ILoggerFactory! 44 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.ITestOutputHelper! outputHelper, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggerFactory! 45 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.ITestOutputHelper! outputHelper, System.Func! configure) -> Microsoft.Extensions.Logging.ILoggerFactory! 46 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.ITestOutputHelper! outputHelper, System.Func! filter) -> Microsoft.Extensions.Logging.ILoggerFactory! 47 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Sdk.IMessageSink! messageSink) -> Microsoft.Extensions.Logging.ILoggerFactory! 48 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Sdk.IMessageSink! messageSink, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> Microsoft.Extensions.Logging.ILoggerFactory! 49 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Sdk.IMessageSink! messageSink, Microsoft.Extensions.Logging.LogLevel minLevel) -> Microsoft.Extensions.Logging.ILoggerFactory! 50 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Sdk.IMessageSink! messageSink, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggerFactory! 51 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Sdk.IMessageSink! messageSink, System.Func! configure) -> Microsoft.Extensions.Logging.ILoggerFactory! 52 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Sdk.IMessageSink! messageSink, System.Func! filter) -> Microsoft.Extensions.Logging.ILoggerFactory! 53 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! 54 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, MartinCostello.Logging.XUnit.IMessageSinkAccessor! accessor) -> Microsoft.Extensions.Logging.ILoggingBuilder! 55 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, MartinCostello.Logging.XUnit.IMessageSinkAccessor! accessor, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! 56 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, MartinCostello.Logging.XUnit.ITestOutputHelperAccessor! accessor) -> Microsoft.Extensions.Logging.ILoggingBuilder! 57 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, MartinCostello.Logging.XUnit.ITestOutputHelperAccessor! accessor, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! 58 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, Xunit.ITestOutputHelper! outputHelper) -> Microsoft.Extensions.Logging.ILoggingBuilder! 59 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, Xunit.ITestOutputHelper! outputHelper, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! 60 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, Xunit.Sdk.IMessageSink! messageSink) -> Microsoft.Extensions.Logging.ILoggingBuilder! 61 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, Xunit.Sdk.IMessageSink! messageSink, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! 62 | static Xunit.IMessageSinkExtensions.ToLogger(this Xunit.Sdk.IMessageSink! messageSink) -> Microsoft.Extensions.Logging.ILogger! 63 | static Xunit.IMessageSinkExtensions.ToLoggerFactory(this Xunit.Sdk.IMessageSink! messageSink) -> Microsoft.Extensions.Logging.ILoggerFactory! 64 | static Xunit.ITestOutputHelperExtensions.ToLogger(this Xunit.ITestOutputHelper! outputHelper) -> Microsoft.Extensions.Logging.ILogger! 65 | static Xunit.ITestOutputHelperExtensions.ToLoggerFactory(this Xunit.ITestOutputHelper! outputHelper) -> Microsoft.Extensions.Logging.ILoggerFactory! 66 | virtual MartinCostello.Logging.XUnit.XUnitLogger.WriteMessage(Microsoft.Extensions.Logging.LogLevel logLevel, int eventId, string? message, System.Exception? exception) -> void 67 | virtual MartinCostello.Logging.XUnit.XUnitLoggerProvider.CreateLogger(string! categoryName) -> Microsoft.Extensions.Logging.ILogger! 68 | virtual MartinCostello.Logging.XUnit.XUnitLoggerProvider.Dispose(bool disposing) -> void 69 | Xunit.IMessageSinkExtensions 70 | Xunit.ITestOutputHelperExtensions 71 | -------------------------------------------------------------------------------- /src/Logging.XUnit.v3/PublicAPI/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | -------------------------------------------------------------------------------- /src/Logging.XUnit.v3/PublicAPI/net10.0/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimeProvider.get -> System.TimeProvider! 3 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimeProvider.set -> void 4 | -------------------------------------------------------------------------------- /src/Logging.XUnit.v3/PublicAPI/net10.0/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | -------------------------------------------------------------------------------- /src/Logging.XUnit.v3/PublicAPI/net8.0/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimeProvider.get -> System.TimeProvider! 3 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimeProvider.set -> void 4 | -------------------------------------------------------------------------------- /src/Logging.XUnit.v3/PublicAPI/net8.0/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | -------------------------------------------------------------------------------- /src/Logging.XUnit/MartinCostello.Logging.XUnit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Logging Extensions for xunit 4 | Extensions for Microsoft.Extensions.Logging for xunit. 5 | true 6 | true 7 | true 8 | Library 9 | MartinCostello.Logging.XUnit 10 | MartinCostello.Logging.XUnit 11 | netstandard2.0;net8.0 12 | xunit Logging Extensions 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | <_Parameter1>af808007-f06a-410b-886d-152b3f39c43f 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Logging.XUnit/PublicAPI/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | MartinCostello.Logging.XUnit.IMessageSinkAccessor 3 | MartinCostello.Logging.XUnit.IMessageSinkAccessor.MessageSink.get -> Xunit.Abstractions.IMessageSink? 4 | MartinCostello.Logging.XUnit.IMessageSinkAccessor.MessageSink.set -> void 5 | MartinCostello.Logging.XUnit.ITestOutputHelperAccessor 6 | MartinCostello.Logging.XUnit.ITestOutputHelperAccessor.OutputHelper.get -> Xunit.Abstractions.ITestOutputHelper? 7 | MartinCostello.Logging.XUnit.ITestOutputHelperAccessor.OutputHelper.set -> void 8 | MartinCostello.Logging.XUnit.XUnitLogger 9 | MartinCostello.Logging.XUnit.XUnitLogger.BeginScope(TState state) -> System.IDisposable? 10 | MartinCostello.Logging.XUnit.XUnitLogger.Filter.get -> System.Func! 11 | MartinCostello.Logging.XUnit.XUnitLogger.Filter.set -> void 12 | MartinCostello.Logging.XUnit.XUnitLogger.IncludeScopes.get -> bool 13 | MartinCostello.Logging.XUnit.XUnitLogger.IncludeScopes.set -> void 14 | MartinCostello.Logging.XUnit.XUnitLogger.IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) -> bool 15 | MartinCostello.Logging.XUnit.XUnitLogger.Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func! formatter) -> void 16 | MartinCostello.Logging.XUnit.XUnitLogger.MessageSinkMessageFactory.get -> System.Func! 17 | MartinCostello.Logging.XUnit.XUnitLogger.MessageSinkMessageFactory.set -> void 18 | MartinCostello.Logging.XUnit.XUnitLogger.Name.get -> string! 19 | MartinCostello.Logging.XUnit.XUnitLogger.XUnitLogger(string! name, MartinCostello.Logging.XUnit.IMessageSinkAccessor! accessor, MartinCostello.Logging.XUnit.XUnitLoggerOptions? options) -> void 20 | MartinCostello.Logging.XUnit.XUnitLogger.XUnitLogger(string! name, MartinCostello.Logging.XUnit.ITestOutputHelperAccessor! accessor, MartinCostello.Logging.XUnit.XUnitLoggerOptions? options) -> void 21 | MartinCostello.Logging.XUnit.XUnitLogger.XUnitLogger(string! name, Xunit.Abstractions.IMessageSink! messageSink, MartinCostello.Logging.XUnit.XUnitLoggerOptions? options) -> void 22 | MartinCostello.Logging.XUnit.XUnitLogger.XUnitLogger(string! name, Xunit.Abstractions.ITestOutputHelper! outputHelper, MartinCostello.Logging.XUnit.XUnitLoggerOptions? options) -> void 23 | MartinCostello.Logging.XUnit.XUnitLoggerOptions 24 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.Filter.get -> System.Func! 25 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.Filter.set -> void 26 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.IncludeScopes.get -> bool 27 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.IncludeScopes.set -> void 28 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.MessageSinkMessageFactory.get -> System.Func! 29 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.MessageSinkMessageFactory.set -> void 30 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimestampFormat.get -> string? 31 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimestampFormat.set -> void 32 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.XUnitLoggerOptions() -> void 33 | MartinCostello.Logging.XUnit.XUnitLoggerProvider 34 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.Dispose() -> void 35 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.XUnitLoggerProvider(MartinCostello.Logging.XUnit.IMessageSinkAccessor! accessor, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> void 36 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.XUnitLoggerProvider(MartinCostello.Logging.XUnit.ITestOutputHelperAccessor! accessor, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> void 37 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.XUnitLoggerProvider(Xunit.Abstractions.IMessageSink! messageSink, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> void 38 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.XUnitLoggerProvider(Xunit.Abstractions.ITestOutputHelper! outputHelper, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> void 39 | MartinCostello.Logging.XUnit.XUnitLoggerProvider.~XUnitLoggerProvider() -> void 40 | Microsoft.Extensions.Logging.XUnitLoggerExtensions 41 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.IMessageSink! messageSink) -> Microsoft.Extensions.Logging.ILoggerFactory! 42 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.IMessageSink! messageSink, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> Microsoft.Extensions.Logging.ILoggerFactory! 43 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.IMessageSink! messageSink, Microsoft.Extensions.Logging.LogLevel minLevel) -> Microsoft.Extensions.Logging.ILoggerFactory! 44 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.IMessageSink! messageSink, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggerFactory! 45 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.IMessageSink! messageSink, System.Func! configure) -> Microsoft.Extensions.Logging.ILoggerFactory! 46 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.IMessageSink! messageSink, System.Func! filter) -> Microsoft.Extensions.Logging.ILoggerFactory! 47 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.ITestOutputHelper! outputHelper) -> Microsoft.Extensions.Logging.ILoggerFactory! 48 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.ITestOutputHelper! outputHelper, MartinCostello.Logging.XUnit.XUnitLoggerOptions! options) -> Microsoft.Extensions.Logging.ILoggerFactory! 49 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.ITestOutputHelper! outputHelper, Microsoft.Extensions.Logging.LogLevel minLevel) -> Microsoft.Extensions.Logging.ILoggerFactory! 50 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.ITestOutputHelper! outputHelper, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggerFactory! 51 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.ITestOutputHelper! outputHelper, System.Func! configure) -> Microsoft.Extensions.Logging.ILoggerFactory! 52 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggerFactory! factory, Xunit.Abstractions.ITestOutputHelper! outputHelper, System.Func! filter) -> Microsoft.Extensions.Logging.ILoggerFactory! 53 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder) -> Microsoft.Extensions.Logging.ILoggingBuilder! 54 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, MartinCostello.Logging.XUnit.IMessageSinkAccessor! accessor) -> Microsoft.Extensions.Logging.ILoggingBuilder! 55 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, MartinCostello.Logging.XUnit.IMessageSinkAccessor! accessor, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! 56 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, MartinCostello.Logging.XUnit.ITestOutputHelperAccessor! accessor) -> Microsoft.Extensions.Logging.ILoggingBuilder! 57 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, MartinCostello.Logging.XUnit.ITestOutputHelperAccessor! accessor, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! 58 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, Xunit.Abstractions.IMessageSink! messageSink) -> Microsoft.Extensions.Logging.ILoggingBuilder! 59 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, Xunit.Abstractions.IMessageSink! messageSink, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! 60 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, Xunit.Abstractions.ITestOutputHelper! outputHelper) -> Microsoft.Extensions.Logging.ILoggingBuilder! 61 | static Microsoft.Extensions.Logging.XUnitLoggerExtensions.AddXUnit(this Microsoft.Extensions.Logging.ILoggingBuilder! builder, Xunit.Abstractions.ITestOutputHelper! outputHelper, System.Action! configure) -> Microsoft.Extensions.Logging.ILoggingBuilder! 62 | static Xunit.Abstractions.IMessageSinkExtensions.ToLogger(this Xunit.Abstractions.IMessageSink! messageSink) -> Microsoft.Extensions.Logging.ILogger! 63 | static Xunit.Abstractions.IMessageSinkExtensions.ToLoggerFactory(this Xunit.Abstractions.IMessageSink! messageSink) -> Microsoft.Extensions.Logging.ILoggerFactory! 64 | static Xunit.Abstractions.ITestOutputHelperExtensions.ToLogger(this Xunit.Abstractions.ITestOutputHelper! outputHelper) -> Microsoft.Extensions.Logging.ILogger! 65 | static Xunit.Abstractions.ITestOutputHelperExtensions.ToLoggerFactory(this Xunit.Abstractions.ITestOutputHelper! outputHelper) -> Microsoft.Extensions.Logging.ILoggerFactory! 66 | virtual MartinCostello.Logging.XUnit.XUnitLogger.WriteMessage(Microsoft.Extensions.Logging.LogLevel logLevel, int eventId, string? message, System.Exception? exception) -> void 67 | virtual MartinCostello.Logging.XUnit.XUnitLoggerProvider.CreateLogger(string! categoryName) -> Microsoft.Extensions.Logging.ILogger! 68 | virtual MartinCostello.Logging.XUnit.XUnitLoggerProvider.Dispose(bool disposing) -> void 69 | Xunit.Abstractions.IMessageSinkExtensions 70 | Xunit.Abstractions.ITestOutputHelperExtensions 71 | -------------------------------------------------------------------------------- /src/Logging.XUnit/PublicAPI/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | -------------------------------------------------------------------------------- /src/Logging.XUnit/PublicAPI/net10.0/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimeProvider.get -> System.TimeProvider! 3 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimeProvider.set -> void 4 | -------------------------------------------------------------------------------- /src/Logging.XUnit/PublicAPI/net10.0/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | -------------------------------------------------------------------------------- /src/Logging.XUnit/PublicAPI/net8.0/PublicAPI.Shipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimeProvider.get -> System.TimeProvider! 3 | MartinCostello.Logging.XUnit.XUnitLoggerOptions.TimeProvider.set -> void 4 | -------------------------------------------------------------------------------- /src/Logging.XUnit/PublicAPI/net8.0/PublicAPI.Unshipped.txt: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | -------------------------------------------------------------------------------- /src/Shared/AmbientTestOutputHelperAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit; 5 | 6 | /// 7 | /// A class representing an implementation of that 8 | /// stores the as an asynchronous local value. This class cannot be inherited. 9 | /// 10 | internal sealed class AmbientTestOutputHelperAccessor : ITestOutputHelperAccessor 11 | { 12 | /// 13 | /// A backing field for the for the current thread. 14 | /// 15 | private static readonly AsyncLocal _current = new(); 16 | 17 | /// 18 | /// Gets or sets the current . 19 | /// 20 | public ITestOutputHelper? OutputHelper 21 | { 22 | get { return _current.Value; } 23 | set { _current.Value = value; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Shared/IMessageSinkAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit; 5 | 6 | /// 7 | /// Defines a property for accessing an . 8 | /// 9 | public interface IMessageSinkAccessor 10 | { 11 | /// 12 | /// Gets or sets the to use. 13 | /// 14 | IMessageSink? MessageSink { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Shared/IMessageSinkExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using System.ComponentModel; 5 | using Microsoft.Extensions.Logging; 6 | 7 | #if XUNIT_V3 8 | namespace Xunit; 9 | #else 10 | #pragma warning disable IDE0130 11 | namespace Xunit.Abstractions; 12 | #endif 13 | 14 | /// 15 | /// A class containing extension methods for the interface. This class cannot be inherited. 16 | /// 17 | [EditorBrowsable(EditorBrowsableState.Never)] 18 | public static class IMessageSinkExtensions 19 | { 20 | /// 21 | /// Returns an that logs to the message sink. 22 | /// 23 | /// The to create the logger factory from. 24 | /// 25 | /// An that writes messages to the message sink. 26 | /// 27 | /// 28 | /// is . 29 | /// 30 | public static ILoggerFactory ToLoggerFactory(this IMessageSink messageSink) 31 | { 32 | #if NET 33 | ArgumentNullException.ThrowIfNull(messageSink); 34 | #else 35 | if (messageSink == null) 36 | { 37 | throw new ArgumentNullException(nameof(messageSink)); 38 | } 39 | #endif 40 | 41 | return new LoggerFactory().AddXUnit(messageSink); 42 | } 43 | 44 | /// 45 | /// Returns an that logs to the message sink. 46 | /// 47 | /// The type of the logger to create. 48 | /// The to create the logger from. 49 | /// 50 | /// An that writes messages to the message sink. 51 | /// 52 | /// 53 | /// is . 54 | /// 55 | public static ILogger ToLogger(this IMessageSink messageSink) 56 | => messageSink.ToLoggerFactory().CreateLogger(); 57 | } 58 | -------------------------------------------------------------------------------- /src/Shared/ITestOutputHelperAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit; 5 | 6 | /// 7 | /// Defines a property for accessing an . 8 | /// 9 | public interface ITestOutputHelperAccessor 10 | { 11 | /// 12 | /// Gets or sets the to use. 13 | /// 14 | ITestOutputHelper? OutputHelper { get; set; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Shared/ITestOutputHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using System.ComponentModel; 5 | using Microsoft.Extensions.Logging; 6 | 7 | #if XUNIT_V3 8 | namespace Xunit; 9 | #else 10 | #pragma warning disable IDE0130 11 | namespace Xunit.Abstractions; 12 | #endif 13 | 14 | /// 15 | /// A class containing extension methods for the interface. This class cannot be inherited. 16 | /// 17 | [EditorBrowsable(EditorBrowsableState.Never)] 18 | public static class ITestOutputHelperExtensions 19 | { 20 | /// 21 | /// Returns an that logs to the output helper. 22 | /// 23 | /// The to create the logger factory from. 24 | /// 25 | /// An that writes messages to the test output helper. 26 | /// 27 | /// 28 | /// is . 29 | /// 30 | public static ILoggerFactory ToLoggerFactory(this ITestOutputHelper outputHelper) 31 | { 32 | #if NET 33 | ArgumentNullException.ThrowIfNull(outputHelper); 34 | #else 35 | if (outputHelper == null) 36 | { 37 | throw new ArgumentNullException(nameof(outputHelper)); 38 | } 39 | #endif 40 | 41 | return new LoggerFactory().AddXUnit(outputHelper); 42 | } 43 | 44 | /// 45 | /// Returns an that logs to the output helper. 46 | /// 47 | /// The type of the logger to create. 48 | /// The to create the logger from. 49 | /// 50 | /// An that writes messages to the test output helper. 51 | /// 52 | /// 53 | /// is . 54 | /// 55 | public static ILogger ToLogger(this ITestOutputHelper outputHelper) 56 | => outputHelper.ToLoggerFactory().CreateLogger(); 57 | } 58 | -------------------------------------------------------------------------------- /src/Shared/MessageSinkAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit; 5 | 6 | /// 7 | /// A class representing the default implementation of . This class cannot be inherited. 8 | /// 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The to use. 13 | /// 14 | /// is . 15 | /// 16 | internal sealed class MessageSinkAccessor(IMessageSink messageSink) : IMessageSinkAccessor 17 | { 18 | /// 19 | /// Gets or sets the current . 20 | /// 21 | public IMessageSink? MessageSink { get; set; } = messageSink ?? throw new ArgumentNullException(nameof(messageSink)); 22 | } 23 | -------------------------------------------------------------------------------- /src/Shared/StringSyntaxAttribute.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | //// Based on https://github.com/dotnet/runtime/blob/65067052e433eda400c5e7cc9f7b21c84640f901/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/StringSyntaxAttribute.cs 5 | 6 | #if NETSTANDARD || !NETCOREAPP 7 | 8 | #pragma warning disable IDE0130 9 | #pragma warning disable SA1600 10 | 11 | namespace System.Diagnostics.CodeAnalysis; 12 | 13 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] 14 | [ExcludeFromCodeCoverage] 15 | internal sealed class StringSyntaxAttribute(string syntax, params object?[] arguments) : Attribute 16 | { 17 | public const string DateTimeFormat = nameof(DateTimeFormat); 18 | 19 | public StringSyntaxAttribute(string syntax) 20 | : this(syntax, []) 21 | { 22 | } 23 | 24 | public string Syntax { get; } = syntax; 25 | 26 | public object?[] Arguments { get; } = arguments; 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /src/Shared/TestOutputHelperAccessor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit; 5 | 6 | /// 7 | /// A class representing the default implementation of . This class cannot be inherited. 8 | /// 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The to use. 13 | /// 14 | /// is . 15 | /// 16 | internal sealed class TestOutputHelperAccessor(ITestOutputHelper outputHelper) : ITestOutputHelperAccessor 17 | { 18 | /// 19 | /// Gets or sets the current . 20 | /// 21 | public ITestOutputHelper? OutputHelper { get; set; } = outputHelper ?? throw new ArgumentNullException(nameof(outputHelper)); 22 | } 23 | -------------------------------------------------------------------------------- /src/Shared/XUnitLogScope.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit; 5 | 6 | /// 7 | /// A class representing a scope for logging. This class cannot be inherited. 8 | /// 9 | /// 10 | /// Initializes a new instance of the class. 11 | /// 12 | /// The state object for the scope. 13 | internal sealed class XUnitLogScope(object state) 14 | { 15 | /// 16 | /// The scope for the current thread. 17 | /// 18 | private static readonly AsyncLocal _value = new(); 19 | 20 | /// 21 | /// Gets the state object for the scope. 22 | /// 23 | public object State { get; } = state; 24 | 25 | /// 26 | /// Gets or sets the current scope. 27 | /// 28 | internal static XUnitLogScope? Current 29 | { 30 | get { return _value.Value; } 31 | set { _value.Value = value; } 32 | } 33 | 34 | /// 35 | /// Gets the parent scope. 36 | /// 37 | internal XUnitLogScope? Parent { get; private set; } 38 | 39 | /// 40 | public override string? ToString() 41 | => State.ToString(); 42 | 43 | /// 44 | /// Pushes a new value into the scope. 45 | /// 46 | /// The state object for the scope. 47 | /// 48 | /// An that pops the scope. 49 | /// 50 | internal static IDisposable Push(object state) 51 | { 52 | var temp = Current; 53 | 54 | Current = new XUnitLogScope(state) 55 | { 56 | Parent = temp, 57 | }; 58 | 59 | return new DisposableScope(); 60 | } 61 | 62 | /// 63 | /// A class the disposes of the current scope. This class cannot be inherited. 64 | /// 65 | private sealed class DisposableScope : IDisposable 66 | { 67 | /// 68 | public void Dispose() 69 | { 70 | Current = Current?.Parent; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Shared/XUnitLogger.IMessageSink.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace MartinCostello.Logging.XUnit; 7 | 8 | /// 9 | /// A class representing an to use with xunit. 10 | /// 11 | public partial class XUnitLogger 12 | { 13 | /// 14 | /// The to use. This field is read-only. 15 | /// 16 | private readonly IMessageSinkAccessor? _messageSinkAccessor; 17 | 18 | /// 19 | /// Gets or sets the message sink message factory to use when writing to an . 20 | /// 21 | private Func _messageSinkMessageFactory; 22 | 23 | /// 24 | /// Initializes a new instance of the class. 25 | /// 26 | /// The name for messages produced by the logger. 27 | /// The to use. 28 | /// The to use. 29 | /// 30 | /// or is . 31 | /// 32 | public XUnitLogger(string name, IMessageSink messageSink, XUnitLoggerOptions? options) 33 | : this(name, new MessageSinkAccessor(messageSink), options) 34 | { 35 | } 36 | 37 | /// 38 | /// Initializes a new instance of the class. 39 | /// 40 | /// The name for messages produced by the logger. 41 | /// The to use. 42 | /// The to use. 43 | /// 44 | /// or is . 45 | /// 46 | public XUnitLogger(string name, IMessageSinkAccessor accessor, XUnitLoggerOptions? options) 47 | : this(name, options) 48 | { 49 | _messageSinkAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); 50 | } 51 | 52 | /// 53 | /// Gets or sets the message sink message factory to use when writing to an . 54 | /// 55 | /// 56 | /// is . 57 | /// 58 | public Func MessageSinkMessageFactory 59 | { 60 | get { return _messageSinkMessageFactory; } 61 | set { _messageSinkMessageFactory = value ?? throw new ArgumentNullException(nameof(value)); } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Shared/XUnitLogger.ITestOutputHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace MartinCostello.Logging.XUnit; 7 | 8 | /// 9 | /// A class representing an to use with xunit. 10 | /// 11 | public partial class XUnitLogger 12 | { 13 | /// 14 | /// The to use. This field is read-only. 15 | /// 16 | private readonly ITestOutputHelperAccessor? _outputHelperAccessor; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The name for messages produced by the logger. 22 | /// The to use. 23 | /// The to use. 24 | /// 25 | /// or is . 26 | /// 27 | public XUnitLogger(string name, ITestOutputHelper outputHelper, XUnitLoggerOptions? options) 28 | : this(name, new TestOutputHelperAccessor(outputHelper), options) 29 | { 30 | } 31 | 32 | /// 33 | /// Initializes a new instance of the class. 34 | /// 35 | /// The name for messages produced by the logger. 36 | /// The to use. 37 | /// The to use. 38 | /// 39 | /// or is . 40 | /// 41 | public XUnitLogger(string name, ITestOutputHelperAccessor accessor, XUnitLoggerOptions? options) 42 | : this(name, options) 43 | { 44 | _outputHelperAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Shared/XUnitLogger.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using System.Text; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace MartinCostello.Logging.XUnit; 8 | 9 | /// 10 | /// A class representing an to use with xunit. 11 | /// 12 | public partial class XUnitLogger : ILogger 13 | { 14 | //// Based on https://github.com/dotnet/runtime/blob/65067052e433eda400c5e7cc9f7b21c84640f901/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs#L41-L66 15 | 16 | /// 17 | /// The padding to use for log levels. 18 | /// 19 | private const string LogLevelPadding = ": "; 20 | 21 | /// 22 | /// The padding to use for messages. This field is read-only. 23 | /// 24 | private static readonly string MessagePadding = new(' ', GetLogLevelString(LogLevel.Debug).Length + LogLevelPadding.Length); 25 | 26 | /// 27 | /// The padding to use for new lines. This field is read-only. 28 | /// 29 | private static readonly string NewLineWithMessagePadding = Environment.NewLine + MessagePadding; 30 | 31 | /// 32 | /// The current builder to use to generate log messages. 33 | /// 34 | [ThreadStatic] 35 | private static StringBuilder? _logBuilder; 36 | 37 | /// 38 | /// The format string used to format the timestamp in log messages. 39 | /// 40 | private readonly string _timestampFormat; 41 | 42 | #if NET8_0_OR_GREATER 43 | 44 | /// 45 | /// The time provider used in log messages. 46 | /// 47 | private readonly TimeProvider _timeProvider; 48 | 49 | #endif 50 | 51 | /// 52 | /// Gets or sets the filter to use. 53 | /// 54 | private Func _filter; 55 | 56 | /// 57 | /// Initializes a new instance of the class. 58 | /// 59 | /// The name for messages produced by the logger. 60 | /// The to use. 61 | private XUnitLogger(string name, XUnitLoggerOptions? options) 62 | { 63 | Name = name ?? throw new ArgumentNullException(nameof(name)); 64 | 65 | _filter = options?.Filter ?? (static (_, _) => true); 66 | _messageSinkMessageFactory = options?.MessageSinkMessageFactory ?? (static (message) => new DiagnosticMessage(message)); 67 | _timestampFormat = options?.TimestampFormat ?? "u"; 68 | IncludeScopes = options?.IncludeScopes ?? false; 69 | #if NET8_0_OR_GREATER 70 | _timeProvider = options?.TimeProvider ?? TimeProvider.System; 71 | Clock = () => _timeProvider.GetLocalNow(); 72 | #else 73 | Clock = static () => DateTimeOffset.Now; 74 | #endif 75 | } 76 | 77 | /// 78 | /// Gets or sets the category filter to apply to logs. 79 | /// 80 | /// 81 | /// is . 82 | /// 83 | public Func Filter 84 | { 85 | get => _filter; 86 | set => _filter = value ?? throw new ArgumentNullException(nameof(value)); 87 | } 88 | 89 | /// 90 | /// Gets or sets a value indicating whether to include scopes. 91 | /// 92 | public bool IncludeScopes { get; set; } 93 | 94 | /// 95 | /// Gets the name of the logger. 96 | /// 97 | public string Name { get; } 98 | 99 | /// 100 | /// Gets or sets a delegate representing the system clock. 101 | /// 102 | internal Func Clock { get; set; } 103 | 104 | /// 105 | public IDisposable? BeginScope(TState state) 106 | where TState : notnull 107 | { 108 | #if NET 109 | ArgumentNullException.ThrowIfNull(state); 110 | #else 111 | if (state == null) 112 | { 113 | throw new ArgumentNullException(nameof(state)); 114 | } 115 | #endif 116 | 117 | return XUnitLogScope.Push(state); 118 | } 119 | 120 | /// 121 | public bool IsEnabled(LogLevel logLevel) 122 | { 123 | if (logLevel == LogLevel.None) 124 | { 125 | return false; 126 | } 127 | 128 | return Filter(Name, logLevel); 129 | } 130 | 131 | /// 132 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) 133 | { 134 | if (!IsEnabled(logLevel)) 135 | { 136 | return; 137 | } 138 | 139 | #if NET 140 | ArgumentNullException.ThrowIfNull(formatter); 141 | #else 142 | if (formatter == null) 143 | { 144 | throw new ArgumentNullException(nameof(formatter)); 145 | } 146 | #endif 147 | 148 | string? message = formatter(state, exception); 149 | 150 | if (!string.IsNullOrEmpty(message) || exception != null) 151 | { 152 | WriteMessage(logLevel, eventId.Id, message, exception); 153 | } 154 | } 155 | 156 | /// 157 | /// Writes a message to the or associated with the instance. 158 | /// 159 | /// The message to write will be written on this level. 160 | /// The Id of the event. 161 | /// The message to write. 162 | /// The exception related to this message. 163 | public virtual void WriteMessage(LogLevel logLevel, int eventId, string? message, Exception? exception) 164 | { 165 | ITestOutputHelper? outputHelper = _outputHelperAccessor?.OutputHelper; 166 | IMessageSink? messageSink = _messageSinkAccessor?.MessageSink; 167 | 168 | if (outputHelper is null && messageSink is null) 169 | { 170 | return; 171 | } 172 | 173 | StringBuilder? logBuilder = _logBuilder; 174 | _logBuilder = null; 175 | 176 | logBuilder ??= new StringBuilder(); 177 | 178 | string logLevelString = GetLogLevelString(logLevel); 179 | 180 | logBuilder.Append(LogLevelPadding); 181 | logBuilder.Append(Name); 182 | logBuilder.Append('['); 183 | logBuilder.Append(eventId); 184 | logBuilder.Append(']'); 185 | logBuilder.AppendLine(); 186 | 187 | if (IncludeScopes) 188 | { 189 | GetScopeInformation(logBuilder); 190 | } 191 | 192 | bool hasMessage = !string.IsNullOrEmpty(message); 193 | 194 | if (hasMessage) 195 | { 196 | logBuilder.Append(MessagePadding); 197 | 198 | int length = logBuilder.Length; 199 | logBuilder.Append(message); 200 | logBuilder.Replace(Environment.NewLine, NewLineWithMessagePadding, length, message!.Length); 201 | } 202 | 203 | if (exception != null) 204 | { 205 | if (hasMessage) 206 | { 207 | logBuilder.AppendLine(); 208 | } 209 | 210 | logBuilder.Append(exception); 211 | } 212 | 213 | // Prefix the formatted message so it renders like this: 214 | // [{timestamp}] {logLevelString}{message} 215 | logBuilder.Insert(0, logLevelString); 216 | logBuilder.Insert(0, "] "); 217 | logBuilder.Insert(0, Clock().ToString(_timestampFormat, CultureInfo.CurrentCulture)); 218 | logBuilder.Insert(0, '['); 219 | 220 | string line = logBuilder.ToString(); 221 | 222 | try 223 | { 224 | outputHelper?.WriteLine(line); 225 | 226 | if (messageSink != null) 227 | { 228 | var sinkMessage = _messageSinkMessageFactory(line); 229 | messageSink.OnMessage(sinkMessage); 230 | } 231 | } 232 | catch (InvalidOperationException) 233 | { 234 | // Ignore exception if the application tries to log after the test ends 235 | // but before the ITestOutputHelper is detached, e.g. "There is no currently active test." 236 | } 237 | 238 | logBuilder.Clear(); 239 | 240 | if (logBuilder.Capacity > 1024) 241 | { 242 | logBuilder.Capacity = 1024; 243 | } 244 | 245 | _logBuilder = logBuilder; 246 | } 247 | 248 | /// 249 | /// Returns the string to use for the specified logging level. 250 | /// 251 | /// The log level to get the representation for. 252 | /// 253 | /// A containing the text representation of . 254 | /// 255 | private static string GetLogLevelString(LogLevel logLevel) 256 | { 257 | return logLevel switch 258 | { 259 | LogLevel.Critical => "crit", 260 | LogLevel.Debug => "dbug", 261 | LogLevel.Error => "fail", 262 | LogLevel.Information => "info", 263 | LogLevel.Trace => "trce", 264 | LogLevel.Warning => "warn", 265 | _ => throw new ArgumentOutOfRangeException(nameof(logLevel)), 266 | }; 267 | } 268 | 269 | /// 270 | /// Gets the scope information for the current operation. 271 | /// 272 | /// The to write the scope to. 273 | private static void GetScopeInformation(StringBuilder builder) 274 | { 275 | var current = XUnitLogScope.Current; 276 | 277 | var stack = new Stack(); 278 | while (current != null) 279 | { 280 | stack.Push(current); 281 | current = current.Parent; 282 | } 283 | 284 | var depth = 0; 285 | static string DepthPadding(int depth) => new(' ', depth * 2); 286 | 287 | while (stack.Count > 0) 288 | { 289 | var elem = stack.Pop(); 290 | foreach (var property in StringifyScope(elem)) 291 | { 292 | builder.Append(MessagePadding) 293 | .Append(DepthPadding(depth)) 294 | .Append("=> ") 295 | .Append(property) 296 | .AppendLine(); 297 | } 298 | 299 | depth++; 300 | } 301 | } 302 | 303 | /// 304 | /// Returns one or more stringified properties from the log scope. 305 | /// 306 | /// The to stringify. 307 | /// An enumeration of scope properties from the current scope. 308 | private static IEnumerable StringifyScope(XUnitLogScope scope) 309 | { 310 | if (scope.State is IEnumerable> pairs) 311 | { 312 | foreach (var pair in pairs) 313 | { 314 | yield return $"{pair.Key}: {pair.Value}"; 315 | } 316 | } 317 | else if (scope.State is IEnumerable entries) 318 | { 319 | foreach (var entry in entries) 320 | { 321 | yield return entry; 322 | } 323 | } 324 | else 325 | { 326 | yield return scope.ToString(); 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/Shared/XUnitLoggerExtensions.IMessageSink.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using MartinCostello.Logging.XUnit; 5 | using Microsoft.Extensions.DependencyInjection.Extensions; 6 | 7 | namespace Microsoft.Extensions.Logging; 8 | 9 | /// 10 | /// A class containing extension methods for configuring logging to xunit. This class cannot be inherited. 11 | /// 12 | public static partial class XUnitLoggerExtensions 13 | { 14 | /// 15 | /// Adds an xunit logger to the logging builder. 16 | /// 17 | /// The to use. 18 | /// The to use. 19 | /// 20 | /// The instance of specified by . 21 | /// 22 | /// 23 | /// or is . 24 | /// 25 | public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSinkAccessor accessor) 26 | { 27 | #if NET 28 | ArgumentNullException.ThrowIfNull(builder); 29 | ArgumentNullException.ThrowIfNull(accessor); 30 | #else 31 | if (builder == null) 32 | { 33 | throw new ArgumentNullException(nameof(builder)); 34 | } 35 | 36 | if (accessor == null) 37 | { 38 | throw new ArgumentNullException(nameof(accessor)); 39 | } 40 | #endif 41 | 42 | return builder.AddXUnit(accessor, static (_) => { }); 43 | } 44 | 45 | /// 46 | /// Adds an xunit logger to the logging builder. 47 | /// 48 | /// The to use. 49 | /// The to use. 50 | /// A delegate to a method to use to configure the logging options. 51 | /// 52 | /// The instance of specified by . 53 | /// 54 | /// 55 | /// , or is . 56 | /// 57 | public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSinkAccessor accessor, Action configure) 58 | { 59 | #if NET 60 | ArgumentNullException.ThrowIfNull(builder); 61 | ArgumentNullException.ThrowIfNull(accessor); 62 | ArgumentNullException.ThrowIfNull(configure); 63 | #else 64 | if (builder == null) 65 | { 66 | throw new ArgumentNullException(nameof(builder)); 67 | } 68 | 69 | if (accessor == null) 70 | { 71 | throw new ArgumentNullException(nameof(accessor)); 72 | } 73 | 74 | if (configure == null) 75 | { 76 | throw new ArgumentNullException(nameof(configure)); 77 | } 78 | #endif 79 | 80 | var options = new XUnitLoggerOptions(); 81 | 82 | configure(options); 83 | 84 | #pragma warning disable CA2000 85 | builder.AddProvider(new XUnitLoggerProvider(accessor, options)); 86 | #pragma warning restore CA2000 87 | 88 | builder.Services.TryAddSingleton(accessor); 89 | 90 | return builder; 91 | } 92 | 93 | /// 94 | /// Adds an xunit logger to the logging builder. 95 | /// 96 | /// The to use. 97 | /// The to use. 98 | /// 99 | /// The instance of specified by . 100 | /// 101 | /// 102 | /// or is . 103 | /// 104 | public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSink messageSink) 105 | { 106 | #if NET 107 | ArgumentNullException.ThrowIfNull(builder); 108 | ArgumentNullException.ThrowIfNull(messageSink); 109 | #else 110 | if (builder == null) 111 | { 112 | throw new ArgumentNullException(nameof(builder)); 113 | } 114 | 115 | if (messageSink == null) 116 | { 117 | throw new ArgumentNullException(nameof(messageSink)); 118 | } 119 | #endif 120 | 121 | return builder.AddXUnit(messageSink, static (_) => { }); 122 | } 123 | 124 | /// 125 | /// Adds an xunit logger to the logging builder. 126 | /// 127 | /// The to use. 128 | /// The to use. 129 | /// A delegate to a method to use to configure the logging options. 130 | /// 131 | /// The instance of specified by . 132 | /// 133 | /// 134 | /// , or is . 135 | /// 136 | public static ILoggingBuilder AddXUnit(this ILoggingBuilder builder, IMessageSink messageSink, Action configure) 137 | { 138 | #if NET 139 | ArgumentNullException.ThrowIfNull(builder); 140 | ArgumentNullException.ThrowIfNull(messageSink); 141 | ArgumentNullException.ThrowIfNull(configure); 142 | #else 143 | if (builder == null) 144 | { 145 | throw new ArgumentNullException(nameof(builder)); 146 | } 147 | 148 | if (messageSink == null) 149 | { 150 | throw new ArgumentNullException(nameof(messageSink)); 151 | } 152 | 153 | if (configure == null) 154 | { 155 | throw new ArgumentNullException(nameof(configure)); 156 | } 157 | #endif 158 | 159 | var options = new XUnitLoggerOptions(); 160 | 161 | configure(options); 162 | 163 | #pragma warning disable CA2000 164 | return builder.AddProvider(new XUnitLoggerProvider(messageSink, options)); 165 | #pragma warning restore CA2000 166 | } 167 | 168 | /// 169 | /// Adds an xunit logger to the factory. 170 | /// 171 | /// The to use. 172 | /// The to use. 173 | /// The minimum to be logged. 174 | /// 175 | /// The instance of specified by . 176 | /// 177 | /// 178 | /// or is . 179 | /// 180 | public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, LogLevel minLevel) 181 | { 182 | #if NET 183 | ArgumentNullException.ThrowIfNull(factory); 184 | ArgumentNullException.ThrowIfNull(messageSink); 185 | #else 186 | if (factory == null) 187 | { 188 | throw new ArgumentNullException(nameof(factory)); 189 | } 190 | 191 | if (messageSink == null) 192 | { 193 | throw new ArgumentNullException(nameof(messageSink)); 194 | } 195 | #endif 196 | 197 | return factory.AddXUnit(messageSink, (_, level) => level >= minLevel); 198 | } 199 | 200 | /// 201 | /// Adds an xunit logger to the factory. 202 | /// 203 | /// The to use. 204 | /// The to use. 205 | /// The category filter to apply to logs. 206 | /// 207 | /// The instance of specified by . 208 | /// 209 | /// 210 | /// , or is . 211 | /// 212 | public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Func filter) 213 | { 214 | #if NET 215 | ArgumentNullException.ThrowIfNull(factory); 216 | ArgumentNullException.ThrowIfNull(messageSink); 217 | ArgumentNullException.ThrowIfNull(filter); 218 | #else 219 | if (factory == null) 220 | { 221 | throw new ArgumentNullException(nameof(factory)); 222 | } 223 | 224 | if (messageSink == null) 225 | { 226 | throw new ArgumentNullException(nameof(messageSink)); 227 | } 228 | 229 | if (filter == null) 230 | { 231 | throw new ArgumentNullException(nameof(filter)); 232 | } 233 | #endif 234 | 235 | return factory.AddXUnit(messageSink, (options) => options.Filter = filter); 236 | } 237 | 238 | /// 239 | /// Adds an xunit logger to the factory. 240 | /// 241 | /// The to use. 242 | /// The to use. 243 | /// 244 | /// The instance of specified by . 245 | /// 246 | /// 247 | /// or is . 248 | /// 249 | public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink) 250 | { 251 | #if NET 252 | ArgumentNullException.ThrowIfNull(factory); 253 | ArgumentNullException.ThrowIfNull(messageSink); 254 | #else 255 | if (factory == null) 256 | { 257 | throw new ArgumentNullException(nameof(factory)); 258 | } 259 | 260 | if (messageSink == null) 261 | { 262 | throw new ArgumentNullException(nameof(messageSink)); 263 | } 264 | #endif 265 | 266 | return factory.AddXUnit(messageSink, static (_) => { }); 267 | } 268 | 269 | /// 270 | /// Adds an xunit logger to the factory. 271 | /// 272 | /// The to use. 273 | /// The to use. 274 | /// The options to use for logging to xunit. 275 | /// 276 | /// The instance of specified by . 277 | /// 278 | /// 279 | /// , OR is . 280 | /// 281 | public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, XUnitLoggerOptions options) 282 | { 283 | #if NET 284 | ArgumentNullException.ThrowIfNull(factory); 285 | ArgumentNullException.ThrowIfNull(messageSink); 286 | ArgumentNullException.ThrowIfNull(options); 287 | #else 288 | if (factory == null) 289 | { 290 | throw new ArgumentNullException(nameof(factory)); 291 | } 292 | 293 | if (messageSink == null) 294 | { 295 | throw new ArgumentNullException(nameof(messageSink)); 296 | } 297 | 298 | if (options == null) 299 | { 300 | throw new ArgumentNullException(nameof(options)); 301 | } 302 | #endif 303 | 304 | return factory.AddXUnit(messageSink, () => options); 305 | } 306 | 307 | /// 308 | /// Adds an xunit logger to the factory. 309 | /// 310 | /// The to use. 311 | /// The to use. 312 | /// A delegate to a method to use to configure the logging options. 313 | /// 314 | /// The instance of specified by . 315 | /// 316 | /// 317 | /// , or is . 318 | /// 319 | public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Action configure) 320 | { 321 | #if NET 322 | ArgumentNullException.ThrowIfNull(factory); 323 | ArgumentNullException.ThrowIfNull(messageSink); 324 | ArgumentNullException.ThrowIfNull(configure); 325 | #else 326 | if (factory == null) 327 | { 328 | throw new ArgumentNullException(nameof(factory)); 329 | } 330 | 331 | if (messageSink == null) 332 | { 333 | throw new ArgumentNullException(nameof(messageSink)); 334 | } 335 | 336 | if (configure == null) 337 | { 338 | throw new ArgumentNullException(nameof(configure)); 339 | } 340 | #endif 341 | 342 | return factory.AddXUnit(messageSink, () => 343 | { 344 | var options = new XUnitLoggerOptions(); 345 | configure(options); 346 | return options; 347 | }); 348 | } 349 | 350 | /// 351 | /// Adds an xunit logger to the factory. 352 | /// 353 | /// The to use. 354 | /// The to use. 355 | /// A delegate to a method that returns a configured to use. 356 | /// 357 | /// The instance of specified by . 358 | /// 359 | /// 360 | /// , or is . 361 | /// 362 | public static ILoggerFactory AddXUnit(this ILoggerFactory factory, IMessageSink messageSink, Func configure) 363 | { 364 | #if NET 365 | ArgumentNullException.ThrowIfNull(factory); 366 | ArgumentNullException.ThrowIfNull(messageSink); 367 | ArgumentNullException.ThrowIfNull(configure); 368 | #else 369 | if (factory == null) 370 | { 371 | throw new ArgumentNullException(nameof(factory)); 372 | } 373 | 374 | if (messageSink == null) 375 | { 376 | throw new ArgumentNullException(nameof(messageSink)); 377 | } 378 | 379 | if (configure == null) 380 | { 381 | throw new ArgumentNullException(nameof(configure)); 382 | } 383 | #endif 384 | 385 | var options = configure(); 386 | 387 | #pragma warning disable CA2000 388 | factory.AddProvider(new XUnitLoggerProvider(messageSink, options)); 389 | #pragma warning restore CA2000 390 | 391 | return factory; 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /src/Shared/XUnitLoggerOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace MartinCostello.Logging.XUnit; 8 | 9 | /// 10 | /// A class representing configuration options for logging to xunit. 11 | /// 12 | public class XUnitLoggerOptions 13 | { 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | public XUnitLoggerOptions() 18 | { 19 | } 20 | 21 | /// 22 | /// Gets or sets the category filter to apply to logs. 23 | /// 24 | public Func Filter { get; set; } = static (c, l) => true; // By default log everything 25 | 26 | /// 27 | /// Gets or sets the message sink message factory to use when writing to a . 28 | /// 29 | public Func MessageSinkMessageFactory { get; set; } = static (m) => new DiagnosticMessage(m); 30 | 31 | /// 32 | /// Gets or sets a value indicating whether to include scopes. 33 | /// 34 | public bool IncludeScopes { get; set; } 35 | 36 | /// 37 | /// Gets or sets format string used to format the timestamp in log messages. Defaults to u. 38 | /// 39 | [StringSyntax(StringSyntaxAttribute.DateTimeFormat)] 40 | public string? TimestampFormat { get; set; } 41 | 42 | #if NET8_0_OR_GREATER 43 | 44 | /// 45 | /// Gets or sets the time provider used in log messages. Defaults to . 46 | /// 47 | public TimeProvider TimeProvider { get; set; } = TimeProvider.System; 48 | 49 | #endif 50 | } 51 | -------------------------------------------------------------------------------- /src/Shared/XUnitLoggerProvider.IMessageSink.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace MartinCostello.Logging.XUnit; 7 | 8 | /// 9 | /// A class representing an to use with xunit. 10 | /// 11 | public partial class XUnitLoggerProvider 12 | { 13 | /// 14 | /// The to use. This field is readonly. 15 | /// 16 | private readonly IMessageSinkAccessor? _messageSinkAccessor; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The to use. 22 | /// The options to use for logging to xunit. 23 | /// 24 | /// or is . 25 | /// 26 | public XUnitLoggerProvider(IMessageSink messageSink, XUnitLoggerOptions options) 27 | : this(new MessageSinkAccessor(messageSink), options) 28 | { 29 | } 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The to use. 35 | /// The options to use for logging to xunit. 36 | /// 37 | /// or is . 38 | /// 39 | public XUnitLoggerProvider(IMessageSinkAccessor accessor, XUnitLoggerOptions options) 40 | { 41 | _messageSinkAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); 42 | _options = options ?? throw new ArgumentNullException(nameof(options)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Shared/XUnitLoggerProvider.ITestOutputHelper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace MartinCostello.Logging.XUnit; 7 | 8 | /// 9 | /// A class representing an to use with xunit. 10 | /// 11 | public partial class XUnitLoggerProvider 12 | { 13 | /// 14 | /// The to use. This field is readonly. 15 | /// 16 | private readonly ITestOutputHelperAccessor? _outputHelperAccessor; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The to use. 22 | /// The options to use for logging to xunit. 23 | /// 24 | /// or is . 25 | /// 26 | public XUnitLoggerProvider(ITestOutputHelper outputHelper, XUnitLoggerOptions options) 27 | : this(new TestOutputHelperAccessor(outputHelper), options) 28 | { 29 | } 30 | 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | /// The to use. 35 | /// The options to use for logging to xunit. 36 | /// 37 | /// or is . 38 | /// 39 | public XUnitLoggerProvider(ITestOutputHelperAccessor accessor, XUnitLoggerOptions options) 40 | { 41 | _outputHelperAccessor = accessor ?? throw new ArgumentNullException(nameof(accessor)); 42 | _options = options ?? throw new ArgumentNullException(nameof(options)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Shared/XUnitLoggerProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using System.Collections.Concurrent; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace MartinCostello.Logging.XUnit; 8 | 9 | /// 10 | /// A class representing an to use with xunit. 11 | /// 12 | [ProviderAlias("XUnit")] 13 | public partial class XUnitLoggerProvider : ILoggerProvider 14 | { 15 | /// 16 | /// The cached loggers to use for each category. This field is readonly. 17 | /// 18 | private readonly ConcurrentDictionary _loggers = []; 19 | 20 | /// 21 | /// The to use. This field is readonly. 22 | /// 23 | private readonly XUnitLoggerOptions _options; 24 | 25 | /// 26 | /// Finalizes an instance of the class. 27 | /// 28 | ~XUnitLoggerProvider() 29 | { 30 | Dispose(false); 31 | } 32 | 33 | /// 34 | public virtual ILogger CreateLogger(string categoryName) 35 | { 36 | return _loggers.GetOrAdd(categoryName, (name) => 37 | _outputHelperAccessor is not null ? 38 | new(name, _outputHelperAccessor, _options) : 39 | new(name, _messageSinkAccessor!, _options)); 40 | } 41 | 42 | /// 43 | public void Dispose() 44 | { 45 | Dispose(true); 46 | GC.SuppressFinalize(this); 47 | } 48 | 49 | /// 50 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 51 | /// 52 | /// 53 | /// to release both managed and unmanaged resources; 54 | /// to release only unmanaged resources. 55 | /// 56 | protected virtual void Dispose(bool disposing) 57 | { 58 | // Nothing to dispose of 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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": "https://github.com/martincostello/xunit-logging", 6 | "copyrightText": "Copyright (c) {ownerName}, {year}. All rights reserved.\nLicensed under the {licenseName} license. See the {licenseFile} file in the project root for full license information.", 7 | "documentExposedElements": true, 8 | "documentInterfaces": true, 9 | "documentInternalElements": true, 10 | "documentPrivateElements": true, 11 | "documentPrivateFields": true, 12 | "fileNamingConvention": "metadata", 13 | "xmlHeader": false, 14 | "variables": { 15 | "licenseFile": "LICENSE", 16 | "licenseName": "Apache 2.0", 17 | "ownerName": "Martin Costello", 18 | "year": "2018" 19 | } 20 | }, 21 | "layoutRules": { 22 | "newlineAtEndOfFile": "require" 23 | }, 24 | "maintainabilityRules": {}, 25 | "namingRules": { 26 | "allowCommonHungarianPrefixes": true, 27 | "allowedHungarianPrefixes": [] 28 | }, 29 | "orderingRules": { 30 | "elementOrder": [ 31 | "kind", 32 | "accessibility", 33 | "constant", 34 | "static", 35 | "readonly" 36 | ], 37 | "systemUsingDirectivesFirst": true, 38 | "usingDirectivesPlacement": "outsideNamespace" 39 | }, 40 | "readabilityRules": {}, 41 | "spacingRules": {} 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Logging.XUnit.Tests/MartinCostello.Logging.XUnit.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tests for MartinCostello.Logging.XUnit. 4 | false 5 | MartinCostello.Logging.XUnit 6 | net9.0 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tests/Logging.XUnit.v3.Tests/MartinCostello.Logging.XUnit.v3.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(DefineConstants);XUNIT_V3 4 | Tests for MartinCostello.Logging.XUnit.v3. 5 | false 6 | Exe 7 | MartinCostello.Logging.XUnit 8 | net9.0 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/SampleApp/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | var app = builder.Build(); 7 | 8 | app.MapGet("/api/values", () => Results.Json(new string[] { "a", "b", "c" })); 9 | 10 | app.MapGet("/api/values/{id}", (string id) => "value"); 11 | 12 | app.MapPost("/api/values", (object value) => Results.NoContent()); 13 | 14 | app.MapPut("/api/values/{id}", (string id) => Results.NoContent()); 15 | 16 | app.MapDelete("/api/values/{id}", (string id) => Results.NoContent()); 17 | 18 | app.Run(); 19 | 20 | namespace SampleApp 21 | { 22 | public partial class Program 23 | { 24 | // Expose the Program class for use with WebApplicationFactory 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/SampleApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:52657", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": false, 15 | "launchUrl": "api/values", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "SampleApp": { 21 | "commandName": "SampleApp", 22 | "launchBrowser": false, 23 | "launchUrl": "api/values", 24 | "applicationUrl": "http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/SampleApp/SampleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | $(NoWarn);CA1801;CA1822;CA1861;SA1600;SA1601 5 | net9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/SampleApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tests/SampleApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*" 8 | } 9 | -------------------------------------------------------------------------------- /tests/Shared/AssemblyTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit; 5 | 6 | public static class AssemblyTests 7 | { 8 | [Fact] 9 | public static void Library_Is_Strong_Named() 10 | { 11 | // Arrange 12 | var assembly = typeof(XUnitLoggerOptions).Assembly; 13 | 14 | // Act 15 | var name = assembly.GetName(); 16 | var actual = name.GetPublicKey(); 17 | 18 | // Assert 19 | actual.ShouldNotBeNull(); 20 | actual.ShouldNotBeEmpty(); 21 | Convert.ToHexStringLower(actual).ShouldBe("00240000048000009400000006020000002400005253413100040000010001004b0b2efbada897147aa03d2076278890aefe2f8023562336d206ec8a719b06e89461c31b43abec615918d509158629f93385930c030494509e418bf396d69ce7dbe0b5b2db1a81543ab42777cb98210677fed69dbeb3237492a7ad69e87a1911ed20eb2d7c300238dc6f6403e3d04a1351c5cb369de4e022b18fbec70f7d21ed"); 22 | 23 | actual = name.GetPublicKeyToken(); 24 | actual.ShouldNotBeNull(); 25 | actual.ShouldNotBeEmpty(); 26 | Convert.ToHexStringLower(actual).ShouldBe("9a192a7522c9e1a0"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Shared/Constructor.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit; 5 | 6 | public enum Constructor 7 | { 8 | ITestOutputHelper, 9 | 10 | IMessageSink, 11 | } 12 | -------------------------------------------------------------------------------- /tests/Shared/Examples.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace MartinCostello.Logging.XUnit; 8 | 9 | public class Examples(ITestOutputHelper outputHelper) 10 | { 11 | [Fact] 12 | public void Calculator_Sums_Two_Equal_Integers() 13 | { 14 | // Arrange using conversion to a logger 15 | var calculator = new Calculator(outputHelper.ToLogger()); 16 | 17 | // Act 18 | int actual = calculator.Sum(2, 2); 19 | 20 | // Assert 21 | actual.ShouldBe(4); 22 | } 23 | 24 | [Fact] 25 | public void Calculator_Sums_Two_Different_Integers() 26 | { 27 | // Arrange using the logging provider 28 | var services = new ServiceCollection() 29 | .AddLogging((builder) => builder.AddXUnit(outputHelper)) 30 | .AddSingleton(); 31 | 32 | IServiceProvider provider = services.BuildServiceProvider(); 33 | 34 | var calculator = provider.GetRequiredService(); 35 | 36 | // Act 37 | int actual = calculator.Sum(1, 2); 38 | 39 | // Assert 40 | actual.ShouldBe(3); 41 | } 42 | 43 | private sealed class Calculator(ILogger logger) 44 | { 45 | public int Sum(int x, int y) 46 | { 47 | int sum = x + y; 48 | 49 | logger.LogInformation("The sum of {X} and {Y} is {Sum}.", x, y, sum); 50 | 51 | return sum; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Shared/Integration/DatabaseFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Extensions.Logging; 5 | 6 | namespace MartinCostello.Logging.XUnit.Integration; 7 | 8 | public sealed class DatabaseFixture : IAsyncLifetime 9 | { 10 | private readonly ILogger _initializeLogger; 11 | private readonly ILogger _disposeLogger; 12 | private string? _connectionString; 13 | 14 | public DatabaseFixture(IMessageSink messageSink) 15 | { 16 | using var loggerFactory = new LoggerFactory(); 17 | 18 | _initializeLogger = loggerFactory.AddXUnit(messageSink, c => c.MessageSinkMessageFactory = CreateMessage).CreateLogger(); 19 | _disposeLogger = messageSink.ToLogger(); 20 | 21 | #if XUNIT_V3 22 | static IMessageSinkMessage CreateMessage(string message) => new DiagnosticMessage() { Message = message }; 23 | #else 24 | static IMessageSinkMessage CreateMessage(string message) => new PrintableDiagnosticMessage(message); 25 | #endif 26 | } 27 | 28 | public string ConnectionString => _connectionString ?? throw new InvalidOperationException("The connection string is only available after InitializeAsync has completed."); 29 | 30 | #if XUNIT_V3 31 | ValueTask IAsyncLifetime.InitializeAsync() 32 | { 33 | _initializeLogger.LogInformation("Initializing database"); 34 | _connectionString = "Server=localhost"; 35 | return ValueTask.CompletedTask; 36 | } 37 | 38 | ValueTask IAsyncDisposable.DisposeAsync() 39 | { 40 | _disposeLogger.LogInformation("Disposing database"); 41 | return ValueTask.CompletedTask; 42 | } 43 | #else 44 | Task IAsyncLifetime.InitializeAsync() 45 | { 46 | _initializeLogger.LogInformation("Initializing database"); 47 | _connectionString = "Server=localhost"; 48 | return Task.CompletedTask; 49 | } 50 | 51 | Task IAsyncLifetime.DisposeAsync() 52 | { 53 | _disposeLogger.LogInformation("Disposing database"); 54 | return Task.CompletedTask; 55 | } 56 | #endif 57 | } 58 | -------------------------------------------------------------------------------- /tests/Shared/Integration/DatabaseTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit.Integration; 5 | 6 | public class DatabaseTests(DatabaseFixture databaseFixture) : IClassFixture 7 | { 8 | public DatabaseFixture DatabaseFixture { get; } = databaseFixture; 9 | 10 | [Fact] 11 | public void Run_Database_Test() 12 | { 13 | DatabaseFixture.ConnectionString.ShouldNotBeEmpty(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Shared/Integration/HttpApplicationTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using System.Net.Http.Json; 5 | using System.Net.Mime; 6 | using System.Text; 7 | 8 | namespace MartinCostello.Logging.XUnit.Integration; 9 | 10 | [Collection(HttpServerCollection.Name)] 11 | public sealed class HttpApplicationTests : IDisposable 12 | { 13 | public HttpApplicationTests(HttpServerFixture fixture, ITestOutputHelper outputHelper) 14 | { 15 | Fixture = fixture; 16 | Fixture.OutputHelper = outputHelper; 17 | } 18 | 19 | private HttpServerFixture Fixture { get; } 20 | 21 | public void Dispose() 22 | { 23 | Fixture.OutputHelper = null; 24 | } 25 | 26 | [Fact] 27 | public async Task Http_Get_Many() 28 | { 29 | // Arrange 30 | using var httpClient = Fixture.CreateClient(); 31 | 32 | // Act 33 | #if XUNIT_V3 34 | using var response = await httpClient.GetAsync("api/values", TestContext.Current.CancellationToken); 35 | #else 36 | using var response = await httpClient.GetAsync("api/values"); 37 | #endif 38 | 39 | // Assert 40 | response.IsSuccessStatusCode.ShouldBeTrue(); 41 | } 42 | 43 | [Fact] 44 | public async Task Http_Get_Single() 45 | { 46 | // Arrange 47 | using var httpClient = Fixture.CreateClient(); 48 | 49 | // Act 50 | #if XUNIT_V3 51 | using var response = await httpClient.GetAsync("api/values/a", TestContext.Current.CancellationToken); 52 | #else 53 | using var response = await httpClient.GetAsync("api/values/a"); 54 | #endif 55 | 56 | // Assert 57 | response.IsSuccessStatusCode.ShouldBeTrue(); 58 | } 59 | 60 | [Fact] 61 | public async Task Http_Post() 62 | { 63 | // Arrange 64 | using var httpClient = Fixture.CreateClient(); 65 | 66 | // Act 67 | #if XUNIT_V3 68 | using var response = await httpClient.PostAsJsonAsync("api/values", new { }, TestContext.Current.CancellationToken); 69 | #else 70 | using var response = await httpClient.PostAsJsonAsync("api/values", new { }); 71 | #endif 72 | 73 | // Assert 74 | response.IsSuccessStatusCode.ShouldBeTrue(); 75 | } 76 | 77 | [Fact] 78 | public async Task Http_Put() 79 | { 80 | // Arrange 81 | using var httpClient = Fixture.CreateClient(); 82 | 83 | // Act 84 | using var content = new StringContent(@"""d""", Encoding.UTF8, MediaTypeNames.Application.Json); 85 | #if XUNIT_V3 86 | using var response = await httpClient.PutAsync("api/values/d", content, TestContext.Current.CancellationToken); 87 | #else 88 | using var response = await httpClient.PutAsync("api/values/d", content); 89 | #endif 90 | 91 | // Assert 92 | response.IsSuccessStatusCode.ShouldBeTrue(); 93 | } 94 | 95 | [Fact] 96 | public async Task Http_Delete() 97 | { 98 | // Arrange 99 | using var httpClient = Fixture.CreateClient(); 100 | 101 | // Act 102 | #if XUNIT_V3 103 | using var response = await httpClient.DeleteAsync("api/values/d", TestContext.Current.CancellationToken); 104 | #else 105 | using var response = await httpClient.DeleteAsync("api/values/d"); 106 | #endif 107 | 108 | // Assert 109 | response.IsSuccessStatusCode.ShouldBeTrue(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/Shared/Integration/HttpServerCollection.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | namespace MartinCostello.Logging.XUnit.Integration; 5 | 6 | /// 7 | /// A class representing the collection fixture for an HTTP server. This class cannot be inherited. 8 | /// 9 | [CollectionDefinition(Name)] 10 | public sealed class HttpServerCollection : ICollectionFixture 11 | { 12 | /// 13 | /// The name of the test fixture. 14 | /// 15 | public const string Name = "HTTP server collection"; 16 | } 17 | -------------------------------------------------------------------------------- /tests/Shared/Integration/HttpServerFixture.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Mvc.Testing; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace MartinCostello.Logging.XUnit.Integration; 9 | 10 | /// 11 | /// A test fixture representing an HTTP server hosting the sample application. This class cannot be inherited. 12 | /// 13 | public sealed class HttpServerFixture : WebApplicationFactory, ITestOutputHelperAccessor 14 | { 15 | /// 16 | public ITestOutputHelper? OutputHelper { get; set; } 17 | 18 | /// 19 | protected override void ConfigureWebHost(IWebHostBuilder builder) 20 | => builder.ConfigureLogging((p) => p.AddXUnit(this)); 21 | } 22 | -------------------------------------------------------------------------------- /tests/Shared/Integration/PrintableDiagnosticMessage.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | #if !XUNIT_V3 5 | 6 | namespace MartinCostello.Logging.XUnit.Integration; 7 | 8 | /// 9 | /// See https://github.com/xunit/xunit/pull/2148#issuecomment-839838421. 10 | /// 11 | internal sealed class PrintableDiagnosticMessage(string message) : DiagnosticMessage(message) 12 | { 13 | public override string ToString() => Message; 14 | } 15 | #endif 16 | -------------------------------------------------------------------------------- /tests/Shared/IntegrationTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | using NSubstitute; 7 | 8 | namespace MartinCostello.Logging.XUnit; 9 | 10 | public static class IntegrationTests 11 | { 12 | [Fact] 13 | public static void Can_Configure_xunit_For_ILoggerBuilder_TestOutputHelper() 14 | { 15 | // Arrange 16 | var outputHelper = Substitute.For(); 17 | var logger = BootstrapBuilder((builder) => builder.AddXUnit(outputHelper)); 18 | 19 | // Act 20 | logger.LogError("This is a brand new problem, a problem without any clues."); 21 | logger.LogInformation("If you know the clues, it's easy to get through."); 22 | 23 | // Assert 24 | outputHelper.Received(2).WriteLine(Arg.Is((p) => p != null)); 25 | } 26 | 27 | [Fact] 28 | public static void Can_Configure_xunit_For_ILoggerBuilder_TestOutputHelper_With_Configuration() 29 | { 30 | // Arrange 31 | var outputHelper = Substitute.For(); 32 | 33 | var logger = BootstrapBuilder( 34 | (builder) => 35 | { 36 | builder.AddXUnit( 37 | outputHelper, 38 | (options) => options.Filter = (_, level) => level >= LogLevel.Error); 39 | }); 40 | 41 | // Act 42 | logger.LogError("This is a brand new problem, a problem without any clues."); 43 | logger.LogTrace("If you know the clues, it's easy to get through."); 44 | 45 | // Assert 46 | outputHelper.Received(1).WriteLine(Arg.Is((p) => p != null)); 47 | } 48 | 49 | [Fact] 50 | public static void Can_Configure_xunit_For_ILoggerBuilderAccessor_TestOutputHelper() 51 | { 52 | // Arrange 53 | var outputHelper = Substitute.For(); 54 | 55 | var accessor = Substitute.For(); 56 | accessor.OutputHelper.Returns(outputHelper); 57 | 58 | var logger = BootstrapBuilder((builder) => builder.AddXUnit(accessor)); 59 | 60 | // Act 61 | logger.LogError("This is a brand new problem, a problem without any clues."); 62 | logger.LogInformation("If you know the clues, it's easy to get through."); 63 | 64 | // Assert 65 | outputHelper.Received(2).WriteLine(Arg.Is((p) => p != null)); 66 | } 67 | 68 | [Fact] 69 | public static void Can_Configure_xunit_For_ILoggerBuilder_TestOutputHelperAccessor_With_Configuration() 70 | { 71 | // Arrange 72 | var outputHelper = Substitute.For(); 73 | 74 | var accessor = Substitute.For(); 75 | accessor.OutputHelper.Returns(outputHelper); 76 | 77 | var logger = BootstrapBuilder( 78 | (builder) => 79 | { 80 | builder.AddXUnit( 81 | outputHelper, 82 | (options) => options.Filter = (_, level) => level >= LogLevel.Error); 83 | }); 84 | 85 | // Act 86 | logger.LogError("This is a brand new problem, a problem without any clues."); 87 | logger.LogTrace("If you know the clues, it's easy to get through."); 88 | 89 | // Assert 90 | outputHelper.Received(1).WriteLine(Arg.Is((p) => p != null)); 91 | } 92 | 93 | [Fact] 94 | public static void Can_Configure_xunit_For_ILoggerFactory() 95 | { 96 | // Arrange 97 | var outputHelper = Substitute.For(); 98 | var logger = BootstrapFactory((builder) => builder.AddXUnit(outputHelper)); 99 | 100 | // Act 101 | logger.LogError("This is a brand new problem, a problem without any clues."); 102 | logger.LogInformation("If you know the clues, it's easy to get through."); 103 | 104 | // Assert 105 | outputHelper.Received(2).WriteLine(Arg.Is((p) => p != null)); 106 | } 107 | 108 | [Fact] 109 | public static void Can_Configure_xunit_For_ILoggerFactory_With_Filter() 110 | { 111 | // Arrange 112 | var outputHelper = Substitute.For(); 113 | var logger = BootstrapFactory((builder) => builder.AddXUnit(outputHelper, (_, level) => level >= LogLevel.Error)); 114 | 115 | // Act 116 | logger.LogError("This is a brand new problem, a problem without any clues."); 117 | logger.LogWarning("If you know the clues, it's easy to get through."); 118 | 119 | // Assert 120 | outputHelper.Received(1).WriteLine(Arg.Is((p) => p != null)); 121 | } 122 | 123 | [Fact] 124 | public static void Can_Configure_xunit_For_ILoggerFactory_With_Minimum_Level() 125 | { 126 | // Arrange 127 | var outputHelper = Substitute.For(); 128 | var logger = BootstrapFactory((builder) => builder.AddXUnit(outputHelper, LogLevel.Information)); 129 | 130 | // Act 131 | logger.LogError("This is a brand new problem, a problem without any clues."); 132 | logger.LogTrace("If you know the clues, it's easy to get through."); 133 | 134 | // Assert 135 | outputHelper.Received(1).WriteLine(Arg.Is((p) => p != null)); 136 | } 137 | 138 | [Fact] 139 | public static void Can_Configure_xunit_For_ILoggerFactory_With_Options() 140 | { 141 | // Arrange 142 | var outputHelper = Substitute.For(); 143 | 144 | var options = new XUnitLoggerOptions() 145 | { 146 | Filter = (_, level) => level >= LogLevel.Error, 147 | }; 148 | 149 | var logger = BootstrapFactory((builder) => builder.AddXUnit(outputHelper, options)); 150 | 151 | // Act 152 | logger.LogError("This is a brand new problem, a problem without any clues."); 153 | logger.LogWarning("If you know the clues, it's easy to get through."); 154 | 155 | // Assert 156 | outputHelper.Received(1).WriteLine(Arg.Is((p) => p != null)); 157 | } 158 | 159 | [Fact] 160 | public static void Can_Configure_xunit_For_ILoggerFactory_With_Options_Factory() 161 | { 162 | // Arrange 163 | var outputHelper = Substitute.For(); 164 | 165 | var options = new XUnitLoggerOptions() 166 | { 167 | Filter = (_, level) => level >= LogLevel.Error, 168 | TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff", 169 | }; 170 | 171 | var logger = BootstrapFactory((builder) => builder.AddXUnit(outputHelper, () => options)); 172 | 173 | // Act 174 | logger.LogError("This is a brand new problem, a problem without any clues."); 175 | logger.LogWarning("If you know the clues, it's easy to get through."); 176 | 177 | // Assert 178 | outputHelper.Received(1).WriteLine(Arg.Is((p) => p != null)); 179 | } 180 | 181 | [Fact] 182 | public static void Can_Configure_xunit_For_ILoggerFactory_With_Configure_Options() 183 | { 184 | // Arrange 185 | var outputHelper = Substitute.For(); 186 | var logger = BootstrapFactory( 187 | (builder) => 188 | { 189 | builder.AddXUnit( 190 | outputHelper, 191 | (options) => options.Filter = (_, level) => level >= LogLevel.Error); 192 | }); 193 | 194 | // Act 195 | logger.LogError("This is a brand new problem, a problem without any clues."); 196 | logger.LogWarning("If you know the clues, it's easy to get through."); 197 | 198 | // Assert 199 | outputHelper.Received(1).WriteLine(Arg.Is((p) => p != null)); 200 | } 201 | 202 | [Fact] 203 | public static void Can_Configure_xunit_For_ILoggerBuilder() 204 | { 205 | // Arrange 206 | var serviceProvider = new ServiceCollection() 207 | .AddLogging((builder) => builder.AddXUnit()) 208 | .BuildServiceProvider(); 209 | 210 | var outputHelper = Substitute.For(); 211 | 212 | serviceProvider.GetRequiredService().OutputHelper = outputHelper; 213 | 214 | var logger = serviceProvider.GetRequiredService>(); 215 | 216 | // Act 217 | logger.LogError("This is a brand new problem, a problem without any clues."); 218 | logger.LogInformation("If you know the clues, it's easy to get through."); 219 | 220 | // Assert 221 | outputHelper.Received(2).WriteLine(Arg.Is((p) => p != null)); 222 | } 223 | 224 | private static ILogger BootstrapBuilder(Action configure) 225 | { 226 | return new ServiceCollection() 227 | .AddLogging(configure) 228 | .BuildServiceProvider() 229 | .GetRequiredService>(); 230 | } 231 | 232 | private static ILogger BootstrapFactory(Action configure) 233 | { 234 | var services = new ServiceCollection() 235 | .AddLogging() 236 | .BuildServiceProvider(); 237 | 238 | var factory = services.GetRequiredService(); 239 | 240 | configure(factory); 241 | 242 | return factory.CreateLogger(); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /tests/Shared/XUnitLoggerExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Logging.Abstractions; 7 | using NSubstitute; 8 | 9 | namespace MartinCostello.Logging.XUnit; 10 | 11 | public static class XUnitLoggerExtensionsTests 12 | { 13 | [Fact] 14 | public static void AddXUnit_TestOutputHelper_For_ILoggerBuilder_Validates_Parameters() 15 | { 16 | // Arrange 17 | var builder = Substitute.For(); 18 | var outputHelper = Substitute.For(); 19 | var accessor = Substitute.For(); 20 | 21 | ILoggingBuilder nullBuilder = null!; 22 | ITestOutputHelperAccessor nullAccessor = null!; 23 | ITestOutputHelper nullHelper = null!; 24 | 25 | // Act and Assert 26 | Assert.Throws("builder", nullBuilder.AddXUnit); 27 | Assert.Throws("builder", () => nullBuilder.AddXUnit(outputHelper)); 28 | Assert.Throws("builder", () => nullBuilder.AddXUnit(outputHelper, ConfigureAction)); 29 | Assert.Throws("builder", () => nullBuilder.AddXUnit(accessor)); 30 | Assert.Throws("builder", () => nullBuilder.AddXUnit(accessor, ConfigureAction)); 31 | Assert.Throws("accessor", () => builder.AddXUnit(nullAccessor)); 32 | Assert.Throws("accessor", () => builder.AddXUnit(nullAccessor, ConfigureAction)); 33 | Assert.Throws("outputHelper", () => builder.AddXUnit(nullHelper)); 34 | Assert.Throws("outputHelper", () => builder.AddXUnit(nullHelper, ConfigureAction)); 35 | Assert.Throws("configure", () => builder.AddXUnit(outputHelper, null!)); 36 | Assert.Throws("configure", () => builder.AddXUnit(accessor, null!)); 37 | } 38 | 39 | [Fact] 40 | public static void AddXUnit_MessageSink_For_ILoggerBuilder_Validates_Parameters() 41 | { 42 | // Arrange 43 | var builder = Substitute.For(); 44 | var messageSink = Substitute.For(); 45 | var accessor = Substitute.For(); 46 | 47 | ILoggingBuilder nullBuilder = null!; 48 | IMessageSinkAccessor nullAccessor = null!; 49 | IMessageSink nullSink = null!; 50 | 51 | // Act and Assert 52 | Assert.Throws("builder", () => nullBuilder.AddXUnit(messageSink)); 53 | Assert.Throws("builder", () => nullBuilder.AddXUnit(messageSink, ConfigureAction)); 54 | Assert.Throws("builder", () => nullBuilder.AddXUnit(accessor)); 55 | Assert.Throws("builder", () => nullBuilder.AddXUnit(accessor, ConfigureAction)); 56 | Assert.Throws("accessor", () => builder.AddXUnit(nullAccessor)); 57 | Assert.Throws("accessor", () => builder.AddXUnit(nullAccessor, ConfigureAction)); 58 | Assert.Throws("messageSink", () => builder.AddXUnit(nullSink)); 59 | Assert.Throws("messageSink", () => builder.AddXUnit(nullSink, ConfigureAction)); 60 | Assert.Throws("configure", () => builder.AddXUnit(messageSink, null!)); 61 | Assert.Throws("configure", () => builder.AddXUnit(accessor, null!)); 62 | } 63 | 64 | [Fact] 65 | public static void AddXUnit_TestOutputHelper_For_ILoggerFactory_Validates_Parameters() 66 | { 67 | // Arrange 68 | ILoggerFactory factory = NullLoggerFactory.Instance; 69 | var logLevel = LogLevel.Information; 70 | var outputHelper = Substitute.For(); 71 | var options = new XUnitLoggerOptions(); 72 | 73 | ILoggerFactory nullFactory = null!; 74 | ITestOutputHelper nullHelper = null!; 75 | 76 | // Act and Assert 77 | Assert.Throws("factory", () => nullFactory.AddXUnit(outputHelper)); 78 | Assert.Throws("factory", () => nullFactory.AddXUnit(outputHelper, options)); 79 | Assert.Throws("factory", () => nullFactory.AddXUnit(outputHelper, ConfigureAction)); 80 | Assert.Throws("factory", () => nullFactory.AddXUnit(outputHelper, ConfigureFunction)); 81 | Assert.Throws("factory", () => nullFactory.AddXUnit(outputHelper, Filter)); 82 | Assert.Throws("factory", () => nullFactory.AddXUnit(outputHelper, logLevel)); 83 | Assert.Throws("outputHelper", () => factory.AddXUnit(nullHelper)); 84 | Assert.Throws("outputHelper", () => factory.AddXUnit(nullHelper, ConfigureAction)); 85 | Assert.Throws("outputHelper", () => factory.AddXUnit(nullHelper, ConfigureFunction)); 86 | Assert.Throws("outputHelper", () => factory.AddXUnit(nullHelper, Filter)); 87 | Assert.Throws("outputHelper", () => factory.AddXUnit(nullHelper, logLevel)); 88 | Assert.Throws("outputHelper", () => factory.AddXUnit(nullHelper, options)); 89 | Assert.Throws("options", () => factory.AddXUnit(outputHelper, (null as XUnitLoggerOptions)!)); 90 | Assert.Throws("configure", () => factory.AddXUnit(outputHelper, (null as Action)!)); 91 | Assert.Throws("configure", () => factory.AddXUnit(outputHelper, (null as Func)!)); 92 | Assert.Throws("filter", () => factory.AddXUnit(outputHelper, (null as Func)!)); 93 | } 94 | 95 | [Fact] 96 | public static void AddXUnit_MessageSink_For_ILoggerFactory_Validates_Parameters() 97 | { 98 | // Arrange 99 | ILoggerFactory factory = NullLoggerFactory.Instance; 100 | var logLevel = LogLevel.Information; 101 | var messageSink = Substitute.For(); 102 | var options = new XUnitLoggerOptions(); 103 | 104 | ILoggerFactory nullFactory = null!; 105 | IMessageSink nullSink = null!; 106 | 107 | // Act and Assert 108 | Assert.Throws("factory", () => nullFactory.AddXUnit(messageSink)); 109 | Assert.Throws("factory", () => nullFactory.AddXUnit(messageSink, options)); 110 | Assert.Throws("factory", () => nullFactory.AddXUnit(messageSink, ConfigureAction)); 111 | Assert.Throws("factory", () => nullFactory.AddXUnit(messageSink, ConfigureFunction)); 112 | Assert.Throws("factory", () => nullFactory.AddXUnit(messageSink, Filter)); 113 | Assert.Throws("factory", () => nullFactory.AddXUnit(messageSink, logLevel)); 114 | Assert.Throws("messageSink", () => factory.AddXUnit(nullSink)); 115 | Assert.Throws("messageSink", () => factory.AddXUnit(nullSink, ConfigureAction)); 116 | Assert.Throws("messageSink", () => factory.AddXUnit(nullSink, ConfigureFunction)); 117 | Assert.Throws("messageSink", () => factory.AddXUnit(nullSink, Filter)); 118 | Assert.Throws("messageSink", () => factory.AddXUnit(nullSink, logLevel)); 119 | Assert.Throws("messageSink", () => factory.AddXUnit(nullSink, options)); 120 | Assert.Throws("options", () => factory.AddXUnit(messageSink, (null as XUnitLoggerOptions)!)); 121 | Assert.Throws("configure", () => factory.AddXUnit(messageSink, (null as Action)!)); 122 | Assert.Throws("configure", () => factory.AddXUnit(messageSink, (null as Func)!)); 123 | Assert.Throws("filter", () => factory.AddXUnit(messageSink, (null as Func)!)); 124 | } 125 | 126 | [Fact] 127 | public static void ToLoggerFactory_Validates_Parameters() 128 | { 129 | // Arrange 130 | ITestOutputHelper outputHelper = null!; 131 | IMessageSink messageSink = null!; 132 | 133 | // Act and Assert 134 | Assert.Throws("outputHelper", outputHelper.ToLoggerFactory); 135 | Assert.Throws("messageSink", messageSink.ToLoggerFactory); 136 | } 137 | 138 | [Fact] 139 | public static void AddXUnit_Registers_Services() 140 | { 141 | // Arrange 142 | var services = new ServiceCollection(); 143 | 144 | // Act 145 | services.AddLogging(c => c.AddXUnit()); 146 | 147 | // Assert 148 | var serviceProvider = services.BuildServiceProvider(); 149 | serviceProvider.GetService().ShouldBeOfType(); 150 | serviceProvider.GetService().ShouldBeOfType(); 151 | } 152 | 153 | [Fact] 154 | public static void AddXUnit_ITestOutputHelperAccessor_Registers_Services() 155 | { 156 | // Arrange 157 | var services = new ServiceCollection(); 158 | var accessor = Substitute.For(); 159 | 160 | // Act 161 | services.AddLogging(c => c.AddXUnit(accessor)); 162 | 163 | // Assert 164 | var serviceProvider = services.BuildServiceProvider(); 165 | serviceProvider.GetService().ShouldBeOfType(); 166 | serviceProvider.GetService().ShouldBe(accessor); 167 | } 168 | 169 | [Fact] 170 | public static void AddXUnit_IMessageSinkAccessor_Registers_Services() 171 | { 172 | // Arrange 173 | var services = new ServiceCollection(); 174 | var accessor = Substitute.For(); 175 | 176 | // Act 177 | services.AddLogging(c => c.AddXUnit(accessor)); 178 | 179 | // Assert 180 | var serviceProvider = services.BuildServiceProvider(); 181 | serviceProvider.GetService().ShouldBeOfType(); 182 | serviceProvider.GetService().ShouldBe(accessor); 183 | } 184 | 185 | [Fact] 186 | public static void AddXUnit_ITestOutputHelper_Registers_Services() 187 | { 188 | // Arrange 189 | var services = new ServiceCollection(); 190 | var testOutputHelper = Substitute.For(); 191 | 192 | // Act 193 | services.AddLogging(c => c.AddXUnit(testOutputHelper)); 194 | 195 | // Assert 196 | var serviceProvider = services.BuildServiceProvider(); 197 | serviceProvider.GetService().ShouldBeOfType(); 198 | } 199 | 200 | [Fact] 201 | public static void AddXUnit_IMessageSink_Registers_Services() 202 | { 203 | // Arrange 204 | var services = new ServiceCollection(); 205 | var messageSink = Substitute.For(); 206 | 207 | // Act 208 | services.AddLogging(c => c.AddXUnit(messageSink)); 209 | 210 | // Assert 211 | var serviceProvider = services.BuildServiceProvider(); 212 | serviceProvider.GetService().ShouldBeOfType(); 213 | } 214 | 215 | [Fact] 216 | public static void AddXUnit_IMessageSink_With_LogLevel_Works() 217 | { 218 | // Arrange 219 | var factory = NullLoggerFactory.Instance; 220 | var messageSink = Substitute.For(); 221 | var minLevel = LogLevel.Debug; 222 | 223 | // Act 224 | factory.AddXUnit(messageSink, minLevel); 225 | 226 | // Assert 227 | ILogger logger = factory.CreateLogger("SomeLogger"); 228 | logger.LogInformation("Some message"); 229 | } 230 | 231 | [Fact] 232 | public static void AddXUnit_IMessageSink_With_Filter_Works() 233 | { 234 | // Arrange 235 | var factory = NullLoggerFactory.Instance; 236 | var messageSink = Substitute.For(); 237 | 238 | // Act 239 | factory.AddXUnit(messageSink, (_) => { }); 240 | 241 | // Assert 242 | ILogger logger = factory.CreateLogger("SomeLogger"); 243 | logger.LogInformation("Some message"); 244 | } 245 | 246 | [Fact] 247 | public static void AddXUnit_IMessageSink_With_Options_Works() 248 | { 249 | // Arrange 250 | var factory = NullLoggerFactory.Instance; 251 | var messageSink = Substitute.For(); 252 | var options = new XUnitLoggerOptions(); 253 | 254 | // Act 255 | factory.AddXUnit(messageSink, options); 256 | 257 | // Assert 258 | ILogger logger = factory.CreateLogger("SomeLogger"); 259 | logger.LogInformation("Some message"); 260 | } 261 | 262 | private static void ConfigureAction(XUnitLoggerOptions options) 263 | { 264 | } 265 | 266 | private static XUnitLoggerOptions ConfigureFunction() => new(); 267 | 268 | private static bool Filter(string? categoryName, LogLevel level) => true; 269 | } 270 | -------------------------------------------------------------------------------- /tests/Shared/XUnitLoggerProviderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Martin Costello, 2018. All rights reserved. 2 | // Licensed under the Apache 2.0 license. See the LICENSE file in the project root for full license information. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using NSubstitute; 6 | 7 | namespace MartinCostello.Logging.XUnit; 8 | 9 | public static class XUnitLoggerProviderTests 10 | { 11 | [Fact] 12 | public static void XUnitLoggerProvider_TestOutputHelper_Constructor_Validates_Parameters() 13 | { 14 | // Arrange 15 | var outputHelper = Substitute.For(); 16 | var accessor = Substitute.For(); 17 | var options = new XUnitLoggerOptions(); 18 | 19 | // Act and Assert 20 | Assert.Throws("outputHelper", () => new XUnitLoggerProvider((null as ITestOutputHelper)!, options)); 21 | Assert.Throws("accessor", () => new XUnitLoggerProvider((null as ITestOutputHelperAccessor)!, options)); 22 | Assert.Throws("options", () => new XUnitLoggerProvider(outputHelper, null!)); 23 | Assert.Throws("options", () => new XUnitLoggerProvider(accessor, null!)); 24 | } 25 | 26 | [Fact] 27 | public static void XUnitLoggerProvider_MessageSink_Constructor_Validates_Parameters() 28 | { 29 | // Arrange 30 | var messageSink = Substitute.For(); 31 | var accessor = Substitute.For(); 32 | var options = new XUnitLoggerOptions(); 33 | 34 | // Act and Assert 35 | Assert.Throws("messageSink", () => new XUnitLoggerProvider((null as IMessageSink)!, options)); 36 | Assert.Throws("accessor", () => new XUnitLoggerProvider((null as IMessageSinkAccessor)!, options)); 37 | Assert.Throws("options", () => new XUnitLoggerProvider(messageSink, null!)); 38 | Assert.Throws("options", () => new XUnitLoggerProvider(accessor, null!)); 39 | } 40 | 41 | [Theory] 42 | [InlineData(Constructor.ITestOutputHelper)] 43 | [InlineData(Constructor.IMessageSink)] 44 | public static void XUnitLoggerProvider_Creates_Logger(Constructor constructor) 45 | { 46 | // Arrange 47 | var testOutputHelper = Substitute.For(); 48 | var messageSink = Substitute.For(); 49 | var options = new XUnitLoggerOptions(); 50 | 51 | string categoryName = "MyLogger"; 52 | 53 | using var target = constructor switch 54 | { 55 | Constructor.ITestOutputHelper => new XUnitLoggerProvider(testOutputHelper, options), 56 | Constructor.IMessageSink => new XUnitLoggerProvider(messageSink, options), 57 | _ => throw new ArgumentOutOfRangeException(nameof(constructor), constructor, null), 58 | }; 59 | 60 | // Act 61 | ILogger actual = target.CreateLogger(categoryName); 62 | 63 | // Assert 64 | actual.ShouldNotBeNull(); 65 | 66 | var xunit = actual.ShouldBeOfType(); 67 | xunit.Name.ShouldBe(categoryName); 68 | xunit.Filter.ShouldBeSameAs(options.Filter); 69 | xunit.MessageSinkMessageFactory.ShouldBeSameAs(options.MessageSinkMessageFactory); 70 | xunit.IncludeScopes.ShouldBeFalse(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/Shared/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", 3 | "diagnosticMessages": true, 4 | "methodDisplay": "method" 5 | } 6 | --------------------------------------------------------------------------------