├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── stale.yml └── workflows │ ├── merge-dependabot.yml │ └── on-push-do-docs.yml ├── .gitignore ├── code_of_conduct.md ├── license.txt ├── readme.md └── src ├── .editorconfig ├── .gitattributes ├── Directory.Build.props ├── Directory.Packages.props ├── Shared.sln.DotSettings ├── Tests ├── InNamespaceTest.cs ├── MissingRegister.cs ├── NoArgumentsDetectedException.include.md ├── ParametersTests.cs ├── SkipDispose.Dispose_should_flush.verified.txt ├── SkipDispose.Write_after_dispose_should_throw.verified.txt ├── SkipDispose.cs ├── Snippets │ ├── ClassBeingTested.cs │ ├── ComplexParameterSample.cs │ ├── ContextPushedDownSample.cs │ ├── ContextSample.cs │ ├── ContextStaticSample.cs │ ├── CurrentTestSample.cs │ ├── CustomBase.cs │ ├── FilterSample.cs │ ├── FixtureSample.cs │ ├── ParametersSample.cs │ ├── TestBaseSample.cs │ ├── TestExceptionSample.cs │ ├── UniqueTestNameSample.cs │ └── XunitLoggerSample.cs ├── StaticConstructor.VerifyLogs.verified.txt ├── StaticConstructor.cs ├── TestContextWithNamespace.cs ├── TestException_Async.cs ├── TestException_Sync.cs ├── Tests.csproj ├── UsingClassFixture.Write_lines.verified.txt ├── UsingClassFixture.cs ├── UsingCurrentException.cs ├── UsingStatic.Async.verified.txt ├── UsingStatic.Null.verified.txt ├── UsingStatic.Overwrites.verified.txt ├── UsingStatic.Split_Lines.verified.txt ├── UsingStatic.Write_lines.verified.txt ├── UsingStatic.cs ├── UsingTestBase.Write_lines.verified.txt └── UsingTestBase.cs ├── XunitContext.sln ├── XunitContext.sln.DotSettings ├── XunitContext ├── Context.cs ├── Context_CurrentTest.cs ├── Context_Parameters.cs ├── Context_TestName.cs ├── DebugPoker.cs ├── Extensions.cs ├── Filters.cs ├── Fixture │ ├── ContextFixture.cs │ └── IContextFixture.cs ├── GlobalUsings.cs ├── Guard.cs ├── ModuleInitializer.cs ├── Parameter.cs ├── SolutionDirectoryFinder.cs ├── TestWriter.cs ├── TraceListener.cs ├── XunitContext.cs ├── XunitContext.csproj ├── XunitContextBase.cs └── build │ ├── XunitContext.props │ └── XunitContext.targets ├── appveyor.yml ├── global.json ├── icon.png ├── key.snk ├── mdsnippets.json └── nuget.config /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: SimonCropp -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug fix 3 | about: Create a bug fix to help us improve 4 | --- 5 | 6 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template. 7 | 8 | 9 | #### Preamble 10 | 11 | General questions may be better placed [StackOveflow](https://stackoverflow.com/). 12 | 13 | Where relevant, ensure you are using the current stable versions on your development stack. For example: 14 | 15 | * Visual Studio 16 | * [.NET SDK or .NET Core SDK](https://www.microsoft.com/net/download) 17 | * Any related NuGet packages 18 | 19 | Any code or stack traces must be properly formatted with [GitHub markdown](https://guides.github.com/features/mastering-markdown/). 20 | 21 | 22 | #### Describe the bug 23 | 24 | A clear and concise description of what the bug is. Include any relevant version information. 25 | 26 | A clear and concise description of what you expected to happen. 27 | 28 | Add any other context about the problem here. 29 | 30 | 31 | #### Minimal Repro 32 | 33 | Ensure you have replicated the bug in a minimal solution with the fewest moving parts. Often this will help point to the true cause of the problem. Upload this repro as part of the issue, preferably a public GitHub repository or a downloadable zip. The repro will allow the maintainers of this project to smoke test the any fix. 34 | 35 | #### Submit a PR that fixes the bug 36 | 37 | Submit a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/) that fixes the bug. Include in this PR a test that verifies the fix. If you were not able to fix the bug, a PR that illustrates your partial progress will suffice. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: How to raise feature requests 4 | --- 5 | 6 | 7 | Note: New issues raised, where it is clear the submitter has not read the issue template, are likely to be closed with "please read the issue template". Please don't take offense at this. It is simply a time management decision. If someone raises an issue, and can't be bothered to spend the time to read the issue template, then the project maintainers should not be expected to spend the time to read the submitted issue. Often too much time is spent going back and forth in issue comments asking for information that is outlined in the issue template. 8 | 9 | If you are certain the feature will be accepted, it is better to raise a [Pull Request (PR)](https://help.github.com/articles/about-pull-requests/). 10 | 11 | If you are uncertain if the feature will be accepted, outline the proposal below to confirm it is viable, prior to raising a PR that implements the feature. 12 | 13 | Note that even if the feature is a good idea and viable, it may not be accepted since the ongoing effort in maintaining the feature may outweigh the benefit it delivers. 14 | 15 | 16 | #### Is the feature request related to a problem 17 | 18 | A clear and concise description of what the problem is. 19 | 20 | 21 | #### Describe the solution 22 | 23 | A clear and concise proposal of how you intend to implement the feature. 24 | 25 | 26 | #### Describe alternatives considered 27 | 28 | A clear and concise description of any alternative solutions or features you've considered. 29 | 30 | 31 | #### Additional context 32 | 33 | Add any other context about the feature request here. 34 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/src" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 7 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Set to true to ignore issues in a milestone (defaults to false) 6 | exemptMilestones: true 7 | # Comment to post when marking an issue as stale. Set to `false` to disable 8 | markComment: > 9 | This issue has been automatically marked as stale because it has not had 10 | recent activity. It will be closed if no further activity occurs. Thank you 11 | for your contributions. 12 | # Comment to post when closing a stale issue. Set to `false` to disable 13 | closeComment: false 14 | # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': 15 | pulls: 16 | daysUntilStale: 30 17 | exemptLabels: 18 | - Question 19 | - Bug 20 | - Feature 21 | - Improvement -------------------------------------------------------------------------------- /.github/workflows/merge-dependabot.yml: -------------------------------------------------------------------------------- 1 | name: merge-dependabot 2 | on: 3 | pull_request: 4 | jobs: 5 | automerge: 6 | runs-on: ubuntu-latest 7 | if: github.actor == 'dependabot[bot]' 8 | steps: 9 | - name: Dependabot Auto Merge 10 | uses: ahmadnassri/action-dependabot-auto-merge@v2.6.6 11 | with: 12 | target: minor 13 | github-token: ${{ secrets.dependabot }} 14 | command: squash and merge -------------------------------------------------------------------------------- /.github/workflows/on-push-do-docs.yml: -------------------------------------------------------------------------------- 1 | name: on-push-do-docs 2 | on: 3 | push: 4 | jobs: 5 | release: 6 | runs-on: windows-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - name: Run MarkdownSnippets 10 | run: | 11 | dotnet tool install --global MarkdownSnippets.Tool 12 | mdsnippets ${GITHUB_WORKSPACE} 13 | shell: bash 14 | - name: Push changes 15 | run: | 16 | git config --local user.email "action@github.com" 17 | git config --local user.name "GitHub Action" 18 | git commit -m "Docs changes" -a || echo "nothing to commit" 19 | remote="https://${GITHUB_ACTOR}:${{secrets.GITHUB_TOKEN}}@github.com/${GITHUB_REPOSITORY}.git" 20 | branch="${GITHUB_REF:11}" 21 | git push "${remote}" ${branch} || echo "nothing to push" 22 | shell: bash -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | bin/ 4 | obj/ 5 | .vs/ 6 | *.DotSettings.user 7 | .idea/ 8 | *.received.* 9 | nugets/ -------------------------------------------------------------------------------- /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, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and 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 at simon.cropp@gmail.com. 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 https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Simon Cropp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # XunitContext 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/sdg2ni2jhe2o33le/branch/main?svg=true)](https://ci.appveyor.com/project/SimonCropp/XunitContext) 4 | [![NuGet Status](https://img.shields.io/nuget/v/XunitContext.svg)](https://www.nuget.org/packages/XunitContext/) 5 | 6 | Extends [xUnit](https://xunit.net/) to expose extra context and simplify logging. 7 | 8 | Redirects [Trace.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.trace.write), [Debug.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.write), and [Console.Write and Console.Error.Write](https://docs.microsoft.com/en-us/dotnet/api/system.console.write) to [ITestOutputHelper](https://xunit.net/docs/capturing-output). Also provides static access to the current [ITestOutputHelper](https://xunit.net/docs/capturing-output) for use within testing utility methods. 9 | 10 | Uses [AsyncLocal](https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1) to track state. 11 | 12 | **See [Milestones](../../milestones?state=closed) for release notes.** 13 | 14 | 15 | ## NuGet package 16 | 17 | https://nuget.org/packages/XunitContext/ 18 | 19 | 20 | ## ClassBeingTested 21 | 22 | 23 | 24 | ```cs 25 | static class ClassBeingTested 26 | { 27 | public static void Method() 28 | { 29 | Trace.WriteLine("From Trace"); 30 | Console.WriteLine("From Console"); 31 | Debug.WriteLine("From Debug"); 32 | Console.Error.WriteLine("From Console Error"); 33 | } 34 | } 35 | ``` 36 | snippet source | anchor 37 | 38 | 39 | 40 | ## XunitContextBase 41 | 42 | `XunitContextBase` is an abstract base class for tests. It exposes logging methods for use from unit tests, and handle the flushing of logs in its `Dispose` method. `XunitContextBase` is actually a thin wrapper over `XunitContext`. `XunitContext`s `Write*` methods can also be use inside a test inheriting from `XunitContextBase`. 43 | 44 | 45 | 46 | ```cs 47 | public class TestBaseSample(ITestOutputHelper output) : 48 | XunitContextBase(output) 49 | { 50 | [Fact] 51 | public void Write_lines() 52 | { 53 | WriteLine("From Test"); 54 | ClassBeingTested.Method(); 55 | 56 | var logs = XunitContext.Logs; 57 | 58 | Assert.Contains("From Test", logs); 59 | Assert.Contains("From Trace", logs); 60 | Assert.Contains("From Debug", logs); 61 | Assert.Contains("From Console", logs); 62 | Assert.Contains("From Console Error", logs); 63 | } 64 | } 65 | ``` 66 | snippet source | anchor 67 | 68 | 69 | 70 | ## xunit Fixture 71 | 72 | In addition to `XunitContextBase` class approach, one is also possible to use `IContextFixture` to gain access to `XunitContext` : 73 | 74 | 75 | 76 | ```cs 77 | public class FixtureSample(ITestOutputHelper helper, ContextFixture ctxFixture) : 78 | IContextFixture 79 | { 80 | Context context = ctxFixture.Start(helper); 81 | 82 | [Fact] 83 | public void Usage() 84 | { 85 | Console.WriteLine("From Test"); 86 | Assert.Contains("From Test", context.LogMessages); 87 | } 88 | } 89 | ``` 90 | snippet source | anchor 91 | 92 | 93 | 94 | ## Logging 95 | 96 | `XunitContext` provides static access to the logging state for tests. It exposes logging methods for use from unit tests, however registration of [ITestOutputHelper](https://xunit.net/docs/capturing-output) and flushing of logs must be handled explicitly. 97 | 98 | 99 | 100 | ```cs 101 | public class XunitLoggerSample : 102 | IDisposable 103 | { 104 | [Fact] 105 | public void Usage() 106 | { 107 | XunitContext.WriteLine("From Test"); 108 | 109 | ClassBeingTested.Method(); 110 | 111 | var logs = XunitContext.Logs; 112 | 113 | Assert.Contains("From Test", logs); 114 | Assert.Contains("From Trace", logs); 115 | Assert.Contains("From Debug", logs); 116 | Assert.Contains("From Console", logs); 117 | Assert.Contains("From Console Error", logs); 118 | } 119 | 120 | public XunitLoggerSample(ITestOutputHelper testOutput) => 121 | XunitContext.Register(testOutput); 122 | 123 | public void Dispose() => 124 | XunitContext.Flush(); 125 | } 126 | ``` 127 | snippet source | anchor 128 | 129 | 130 | `XunitContext` redirects [Trace.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.trace.write), [Console.Write](https://docs.microsoft.com/en-us/dotnet/api/system.console.write), and [Debug.Write](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.write) in its static constructor. 131 | 132 | 133 | 134 | ```cs 135 | Trace.Listeners.Clear(); 136 | Trace.Listeners.Add(new TraceListener()); 137 | #if (NETFRAMEWORK) 138 | Debug.Listeners.Clear(); 139 | Debug.Listeners.Add(new TraceListener()); 140 | #else 141 | DebugPoker.Overwrite( 142 | text => 143 | { 144 | if (string.IsNullOrEmpty(text)) 145 | { 146 | return; 147 | } 148 | 149 | if (text.EndsWith(Environment.NewLine)) 150 | { 151 | WriteLine(text.TrimTrailingNewline()); 152 | return; 153 | } 154 | 155 | Write(text); 156 | }); 157 | #endif 158 | TestWriter writer = new(); 159 | Console.SetOut(writer); 160 | Console.SetError(writer); 161 | ``` 162 | snippet source | anchor 163 | 164 | 165 | These API calls are then routed to the correct xUnit [ITestOutputHelper](https://xunit.net/docs/capturing-output) via a static [AsyncLocal](https://docs.microsoft.com/en-us/dotnet/api/system.threading.asynclocal-1). 166 | 167 | 168 | ### Logging Libs 169 | 170 | Approaches to routing common logging libraries to Diagnostics.Trace: 171 | 172 | * [Serilog](https://serilog.net/) use [Serilog.Sinks.Trace](https://github.com/serilog/serilog-sinks-trace). 173 | * [NLog](https://github.com/NLog/NLog) use a [Trace target](https://github.com/NLog/NLog/wiki/Trace-target). 174 | 175 | 176 | ## Filters 177 | 178 | `XunitContext.Filters` can be used to filter out unwanted lines: 179 | 180 | 181 | 182 | ```cs 183 | public class FilterSample(ITestOutputHelper output) : 184 | XunitContextBase(output) 185 | { 186 | static FilterSample() => 187 | Filters.Add(_ => _ != null && !_.Contains("ignored")); 188 | 189 | [Fact] 190 | public void Write_lines() 191 | { 192 | WriteLine("first"); 193 | WriteLine("with ignored string"); 194 | WriteLine("last"); 195 | var logs = XunitContext.Logs; 196 | 197 | Assert.Contains("first", logs); 198 | Assert.DoesNotContain("with ignored string", logs); 199 | Assert.Contains("last", logs); 200 | } 201 | } 202 | ``` 203 | snippet source | anchor 204 | 205 | 206 | Filters are static and shared for all tests. 207 | 208 | 209 | ## Context 210 | 211 | For every tests there is a contextual API to perform several operations. 212 | 213 | * `Context.TestOutput`: Access to [ITestOutputHelper](https://xunit.net/docs/capturing-output). 214 | * `Context.Write` and `Context.WriteLine`: Write to the current log. 215 | * `Context.LogMessages`: Access to all log message for the current test. 216 | * [Counters](#counters): Provide access in predicable and incrementing values for the following types: `Guid`, `Int`, `Long`, `UInt`, and `ULong`. 217 | * `Context.Test`: Access to the current `ITest`. 218 | * `Context.SourceFile`: Access to the file path for the current test. 219 | * `Context.SourceDirectory`: Access to the directory path for the current test. 220 | * `Context.SolutionDirectory`: The current solution directory. Obtained by walking up the directory tree from `SourceDirectory`. 221 | * `Context.TestException`: Access to the exception if the current test has failed. See [Test Failure](test-failure). 222 | 223 | 224 | 225 | ```cs 226 | // ReSharper disable UnusedVariable 227 | 228 | public class ContextSample(ITestOutputHelper output) : 229 | XunitContextBase(output) 230 | { 231 | [Fact] 232 | public void Usage() 233 | { 234 | Context.WriteLine("Some message"); 235 | 236 | var currentLogMessages = Context.LogMessages; 237 | 238 | var testOutputHelper = Context.TestOutput; 239 | 240 | var currentTest = Context.Test; 241 | 242 | var sourceFile = Context.SourceFile; 243 | 244 | var sourceDirectory = Context.SourceDirectory; 245 | 246 | var solutionDirectory = Context.SolutionDirectory; 247 | 248 | var currentTestException = Context.TestException; 249 | } 250 | } 251 | ``` 252 | snippet source | anchor 253 | 254 | 255 | Some members are pushed down to the be accessible directly from `XunitContextBase`: 256 | 257 | 258 | 259 | ```cs 260 | // ReSharper disable UnusedVariable 261 | 262 | public class ContextPushedDownSample(ITestOutputHelper output) : 263 | XunitContextBase(output) 264 | { 265 | [Fact] 266 | public void Usage() 267 | { 268 | WriteLine("Some message"); 269 | 270 | var currentLogMessages = Logs; 271 | 272 | var testOutputHelper = Output; 273 | 274 | var sourceFile = SourceFile; 275 | 276 | var sourceDirectory = SourceDirectory; 277 | 278 | var solutionDirectory = SolutionDirectory; 279 | 280 | var currentTestException = TestException; 281 | } 282 | } 283 | ``` 284 | snippet source | anchor 285 | 286 | 287 | Context can accessed via a static API: 288 | 289 | 290 | 291 | ```cs 292 | // ReSharper disable UnusedVariable 293 | 294 | public class ContextStaticSample(ITestOutputHelper output) : 295 | XunitContextBase(output) 296 | { 297 | [Fact] 298 | public void StaticUsage() 299 | { 300 | XunitContext.Context.WriteLine("Some message"); 301 | 302 | var currentLogMessages = XunitContext.Context.LogMessages; 303 | 304 | var testOutputHelper = XunitContext.Context.TestOutput; 305 | 306 | var currentTest = XunitContext.Context.Test; 307 | 308 | var sourceFile = XunitContext.Context.SourceFile; 309 | 310 | var sourceDirectory = XunitContext.Context.SourceDirectory; 311 | 312 | var solutionDirectory = XunitContext.Context.SolutionDirectory; 313 | 314 | var currentTestException = XunitContext.Context.TestException; 315 | } 316 | } 317 | ``` 318 | snippet source | anchor 319 | 320 | 321 | 322 | ### Current Test 323 | 324 | There is currently no API in xUnit to retrieve information on the current test. See issues [#1359](https://github.com/xunit/xunit/issues/1359), [#416](https://github.com/xunit/xunit/issues/416), and [#398](https://github.com/xunit/xunit/issues/398). 325 | 326 | To work around this, this project exposes the current instance of `ITest` via reflection. 327 | 328 | Usage: 329 | 330 | 331 | 332 | ```cs 333 | // ReSharper disable UnusedVariable 334 | 335 | public class CurrentTestSample(ITestOutputHelper output) : 336 | XunitContextBase(output) 337 | { 338 | [Fact] 339 | public void Usage() 340 | { 341 | var currentTest = Context.Test; 342 | // DisplayName will be 'CurrentTestSample.Usage' 343 | var displayName = currentTest.DisplayName; 344 | } 345 | 346 | [Fact] 347 | public void StaticUsage() 348 | { 349 | var currentTest = XunitContext.Context.Test; 350 | // DisplayName will be 'CurrentTestSample.StaticUsage' 351 | var displayName = currentTest.DisplayName; 352 | } 353 | } 354 | ``` 355 | snippet source | anchor 356 | 357 | 358 | Implementation: 359 | 360 | 361 | 362 | ```cs 363 | namespace Xunit; 364 | 365 | public partial class Context 366 | { 367 | ITest? test; 368 | 369 | 370 | public ITest Test 371 | { 372 | get 373 | { 374 | InitTest(); 375 | 376 | return test!; 377 | } 378 | } 379 | 380 | MethodInfo? methodInfo; 381 | 382 | public MethodInfo MethodInfo 383 | { 384 | get 385 | { 386 | InitTest(); 387 | return methodInfo!; 388 | } 389 | } 390 | 391 | Type? testType; 392 | 393 | public Type TestType 394 | { 395 | get 396 | { 397 | InitTest(); 398 | return testType!; 399 | } 400 | } 401 | 402 | void InitTest() 403 | { 404 | if (test != null) 405 | { 406 | return; 407 | } 408 | 409 | if (TestOutput == null) 410 | { 411 | throw new(MissingTestOutput); 412 | } 413 | 414 | #if NET8_0_OR_GREATER 415 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "test")] 416 | static extern ref ITest GetTest(TestOutputHelper? c); 417 | test = GetTest((TestOutputHelper) TestOutput); 418 | #else 419 | test = (ITest) GetTestMethod(TestOutput) 420 | .GetValue(TestOutput)!; 421 | #endif 422 | var method = (ReflectionMethodInfo) test.TestCase.TestMethod.Method; 423 | var type = (ReflectionTypeInfo) test.TestCase.TestMethod.TestClass.Class; 424 | methodInfo = method.MethodInfo; 425 | testType = type.Type; 426 | } 427 | 428 | public const string MissingTestOutput = "ITestOutputHelper has not been set. It is possible that the call to `XunitContext.Register()` is missing, or the current test does not inherit from `XunitContextBase`."; 429 | 430 | #if !NET8_0_OR_GREATER 431 | static FieldInfo? cachedTestMember; 432 | 433 | static FieldInfo GetTestMethod(ITestOutputHelper testOutput) 434 | { 435 | if (cachedTestMember != null) 436 | { 437 | return cachedTestMember; 438 | } 439 | 440 | var testOutputType = testOutput.GetType(); 441 | cachedTestMember = testOutputType.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); 442 | if (cachedTestMember == null) 443 | { 444 | throw new($"Unable to find 'test' field on {testOutputType.FullName}"); 445 | } 446 | 447 | return cachedTestMember; 448 | } 449 | #endif 450 | } 451 | ``` 452 | snippet source | anchor 453 | 454 | 455 | 456 | ### Test Failure 457 | 458 | When a test fails it is expressed as an exception. The exception can be viewed by enabling exception capture, and then accessing `Context.TestException`. The `TestException` will be null if the test has passed. 459 | 460 | One common case is to perform some logic, based on the existence of the exception, in the `Dispose` of a test. 461 | 462 | 463 | 464 | ```cs 465 | // ReSharper disable UnusedVariable 466 | public static class GlobalSetup 467 | { 468 | [ModuleInitializer] 469 | public static void Setup() => 470 | XunitContext.EnableExceptionCapture(); 471 | } 472 | 473 | public class TestExceptionSample(ITestOutputHelper output) : 474 | XunitContextBase(output) 475 | { 476 | [Fact(Skip = "Will fail")] 477 | public void Usage() => 478 | //This tests will fail 479 | Assert.False(true); 480 | 481 | public override void Dispose() 482 | { 483 | var theExceptionThrownByTest = Context.TestException; 484 | var testDisplayName = Context.Test.DisplayName; 485 | var testCase = Context.Test.TestCase; 486 | base.Dispose(); 487 | } 488 | } 489 | ``` 490 | snippet source | anchor 491 | 492 | 493 | 494 | ### Base Class 495 | 496 | When creating a custom base class for other tests, it is necessary to pass through the source file path to `XunitContextBase` via the constructor. 497 | 498 | 499 | 500 | ```cs 501 | public class CustomBase( 502 | ITestOutputHelper testOutput, 503 | [CallerFilePath] string sourceFile = "") 504 | : 505 | XunitContextBase(testOutput, sourceFile); 506 | ``` 507 | snippet source | anchor 508 | 509 | 510 | 511 | ### Parameters 512 | 513 | Provided the parameters passed to the current test when using a `[Theory]`. 514 | 515 | Use cases: 516 | 517 | * To derive the [unique test name](#uniquetestname). 518 | * In extensibility scenarios for example [Verify file naming](https://github.com/SimonCropp/Verify/blob/master/docs/naming.md). 519 | 520 | Usage: 521 | 522 | 523 | 524 | ```cs 525 | public class ParametersSample(ITestOutputHelper output) : 526 | XunitContextBase(output) 527 | { 528 | [Theory] 529 | [MemberData(nameof(GetData))] 530 | public void Usage(string arg) 531 | { 532 | var parameter = Context.Parameters.Single(); 533 | var parameterInfo = parameter.Info; 534 | Assert.Equal("arg", parameterInfo.Name); 535 | Assert.Equal(arg, parameter.Value); 536 | } 537 | 538 | public static IEnumerable GetData() 539 | { 540 | yield return ["Value1"]; 541 | yield return ["Value2"]; 542 | } 543 | } 544 | ``` 545 | snippet source | anchor 546 | 547 | 548 | Implementation: 549 | 550 | 551 | 552 | ```cs 553 | static List GetParameters(ITestCase testCase) => 554 | GetParameters(testCase, testCase.TestMethodArguments); 555 | 556 | static List GetParameters(ITestCase testCase, object[] arguments) 557 | { 558 | var method = testCase.TestMethod; 559 | var infos = method 560 | .Method.GetParameters() 561 | .ToList(); 562 | if (arguments == null || arguments.Length == 0) 563 | { 564 | if (infos.Count == 0) 565 | { 566 | return empty; 567 | } 568 | 569 | throw NewNoArgumentsDetectedException(); 570 | } 571 | 572 | List items = []; 573 | 574 | for (var index = 0; index < infos.Count; index++) 575 | { 576 | items.Add(new(infos[index], arguments[index])); 577 | } 578 | 579 | return items; 580 | } 581 | ``` 582 | snippet source | anchor 583 | 584 | 585 | 586 | #### Complex parameters 587 | 588 | Only core types (string, int, DateTime etc) can use the above automated approach. If a complex type is used the following exception will be thrown 589 | 590 | 591 | > No arguments detected for method with parameters. 592 | > This is most likely caused by using a parameter that Xunit cannot serialize. 593 | > Instead pass in a simple type as a parameter and construct the complex object inside the test. 594 | > Alternatively; override the current parameters using `UseParameters()` via the current test base class, or via `XunitContext.Current.UseParameters()`. 595 | 596 | 597 | To use complex types override the parameter resolution using `XunitContextBase.UseParameters`: 598 | 599 | 600 | 601 | ```cs 602 | public class ComplexParameterSample(ITestOutputHelper output) : 603 | XunitContextBase(output) 604 | { 605 | [Theory] 606 | [MemberData(nameof(GetData))] 607 | public void UseComplexMemberData(ComplexClass arg) 608 | { 609 | UseParameters(arg); 610 | var parameter = Context.Parameters.Single(); 611 | var parameterInfo = parameter.Info; 612 | Assert.Equal("arg", parameterInfo.Name); 613 | Assert.Equal(arg, parameter.Value); 614 | } 615 | 616 | public static IEnumerable GetData() 617 | { 618 | yield return [new ComplexClass("Value1")]; 619 | yield return [new ComplexClass("Value2")]; 620 | } 621 | 622 | public class ComplexClass(string value) 623 | { 624 | public string Value { get; } = value; 625 | } 626 | } 627 | ``` 628 | snippet source | anchor 629 | 630 | 631 | 632 | ### UniqueTestName 633 | 634 | Provided a string that uniquely identifies a test case. 635 | 636 | Usage: 637 | 638 | 639 | 640 | ```cs 641 | public class UniqueTestNameSample(ITestOutputHelper output) : 642 | XunitContextBase(output) 643 | { 644 | [Fact] 645 | public void Usage() 646 | { 647 | var testName = Context.UniqueTestName; 648 | 649 | Context.WriteLine(testName); 650 | } 651 | } 652 | ``` 653 | snippet source | anchor 654 | 655 | 656 | Implementation: 657 | 658 | 659 | 660 | ```cs 661 | string GetUniqueTestName(ITestCase testCase) 662 | { 663 | var method = testCase.TestMethod; 664 | var name = $"{method.TestClass.Class.ClassName()}.{method.Method.Name}"; 665 | if (!Parameters.Any()) 666 | { 667 | return name; 668 | } 669 | 670 | var builder = new StringBuilder($"{name}_"); 671 | foreach (var parameter in Parameters) 672 | { 673 | builder.Append($"{parameter.Info.Name}="); 674 | builder.Append(string.Join(",", SplitParams(parameter.Value))); 675 | builder.Append('_'); 676 | } 677 | 678 | builder.Length -= 1; 679 | 680 | return builder.ToString(); 681 | } 682 | 683 | static IEnumerable SplitParams(object? parameter) 684 | { 685 | if (parameter == null) 686 | { 687 | yield return "null"; 688 | yield break; 689 | } 690 | 691 | if (parameter is string stringValue) 692 | { 693 | yield return stringValue; 694 | yield break; 695 | } 696 | 697 | if (parameter is IEnumerable enumerable) 698 | { 699 | foreach (var item in enumerable) 700 | { 701 | foreach (var sub in SplitParams(item)) 702 | { 703 | yield return sub; 704 | } 705 | } 706 | 707 | yield break; 708 | } 709 | 710 | var toString = parameter.ToString(); 711 | if (toString == null) 712 | { 713 | yield return "null"; 714 | } 715 | else 716 | { 717 | yield return toString; 718 | } 719 | } 720 | ``` 721 | snippet source | anchor 722 | 723 | 724 | 725 | ## Global Setup 726 | 727 | Xunit has no way to run code once before any tests executing. So use one of the following: 728 | 729 | * [C# 9 Module Initializer](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-9.0/module-initializers). 730 | * [Fody Module Initializer](https://github.com/Fody/ModuleInit). 731 | * Having a single base class that all tests inherit from, and place any configuration code in the static constructor of that type. 732 | 733 | 734 | ## Icon 735 | 736 | [Wolverine](https://thenounproject.com/term/wolverine/18415/) designed by [Mike Rowe](https://thenounproject.com/itsmikerowe/) from [The Noun Project](https://thenounproject.com/). 737 | -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | 6 | [*.cs] 7 | indent_size = 4 8 | charset = utf-8 9 | 10 | # Redundant accessor body 11 | resharper_redundant_accessor_body_highlighting = error 12 | 13 | # Replace with field keyword 14 | resharper_replace_with_field_keyword_highlighting = error 15 | 16 | # Replace with single call to Single(..) 17 | resharper_replace_with_single_call_to_single_highlighting = error 18 | 19 | # Replace with single call to SingleOrDefault(..) 20 | resharper_replace_with_single_call_to_single_or_default_highlighting = error 21 | 22 | # Replace with single call to LastOrDefault(..) 23 | resharper_replace_with_single_call_to_last_or_default_highlighting = error 24 | 25 | # Replace with single call to Last(..) 26 | resharper_replace_with_single_call_to_last_highlighting = error 27 | 28 | # Replace with single call to First(..) 29 | resharper_replace_with_single_call_to_first_highlighting = error 30 | 31 | # Replace with single call to FirstOrDefault(..) 32 | resharper_replace_with_single_call_to_first_or_default_highlighting = error 33 | 34 | # Replace with single call to Any(..) 35 | resharper_replace_with_single_call_to_any_highlighting = error 36 | 37 | # Replace with single call to Count(..) 38 | resharper_replace_with_single_call_to_count_highlighting = error 39 | 40 | # Declare types in namespaces 41 | dotnet_diagnostic.CA1050.severity = none 42 | 43 | # Use Literals Where Appropriate 44 | dotnet_diagnostic.CA1802.severity = error 45 | 46 | # Template should be a static expression 47 | dotnet_diagnostic.CA2254.severity = error 48 | 49 | # Potentially misleading parameter name in lambda or local function 50 | resharper_all_underscore_local_parameter_name_highlighting = none 51 | 52 | # Redundant explicit collection creation in argument of 'params' parameter 53 | resharper_redundant_explicit_params_array_creation_highlighting = error 54 | 55 | # Do not initialize unnecessarily 56 | dotnet_diagnostic.CA1805.severity = error 57 | 58 | # Avoid unsealed attributes 59 | dotnet_diagnostic.CA1813.severity = error 60 | 61 | # Test for empty strings using string length 62 | dotnet_diagnostic.CA1820.severity = none 63 | 64 | # Remove empty finalizers 65 | dotnet_diagnostic.CA1821.severity = error 66 | 67 | # Mark members as static 68 | dotnet_diagnostic.CA1822.severity = error 69 | 70 | # Avoid unused private fields 71 | dotnet_diagnostic.CA1823.severity = error 72 | 73 | # Avoid zero-length array allocations 74 | dotnet_diagnostic.CA1825.severity = error 75 | 76 | # Use property instead of Linq Enumerable method 77 | dotnet_diagnostic.CA1826.severity = error 78 | 79 | # Do not use Count()/LongCount() when Any() can be used 80 | dotnet_diagnostic.CA1827.severity = error 81 | dotnet_diagnostic.CA1828.severity = error 82 | 83 | # Use Length/Count property instead of Enumerable.Count method 84 | dotnet_diagnostic.CA1829.severity = error 85 | 86 | # Prefer strongly-typed Append and Insert method overloads on StringBuilder 87 | dotnet_diagnostic.CA1830.severity = error 88 | 89 | # Use AsSpan instead of Range-based indexers for string when appropriate 90 | dotnet_diagnostic.CA1831.severity = error 91 | 92 | # Use AsSpan instead of Range-based indexers for string when appropriate 93 | dotnet_diagnostic.CA1831.severity = error 94 | dotnet_diagnostic.CA1832.severity = error 95 | dotnet_diagnostic.CA1833.severity = error 96 | 97 | # Use StringBuilder.Append(char) for single character strings 98 | dotnet_diagnostic.CA1834.severity = error 99 | 100 | # Prefer IsEmpty over Count when available 101 | dotnet_diagnostic.CA1836.severity = error 102 | 103 | # Prefer IsEmpty over Count when available 104 | dotnet_diagnostic.CA1836.severity = error 105 | 106 | # Use Environment.ProcessId instead of Process.GetCurrentProcess().Id 107 | dotnet_diagnostic.CA1837.severity = error 108 | 109 | # Use Environment.ProcessPath instead of Process.GetCurrentProcess().MainModule.FileName 110 | dotnet_diagnostic.CA1839.severity = error 111 | 112 | # Use Environment.CurrentManagedThreadId instead of Thread.CurrentThread.ManagedThreadId 113 | dotnet_diagnostic.CA1840.severity = error 114 | 115 | # Prefer Dictionary Contains methods 116 | dotnet_diagnostic.CA1841.severity = error 117 | 118 | # Do not use WhenAll with a single task 119 | dotnet_diagnostic.CA1842.severity = error 120 | 121 | # Do not use WhenAll/WaitAll with a single task 122 | dotnet_diagnostic.CA1842.severity = error 123 | dotnet_diagnostic.CA1843.severity = error 124 | 125 | # Use span-based 'string.Concat' 126 | dotnet_diagnostic.CA1845.severity = error 127 | 128 | # Prefer AsSpan over Substring 129 | dotnet_diagnostic.CA1846.severity = error 130 | 131 | # Use string.Contains(char) instead of string.Contains(string) with single characters 132 | dotnet_diagnostic.CA1847.severity = error 133 | 134 | # Prefer static HashData method over ComputeHash 135 | dotnet_diagnostic.CA1850.severity = error 136 | 137 | # Possible multiple enumerations of IEnumerable collection 138 | dotnet_diagnostic.CA1851.severity = error 139 | 140 | # Unnecessary call to Dictionary.ContainsKey(key) 141 | dotnet_diagnostic.CA1853.severity = error 142 | 143 | # Prefer the IDictionary.TryGetValue(TKey, out TValue) method 144 | dotnet_diagnostic.CA1854.severity = error 145 | 146 | # Use Span.Clear() instead of Span.Fill() 147 | dotnet_diagnostic.CA1855.severity = error 148 | 149 | # Incorrect usage of ConstantExpected attribute 150 | dotnet_diagnostic.CA1856.severity = error 151 | 152 | # The parameter expects a constant for optimal performance 153 | dotnet_diagnostic.CA1857.severity = error 154 | 155 | # Use StartsWith instead of IndexOf 156 | dotnet_diagnostic.CA1858.severity = error 157 | 158 | # Avoid using Enumerable.Any() extension method 159 | dotnet_diagnostic.CA1860.severity = error 160 | 161 | # Avoid constant arrays as arguments 162 | dotnet_diagnostic.CA1861.severity = error 163 | 164 | # Use the StringComparison method overloads to perform case-insensitive string comparisons 165 | dotnet_diagnostic.CA1862.severity = error 166 | 167 | # Prefer the IDictionary.TryAdd(TKey, TValue) method 168 | dotnet_diagnostic.CA1864.severity = error 169 | 170 | # Use string.Method(char) instead of string.Method(string) for string with single char 171 | dotnet_diagnostic.CA1865.severity = error 172 | dotnet_diagnostic.CA1866.severity = error 173 | dotnet_diagnostic.CA1867.severity = error 174 | 175 | # Unnecessary call to 'Contains' for sets 176 | dotnet_diagnostic.CA1868.severity = error 177 | 178 | # Cache and reuse 'JsonSerializerOptions' instances 179 | dotnet_diagnostic.CA1869.severity = error 180 | 181 | # Use a cached 'SearchValues' instance 182 | dotnet_diagnostic.CA1870.severity = error 183 | 184 | # Microsoft .NET properties 185 | trim_trailing_whitespace = true 186 | csharp_preferred_modifier_order = public, private, protected, internal, new, static, abstract, virtual, sealed, readonly, override, extern, unsafe, volatile, async:suggestion 187 | resharper_namespace_body = file_scoped 188 | dotnet_naming_rule.private_constants_rule.severity = warning 189 | dotnet_naming_rule.private_constants_rule.style = lower_camel_case_style 190 | dotnet_naming_rule.private_constants_rule.symbols = private_constants_symbols 191 | dotnet_naming_rule.private_instance_fields_rule.severity = warning 192 | dotnet_naming_rule.private_instance_fields_rule.style = lower_camel_case_style 193 | dotnet_naming_rule.private_instance_fields_rule.symbols = private_instance_fields_symbols 194 | dotnet_naming_rule.private_static_fields_rule.severity = warning 195 | dotnet_naming_rule.private_static_fields_rule.style = lower_camel_case_style 196 | dotnet_naming_rule.private_static_fields_rule.symbols = private_static_fields_symbols 197 | dotnet_naming_rule.private_static_readonly_rule.severity = warning 198 | dotnet_naming_rule.private_static_readonly_rule.style = lower_camel_case_style 199 | dotnet_naming_rule.private_static_readonly_rule.symbols = private_static_readonly_symbols 200 | dotnet_naming_style.lower_camel_case_style.capitalization = camel_case 201 | dotnet_naming_style.upper_camel_case_style.capitalization = pascal_case 202 | dotnet_naming_symbols.private_constants_symbols.applicable_accessibilities = private 203 | dotnet_naming_symbols.private_constants_symbols.applicable_kinds = field 204 | dotnet_naming_symbols.private_constants_symbols.required_modifiers = const 205 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_accessibilities = private 206 | dotnet_naming_symbols.private_instance_fields_symbols.applicable_kinds = field 207 | dotnet_naming_symbols.private_static_fields_symbols.applicable_accessibilities = private 208 | dotnet_naming_symbols.private_static_fields_symbols.applicable_kinds = field 209 | dotnet_naming_symbols.private_static_fields_symbols.required_modifiers = static 210 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_accessibilities = private 211 | dotnet_naming_symbols.private_static_readonly_symbols.applicable_kinds = field 212 | dotnet_naming_symbols.private_static_readonly_symbols.required_modifiers = static, readonly 213 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none 214 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none 215 | dotnet_style_parentheses_in_relational_binary_operators = never_if_unnecessary:none 216 | 217 | # ReSharper properties 218 | resharper_object_creation_when_type_not_evident = target_typed 219 | 220 | # ReSharper inspection severities 221 | resharper_arrange_object_creation_when_type_evident_highlighting = error 222 | resharper_arrange_object_creation_when_type_not_evident_highlighting = error 223 | resharper_arrange_redundant_parentheses_highlighting = error 224 | resharper_arrange_static_member_qualifier_highlighting = error 225 | resharper_arrange_this_qualifier_highlighting = error 226 | resharper_arrange_type_member_modifiers_highlighting = none 227 | resharper_built_in_type_reference_style_for_member_access_highlighting = hint 228 | resharper_built_in_type_reference_style_highlighting = hint 229 | resharper_check_namespace_highlighting = none 230 | resharper_convert_to_using_declaration_highlighting = error 231 | resharper_css_not_resolved_highlighting = warning 232 | resharper_field_can_be_made_read_only_local_highlighting = none 233 | resharper_merge_into_logical_pattern_highlighting = warning 234 | resharper_merge_into_pattern_highlighting = error 235 | resharper_method_has_async_overload_highlighting = warning 236 | # because stop rider giving errors before source generators have run 237 | resharper_partial_type_with_single_part_highlighting = warning 238 | resharper_redundant_base_qualifier_highlighting = warning 239 | resharper_redundant_cast_highlighting = error 240 | resharper_redundant_empty_object_creation_argument_list_highlighting = error 241 | resharper_redundant_empty_object_or_collection_initializer_highlighting = error 242 | resharper_redundant_name_qualifier_highlighting = error 243 | resharper_redundant_suppress_nullable_warning_expression_highlighting = error 244 | resharper_redundant_using_directive_highlighting = error 245 | resharper_redundant_verbatim_string_prefix_highlighting = error 246 | resharper_redundant_lambda_signature_parentheses_highlighting = error 247 | resharper_replace_substring_with_range_indexer_highlighting = warning 248 | resharper_suggest_var_or_type_built_in_types_highlighting = error 249 | resharper_suggest_var_or_type_elsewhere_highlighting = error 250 | resharper_suggest_var_or_type_simple_types_highlighting = error 251 | resharper_unnecessary_whitespace_highlighting = error 252 | resharper_use_await_using_highlighting = warning 253 | resharper_use_deconstruction_highlighting = warning 254 | 255 | # Sort using and Import directives with System.* appearing first 256 | dotnet_sort_system_directives_first = true 257 | 258 | # Avoid "this." and "Me." if not necessary 259 | dotnet_style_qualification_for_field = false:error 260 | dotnet_style_qualification_for_property = false:error 261 | dotnet_style_qualification_for_method = false:error 262 | dotnet_style_qualification_for_event = false:error 263 | 264 | # Use language keywords instead of framework type names for type references 265 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 266 | dotnet_style_predefined_type_for_member_access = true:error 267 | 268 | # Suggest more modern language features when available 269 | dotnet_style_object_initializer = true:error 270 | dotnet_style_collection_initializer = true:error 271 | dotnet_style_coalesce_expression = false:error 272 | dotnet_style_null_propagation = true:error 273 | dotnet_style_explicit_tuple_names = true:error 274 | 275 | # Use collection expression syntax 276 | resharper_use_collection_expression_highlighting = error 277 | 278 | # Prefer "var" everywhere 279 | csharp_style_var_for_built_in_types = true:error 280 | csharp_style_var_when_type_is_apparent = true:error 281 | csharp_style_var_elsewhere = true:error 282 | 283 | # Prefer method-like constructs to have a block body 284 | csharp_style_expression_bodied_methods = true:error 285 | csharp_style_expression_bodied_local_functions = true:error 286 | csharp_style_expression_bodied_constructors = true:error 287 | csharp_style_expression_bodied_operators = true:error 288 | resharper_place_expr_method_on_single_line = false 289 | 290 | # Prefer property-like constructs to have an expression-body 291 | csharp_style_expression_bodied_properties = true:error 292 | csharp_style_expression_bodied_indexers = true:error 293 | csharp_style_expression_bodied_accessors = true:error 294 | 295 | # Suggest more modern language features when available 296 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 297 | csharp_style_pattern_matching_over_as_with_null_check = true:error 298 | csharp_style_inlined_variable_declaration = true:suggestion 299 | csharp_style_throw_expression = true:suggestion 300 | csharp_style_conditional_delegate_call = true:suggestion 301 | 302 | # Newline settings 303 | #csharp_new_line_before_open_brace = all:error 304 | resharper_max_array_initializer_elements_on_line = 1 305 | csharp_new_line_before_else = true 306 | csharp_new_line_before_catch = true 307 | csharp_new_line_before_finally = true 308 | csharp_new_line_before_members_in_object_initializers = true 309 | csharp_new_line_before_members_in_anonymous_types = true 310 | resharper_wrap_before_first_type_parameter_constraint = true 311 | resharper_wrap_extends_list_style = chop_always 312 | resharper_wrap_after_dot_in_method_calls = false 313 | resharper_wrap_before_binary_pattern_op = false 314 | resharper_wrap_object_and_collection_initializer_style = chop_always 315 | resharper_place_simple_initializer_on_single_line = false 316 | 317 | # space 318 | resharper_space_around_lambda_arrow = true 319 | 320 | dotnet_style_require_accessibility_modifiers = never:error 321 | resharper_place_type_constraints_on_same_line = false 322 | resharper_blank_lines_inside_namespace = 0 323 | resharper_blank_lines_after_file_scoped_namespace_directive = 1 324 | resharper_blank_lines_inside_type = 0 325 | 326 | insert_final_newline = false 327 | resharper_place_attribute_on_same_line = false 328 | resharper_space_around_lambda_arrow = true 329 | resharper_place_constructor_initializer_on_same_line = false 330 | 331 | #braces https://www.jetbrains.com/help/resharper/EditorConfig_CSHARP_CSharpCodeStylePageImplSchema.html#Braces 332 | resharper_braces_for_ifelse = required 333 | resharper_braces_for_foreach = required 334 | resharper_braces_for_while = required 335 | resharper_braces_for_dowhile = required 336 | resharper_braces_for_lock = required 337 | resharper_braces_for_fixed = required 338 | resharper_braces_for_for = required 339 | 340 | resharper_return_value_of_pure_method_is_not_used_highlighting = error 341 | 342 | resharper_all_underscore_local_parameter_name_highlighting = none 343 | 344 | resharper_misleading_body_like_statement_highlighting = error 345 | 346 | resharper_redundant_record_class_keyword_highlighting = error 347 | 348 | resharper_redundant_extends_list_entry_highlighting = error 349 | 350 | # Xml files 351 | [*.{xml,config,nuspec,resx,vsixmanifest,csproj,targets,props,fsproj}] 352 | indent_size = 2 353 | # https://www.jetbrains.com/help/resharper/EditorConfig_XML_XmlCodeStylePageSchema.html#resharper_xml_blank_line_after_pi 354 | resharper_blank_line_after_pi = false 355 | resharper_space_before_self_closing = true 356 | ij_xml_space_inside_empty_tag = true 357 | 358 | [*.json] 359 | indent_size = 2 360 | 361 | # Verify settings 362 | [*.{received,verified}.{txt,xml,json,md,sql,csv,html,htm,md}] 363 | charset = utf-8-bom 364 | end_of_line = lf 365 | indent_size = unset 366 | indent_style = unset 367 | insert_final_newline = false 368 | tab_width = unset 369 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/.gitattributes: -------------------------------------------------------------------------------- 1 | * text 2 | *.png binary 3 | *.snk binary 4 | 5 | *.verified.txt text eol=lf working-tree-encoding=UTF-8 6 | *.verified.xml text eol=lf working-tree-encoding=UTF-8 7 | *.verified.json text eol=lf working-tree-encoding=UTF-8 8 | 9 | .editorconfig text eol=lf working-tree-encoding=UTF-8 10 | Shared.sln.DotSettings text eol=lf working-tree-encoding=UTF-8 -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 3.3.2 5 | preview 6 | 1.0.0 7 | CS1591;xUnit1026;CA2255;NU1608;NU1109 8 | enable 9 | true 10 | true 11 | true 12 | true 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Shared.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | False 3 | Quiet 4 | True 5 | True 6 | True 7 | DO_NOT_SHOW 8 | ERROR 9 | ERROR 10 | ERROR 11 | WARNING 12 | ERROR 13 | ERROR 14 | ERROR 15 | ERROR 16 | ERROR 17 | ERROR 18 | ERROR 19 | ERROR 20 | ERROR 21 | ERROR 22 | ERROR 23 | ERROR 24 | ERROR 25 | ERROR 26 | ERROR 27 | ERROR 28 | ERROR 29 | ERROR 30 | ERROR 31 | ERROR 32 | ERROR 33 | ERROR 34 | ERROR 35 | ERROR 36 | ERROR 37 | ERROR 38 | ERROR 39 | ERROR 40 | ERROR 41 | ERROR 42 | ERROR 43 | DO_NOT_SHOW 44 | DO_NOT_SHOW 45 | ERROR 46 | ERROR 47 | ERROR 48 | ERROR 49 | ERROR 50 | ERROR 51 | ERROR 52 | ERROR 53 | ERROR 54 | ERROR 55 | ERROR 56 | ERROR 57 | C90+,E79+,S14+ 58 | ERROR 59 | ERROR 60 | ERROR 61 | ERROR 62 | ERROR 63 | ERROR 64 | ERROR 65 | ERROR 66 | ERROR 67 | ERROR 68 | ERROR 69 | ERROR 70 | ERROR 71 | ERROR 72 | ERROR 73 | ERROR 74 | ERROR 75 | ERROR 76 | ERROR 77 | ERROR 78 | ERROR 79 | ERROR 80 | ERROR 81 | ERROR 82 | ERROR 83 | ERROR 84 | ERROR 85 | ERROR 86 | ERROR 87 | ERROR 88 | ERROR 89 | ERROR 90 | ERROR 91 | ERROR 92 | ERROR 93 | ERROR 94 | ERROR 95 | ERROR 96 | ERROR 97 | ERROR 98 | ERROR 99 | ERROR 100 | ERROR 101 | ERROR 102 | ERROR 103 | ERROR 104 | ERROR 105 | ERROR 106 | ERROR 107 | ERROR 108 | ERROR 109 | ERROR 110 | ERROR 111 | ERROR 112 | ERROR 113 | ERROR 114 | ERROR 115 | ERROR 116 | ERROR 117 | ERROR 118 | ERROR 119 | ERROR 120 | ERROR 121 | ERROR 122 | DO_NOT_SHOW 123 | *.received.* 124 | *.verified.* 125 | ERROR 126 | ERROR 127 | DO_NOT_SHOW 128 | ECMAScript 2016 129 | <?xml version="1.0" encoding="utf-16"?><Profile name="c# Cleanup"><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><CSCodeStyleAttributes ArrangeVarStyle="True" ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeCodeBodyStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" ArrangeNamespaces="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JSStringLiteralQuotesDescriptor>True</JSStringLiteralQuotesDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsInsertSemicolon>True</JsInsertSemicolon><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><HtmlReformatCode>True</HtmlReformatCode><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><IDEA_SETTINGS>&lt;profile version="1.0"&gt; 130 | &lt;option name="myName" value="c# Cleanup" /&gt; 131 | &lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; 132 | &lt;Language id="EditorConfig"&gt; 133 | &lt;Reformat&gt;false&lt;/Reformat&gt; 134 | &lt;/Language&gt; 135 | &lt;Language id="HTML"&gt; 136 | &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; 137 | &lt;Reformat&gt;false&lt;/Reformat&gt; 138 | &lt;Rearrange&gt;false&lt;/Rearrange&gt; 139 | &lt;/Language&gt; 140 | &lt;Language id="JSON"&gt; 141 | &lt;Reformat&gt;false&lt;/Reformat&gt; 142 | &lt;/Language&gt; 143 | &lt;Language id="RELAX-NG"&gt; 144 | &lt;Reformat&gt;false&lt;/Reformat&gt; 145 | &lt;/Language&gt; 146 | &lt;Language id="XML"&gt; 147 | &lt;OptimizeImports&gt;false&lt;/OptimizeImports&gt; 148 | &lt;Reformat&gt;false&lt;/Reformat&gt; 149 | &lt;Rearrange&gt;false&lt;/Rearrange&gt; 150 | &lt;/Language&gt; 151 | &lt;/profile&gt;</RIDER_SETTINGS></Profile> 152 | ExpressionBody 153 | ExpressionBody 154 | ExpressionBody 155 | False 156 | NEVER 157 | NEVER 158 | False 159 | False 160 | False 161 | True 162 | False 163 | CHOP_ALWAYS 164 | False 165 | False 166 | RemoveIndent 167 | RemoveIndent 168 | False 169 | True 170 | True 171 | True 172 | True 173 | True 174 | ERROR 175 | DoNothing 176 | -------------------------------------------------------------------------------- /src/Tests/InNamespaceTest.cs: -------------------------------------------------------------------------------- 1 | namespace MyNamespace.Bar; 2 | 3 | public class InNamespaceTest(ITestOutputHelper output) : 4 | XunitContextBase(output) 5 | { 6 | [Fact] 7 | public void Usage() => 8 | Assert.Equal("InNamespaceTest.Usage", Context.UniqueTestName); 9 | } -------------------------------------------------------------------------------- /src/Tests/MissingRegister.cs: -------------------------------------------------------------------------------- 1 | public class MissingRegister 2 | { 3 | [Fact] 4 | public void CurrentTest() 5 | { 6 | var exception = Assert.Throws(() => XunitContext.Context.Test.DisplayName); 7 | Assert.Equal(Context.MissingTestOutput, exception.Message); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Tests/NoArgumentsDetectedException.include.md: -------------------------------------------------------------------------------- 1 | 2 | > No arguments detected for method with parameters. 3 | > This is most likely caused by using a parameter that Xunit cannot serialize. 4 | > Instead pass in a simple type as a parameter and construct the complex object inside the test. 5 | > Alternatively; override the current parameters using `UseParameters()` via the current test base class, or via `XunitContext.Current.UseParameters()`. 6 | 7 | -------------------------------------------------------------------------------- /src/Tests/ParametersTests.cs: -------------------------------------------------------------------------------- 1 | public class ParametersTests(ITestOutputHelper output) : 2 | XunitContextBase(output) 3 | { 4 | [Theory] 5 | [InlineData("Value1")] 6 | [InlineData("Value2")] 7 | public void InlineData(string arg) 8 | { 9 | var parameter = Context.Parameters.Single(); 10 | var parameterInfo = parameter.Info; 11 | Assert.Equal("arg", parameterInfo.Name); 12 | Assert.Equal(arg, parameter.Value); 13 | Assert.Equal($"ParametersTests.InlineData_arg={arg}", Context.UniqueTestName); 14 | } 15 | 16 | [Theory] 17 | [MemberData(nameof(GetData))] 18 | public void MemberData(string arg) 19 | { 20 | var parameter = Context.Parameters.Single(); 21 | var parameterInfo = parameter.Info; 22 | Assert.Equal("arg", parameterInfo.Name); 23 | Assert.Equal(arg, parameter.Value); 24 | Assert.Equal($"ParametersTests.MemberData_arg={arg}", Context.UniqueTestName); 25 | } 26 | 27 | public static IEnumerable GetData() 28 | { 29 | yield return ["Value1"]; 30 | yield return ["Value2"]; 31 | } 32 | 33 | [Theory] 34 | [MemberData(nameof(GetEnumerableData))] 35 | public void EnumerableMemberData(string arg1, string[] arg2) => 36 | Assert.Equal("ParametersTests.EnumerableMemberData_arg1=Value1_arg2=Value2,Value3,null", Context.UniqueTestName); 37 | 38 | public static IEnumerable GetEnumerableData() 39 | { 40 | yield return 41 | [ 42 | "Value1", 43 | new[] 44 | { 45 | "Value2", 46 | "Value3", 47 | null 48 | } 49 | ]; 50 | } 51 | 52 | [Theory] 53 | [MemberData(nameof(GetDataComplex))] 54 | public void ShouldNotThrowIfParamsNotAccessed(ComplexClass arg) 55 | { 56 | } 57 | 58 | [Theory] 59 | [MemberData(nameof(GetDataComplex))] 60 | public async Task ShouldThrowForComplexParam(ComplexClass arg) 61 | { 62 | var exception = Assert.Throws(() => 63 | { 64 | // ReSharper disable once UnusedVariable 65 | var parameter = Context.Parameters.Single(); 66 | }); 67 | var md = Path.Combine(SourceDirectory, "NoArgumentsDetectedException.include.md"); 68 | File.Delete(md); 69 | await using var writer = File.CreateText(md); 70 | await writer.WriteLineAsync(); 71 | foreach (var line in exception.Message.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)) 72 | { 73 | await writer.WriteLineAsync($"> {line}"); 74 | } 75 | 76 | await writer.WriteLineAsync(); 77 | } 78 | 79 | [Theory] 80 | [MemberData(nameof(GetDataComplex))] 81 | public void MemberDataComplex(ComplexClass arg) 82 | { 83 | UseParameters(arg); 84 | var parameter = Context.Parameters.Single(); 85 | var parameterInfo = parameter.Info; 86 | Assert.Equal("arg", parameterInfo.Name); 87 | Assert.Equal(arg, parameter.Value); 88 | } 89 | 90 | public static IEnumerable GetDataComplex() 91 | { 92 | yield return [new ComplexClass("Value1")]; 93 | yield return [new ComplexClass("Value2")]; 94 | } 95 | } 96 | 97 | public class ComplexClass(string value) 98 | { 99 | public string Value { get; } = value; 100 | } -------------------------------------------------------------------------------- /src/Tests/SkipDispose.Dispose_should_flush.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | part1 part2 3 | ] -------------------------------------------------------------------------------- /src/Tests/SkipDispose.Write_after_dispose_should_throw.verified.txt: -------------------------------------------------------------------------------- 1 | { 2 | Type: Exception, 3 | Message: Context has been flushed. Could not write the string: Environment.NewLine, 4 | StackTrace: 5 | at Xunit.Context.ThrowIfFlushed(String logText) 6 | at Xunit.Context.WriteLine() 7 | at Xunit.XunitContextBase.WriteLine() 8 | at Xunit.Assert.RecordException(Action testCode) 9 | } -------------------------------------------------------------------------------- /src/Tests/SkipDispose.cs: -------------------------------------------------------------------------------- 1 | public class SkipDispose(ITestOutputHelper testOutput) : 2 | XunitContextBase(testOutput) 3 | { 4 | [Fact] 5 | public Task Dispose_should_flush() 6 | { 7 | Write("part1"); 8 | Write(" part2"); 9 | base.Dispose(); 10 | return Verify(Logs); 11 | } 12 | 13 | [Fact] 14 | public Task Write_after_dispose_should_throw() 15 | { 16 | base.Dispose(); 17 | var exception = Assert.Throws(WriteLine); 18 | 19 | return Verify(exception); 20 | } 21 | 22 | public override void Dispose() 23 | { 24 | } 25 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/ClassBeingTested.cs: -------------------------------------------------------------------------------- 1 | static class ClassBeingTested 2 | { 3 | public static void Method() 4 | { 5 | Trace.WriteLine("From Trace"); 6 | Console.WriteLine("From Console"); 7 | Debug.WriteLine("From Debug"); 8 | Console.Error.WriteLine("From Console Error"); 9 | } 10 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/ComplexParameterSample.cs: -------------------------------------------------------------------------------- 1 | public class ComplexParameterSample(ITestOutputHelper output) : 2 | XunitContextBase(output) 3 | { 4 | [Theory] 5 | [MemberData(nameof(GetData))] 6 | public void UseComplexMemberData(ComplexClass arg) 7 | { 8 | UseParameters(arg); 9 | var parameter = Context.Parameters.Single(); 10 | var parameterInfo = parameter.Info; 11 | Assert.Equal("arg", parameterInfo.Name); 12 | Assert.Equal(arg, parameter.Value); 13 | } 14 | 15 | public static IEnumerable GetData() 16 | { 17 | yield return [new ComplexClass("Value1")]; 18 | yield return [new ComplexClass("Value2")]; 19 | } 20 | 21 | public class ComplexClass(string value) 22 | { 23 | public string Value { get; } = value; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/ContextPushedDownSample.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedVariable 2 | 3 | public class ContextPushedDownSample(ITestOutputHelper output) : 4 | XunitContextBase(output) 5 | { 6 | [Fact] 7 | public void Usage() 8 | { 9 | WriteLine("Some message"); 10 | 11 | var currentLogMessages = Logs; 12 | 13 | var testOutputHelper = Output; 14 | 15 | var sourceFile = SourceFile; 16 | 17 | var sourceDirectory = SourceDirectory; 18 | 19 | var solutionDirectory = SolutionDirectory; 20 | 21 | var currentTestException = TestException; 22 | } 23 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/ContextSample.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedVariable 2 | 3 | public class ContextSample(ITestOutputHelper output) : 4 | XunitContextBase(output) 5 | { 6 | [Fact] 7 | public void Usage() 8 | { 9 | Context.WriteLine("Some message"); 10 | 11 | var currentLogMessages = Context.LogMessages; 12 | 13 | var testOutputHelper = Context.TestOutput; 14 | 15 | var currentTest = Context.Test; 16 | 17 | var sourceFile = Context.SourceFile; 18 | 19 | var sourceDirectory = Context.SourceDirectory; 20 | 21 | var solutionDirectory = Context.SolutionDirectory; 22 | 23 | var currentTestException = Context.TestException; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/ContextStaticSample.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedVariable 2 | 3 | public class ContextStaticSample(ITestOutputHelper output) : 4 | XunitContextBase(output) 5 | { 6 | [Fact] 7 | public void StaticUsage() 8 | { 9 | XunitContext.Context.WriteLine("Some message"); 10 | 11 | var currentLogMessages = XunitContext.Context.LogMessages; 12 | 13 | var testOutputHelper = XunitContext.Context.TestOutput; 14 | 15 | var currentTest = XunitContext.Context.Test; 16 | 17 | var sourceFile = XunitContext.Context.SourceFile; 18 | 19 | var sourceDirectory = XunitContext.Context.SourceDirectory; 20 | 21 | var solutionDirectory = XunitContext.Context.SolutionDirectory; 22 | 23 | var currentTestException = XunitContext.Context.TestException; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/CurrentTestSample.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedVariable 2 | 3 | public class CurrentTestSample(ITestOutputHelper output) : 4 | XunitContextBase(output) 5 | { 6 | [Fact] 7 | public void Usage() 8 | { 9 | var currentTest = Context.Test; 10 | // DisplayName will be 'CurrentTestSample.Usage' 11 | var displayName = currentTest.DisplayName; 12 | } 13 | 14 | [Fact] 15 | public void StaticUsage() 16 | { 17 | var currentTest = XunitContext.Context.Test; 18 | // DisplayName will be 'CurrentTestSample.StaticUsage' 19 | var displayName = currentTest.DisplayName; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/CustomBase.cs: -------------------------------------------------------------------------------- 1 | #region XunitContextCustomBase 2 | 3 | public class CustomBase( 4 | ITestOutputHelper testOutput, 5 | [CallerFilePath] string sourceFile = "") 6 | : 7 | XunitContextBase(testOutput, sourceFile); 8 | 9 | #endregion -------------------------------------------------------------------------------- /src/Tests/Snippets/FilterSample.cs: -------------------------------------------------------------------------------- 1 | public class FilterSample(ITestOutputHelper output) : 2 | XunitContextBase(output) 3 | { 4 | static FilterSample() => 5 | Filters.Add(_ => _ != null && !_.Contains("ignored")); 6 | 7 | [Fact] 8 | public void Write_lines() 9 | { 10 | WriteLine("first"); 11 | WriteLine("with ignored string"); 12 | WriteLine("last"); 13 | var logs = XunitContext.Logs; 14 | 15 | Assert.Contains("first", logs); 16 | Assert.DoesNotContain("with ignored string", logs); 17 | Assert.Contains("last", logs); 18 | } 19 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/FixtureSample.cs: -------------------------------------------------------------------------------- 1 | public class FixtureSample(ITestOutputHelper helper, ContextFixture ctxFixture) : 2 | IContextFixture 3 | { 4 | Context context = ctxFixture.Start(helper); 5 | 6 | [Fact] 7 | public void Usage() 8 | { 9 | Console.WriteLine("From Test"); 10 | Assert.Contains("From Test", context.LogMessages); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/ParametersSample.cs: -------------------------------------------------------------------------------- 1 | public class ParametersSample(ITestOutputHelper output) : 2 | XunitContextBase(output) 3 | { 4 | [Theory] 5 | [MemberData(nameof(GetData))] 6 | public void Usage(string arg) 7 | { 8 | var parameter = Context.Parameters.Single(); 9 | var parameterInfo = parameter.Info; 10 | Assert.Equal("arg", parameterInfo.Name); 11 | Assert.Equal(arg, parameter.Value); 12 | } 13 | 14 | public static IEnumerable GetData() 15 | { 16 | yield return ["Value1"]; 17 | yield return ["Value2"]; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/TestBaseSample.cs: -------------------------------------------------------------------------------- 1 | public class TestBaseSample(ITestOutputHelper output) : 2 | XunitContextBase(output) 3 | { 4 | [Fact] 5 | public void Write_lines() 6 | { 7 | WriteLine("From Test"); 8 | ClassBeingTested.Method(); 9 | 10 | var logs = XunitContext.Logs; 11 | 12 | Assert.Contains("From Test", logs); 13 | Assert.Contains("From Trace", logs); 14 | Assert.Contains("From Debug", logs); 15 | Assert.Contains("From Console", logs); 16 | Assert.Contains("From Console Error", logs); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/TestExceptionSample.cs: -------------------------------------------------------------------------------- 1 | #region TestExceptionSample 2 | 3 | // ReSharper disable UnusedVariable 4 | public static class GlobalSetup 5 | { 6 | [ModuleInitializer] 7 | public static void Setup() => 8 | XunitContext.EnableExceptionCapture(); 9 | } 10 | 11 | public class TestExceptionSample(ITestOutputHelper output) : 12 | XunitContextBase(output) 13 | { 14 | [Fact(Skip = "Will fail")] 15 | public void Usage() => 16 | //This tests will fail 17 | Assert.False(true); 18 | 19 | public override void Dispose() 20 | { 21 | var theExceptionThrownByTest = Context.TestException; 22 | var testDisplayName = Context.Test.DisplayName; 23 | var testCase = Context.Test.TestCase; 24 | base.Dispose(); 25 | } 26 | } 27 | 28 | #endregion -------------------------------------------------------------------------------- /src/Tests/Snippets/UniqueTestNameSample.cs: -------------------------------------------------------------------------------- 1 | public class UniqueTestNameSample(ITestOutputHelper output) : 2 | XunitContextBase(output) 3 | { 4 | [Fact] 5 | public void Usage() 6 | { 7 | var testName = Context.UniqueTestName; 8 | 9 | Context.WriteLine(testName); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Tests/Snippets/XunitLoggerSample.cs: -------------------------------------------------------------------------------- 1 | public class XunitLoggerSample : 2 | IDisposable 3 | { 4 | [Fact] 5 | public void Usage() 6 | { 7 | XunitContext.WriteLine("From Test"); 8 | 9 | ClassBeingTested.Method(); 10 | 11 | var logs = XunitContext.Logs; 12 | 13 | Assert.Contains("From Test", logs); 14 | Assert.Contains("From Trace", logs); 15 | Assert.Contains("From Debug", logs); 16 | Assert.Contains("From Console", logs); 17 | Assert.Contains("From Console Error", logs); 18 | } 19 | 20 | public XunitLoggerSample(ITestOutputHelper testOutput) => 21 | XunitContext.Register(testOutput); 22 | 23 | public void Dispose() => 24 | XunitContext.Flush(); 25 | } -------------------------------------------------------------------------------- /src/Tests/StaticConstructor.VerifyLogs.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | aFoo 3 | 4 | ] -------------------------------------------------------------------------------- /src/Tests/StaticConstructor.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable UnusedVariable 2 | 3 | public class StaticConstructor 4 | { 5 | static StaticConstructor() 6 | { 7 | var type = typeof(XunitContext); 8 | Console.Write("a"); 9 | Console.WriteLine("Foo"); 10 | } 11 | 12 | [Fact] 13 | public Task VerifyLogs() 14 | { 15 | Assert.EndsWith("StaticConstructor.cs", XunitContext.Context.SourceFile); 16 | var logs = XunitContext.Flush(false); 17 | return Verify(logs); 18 | } 19 | 20 | public StaticConstructor(ITestOutputHelper testOutput) => 21 | XunitContext.Register(testOutput); 22 | } -------------------------------------------------------------------------------- /src/Tests/TestContextWithNamespace.cs: -------------------------------------------------------------------------------- 1 | namespace TheNamespace; 2 | 3 | public class TestContextWithNamespace(ITestOutputHelper testOutput) : 4 | XunitContextBase(testOutput) 5 | { 6 | [Fact] 7 | public void CurrentTest() 8 | { 9 | Assert.Equal("TestContextWithNamespace", Context.ClassName); 10 | Assert.Equal("CurrentTest", Context.MethodName); 11 | Assert.EndsWith("TestContextWithNamespace.cs", Context.SourceFile); 12 | Assert.EndsWith("TestContextWithNamespace.CurrentTest", Context.UniqueTestName); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Tests/TestException_Async.cs: -------------------------------------------------------------------------------- 1 | // #if DEBUG 2 | // public class TestException_Async: 3 | // XunitContextBase 4 | // { 5 | // [Fact] 6 | // public async Task Root() 7 | // { 8 | // await Task.Delay(1); 9 | // throw new("root"); 10 | // } 11 | // 12 | // [Fact] 13 | // public async Task AssertThrows() 14 | // { 15 | // await Task.Delay(1); 16 | // Assert.Throws(MethodThatThrows); 17 | // } 18 | // 19 | // [Fact] 20 | // public async Task Caught() 21 | // { 22 | // await Task.Delay(1); 23 | // try 24 | // { 25 | // MethodThatThrows(); 26 | // } 27 | // catch 28 | // { 29 | // } 30 | // } 31 | // 32 | // [Fact] 33 | // public async Task FailedAssert() 34 | // { 35 | // await Task.Delay(1); 36 | // Assert.True(false); 37 | // } 38 | // 39 | // [Fact] 40 | // public async Task Nested() 41 | // { 42 | // await Task.Delay(1); 43 | // MethodThatThrows(); 44 | // } 45 | // 46 | // static void MethodThatThrows() => 47 | // throw new("nested"); 48 | // 49 | // public TestException_Async(ITestOutputHelper output) : 50 | // base(output) 51 | // { 52 | // } 53 | // 54 | // public override void Dispose() 55 | // { 56 | // var theExceptionThrownByTest = Context.TestException; 57 | // base.Dispose(); 58 | // } 59 | // } 60 | // #endif 61 | 62 | -------------------------------------------------------------------------------- /src/Tests/TestException_Sync.cs: -------------------------------------------------------------------------------- 1 | // #if DEBUG 2 | // public class TestException_Sync : 3 | // XunitContextBase 4 | // { 5 | // [Fact] 6 | // public void Root() => 7 | // throw new("root"); 8 | // 9 | // [Fact] 10 | // public void AssertThrows() => 11 | // Assert.Throws(MethodThatThrows); 12 | // 13 | // [Fact] 14 | // public void Caught() 15 | // { 16 | // try 17 | // { 18 | // MethodThatThrows(); 19 | // } 20 | // catch 21 | // { 22 | // } 23 | // } 24 | // 25 | // [Fact] 26 | // public void FailedAssert() => 27 | // Assert.True(false); 28 | // 29 | // [Fact] 30 | // public void Nested() => 31 | // MethodThatThrows(); 32 | // 33 | // static void MethodThatThrows() => 34 | // throw new("nested"); 35 | // 36 | // public TestException_Sync(ITestOutputHelper output) : 37 | // base(output) 38 | // { 39 | // } 40 | // 41 | // public override void Dispose() 42 | // { 43 | // var theExceptionThrownByTest = Context.TestException; 44 | // base.Dispose(); 45 | // } 46 | // } 47 | // #endif 48 | 49 | -------------------------------------------------------------------------------- /src/Tests/Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | DEBUG;TRACE 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Tests/UsingClassFixture.Write_lines.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | part1 part2, 3 | part3 4 | ] -------------------------------------------------------------------------------- /src/Tests/UsingClassFixture.cs: -------------------------------------------------------------------------------- 1 | public class UsingClassFixture(ITestOutputHelper helper, ContextFixture fixture) : 2 | IContextFixture 3 | { 4 | static UsingClassFixture() => 5 | Filters.Add(_ => _ != "ignored"); 6 | 7 | Context context = fixture.Start(helper); 8 | 9 | [Fact] 10 | public void CurrentTest() 11 | { 12 | Assert.Equal("UsingClassFixture", context.ClassName); 13 | Assert.Equal("CurrentTest", context.MethodName); 14 | Assert.EndsWith("UsingClassFixture.cs", context.SourceFile); 15 | Assert.True(File.Exists(context.SourceFile)); 16 | Assert.EndsWith("Tests", context.SourceDirectory); 17 | Assert.True(Directory.Exists(context.SourceDirectory)); 18 | Assert.EndsWith("src", context.SolutionDirectory); 19 | Assert.True(Directory.Exists(context.SolutionDirectory)); 20 | Assert.EndsWith("UsingClassFixture.CurrentTest", context.UniqueTestName); 21 | } 22 | 23 | [Fact] 24 | public Task Write_lines() 25 | { 26 | Debug.Write("part1"); 27 | Console.Write(" part2"); 28 | Console.WriteLine(); 29 | Console.WriteLine("part3"); 30 | Console.WriteLine("ignored"); 31 | return Verify(context.LogMessages); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Tests/UsingCurrentException.cs: -------------------------------------------------------------------------------- 1 | // #if DEBUG 2 | // 3 | // public class UsingCurrentException : 4 | // XunitContextBase 5 | // { 6 | // [Fact] 7 | // public void Fails() => 8 | // Assert.True(false); 9 | // 10 | // [Fact] 11 | // public void Passes() 12 | // { 13 | // } 14 | // 15 | // public UsingCurrentException(ITestOutputHelper testOutput) : 16 | // base(testOutput) 17 | // { 18 | // } 19 | // 20 | // public override void Dispose() 21 | // { 22 | // var contextTestException = Context.TestException; 23 | // var methodName = Context.Test.TestCase.TestMethod.Method.Name; 24 | // if (methodName == "Passes") 25 | // { 26 | // Assert.Null(contextTestException); 27 | // } 28 | // if (methodName == "Fails") 29 | // { 30 | // Assert.NotNull(contextTestException); 31 | // } 32 | // base.Dispose(); 33 | // } 34 | // } 35 | // 36 | // #endif 37 | 38 | -------------------------------------------------------------------------------- /src/Tests/UsingStatic.Async.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | part1, 3 | part2 4 | ] -------------------------------------------------------------------------------- /src/Tests/UsingStatic.Null.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | XunitLogger.WriteLine, 3 | , 4 | Console.WriteLine(null), 5 | , 6 | Debug.WriteLine(null), 7 | , 8 | Debug.Write(null), 9 | Trace.WriteLine(null), 10 | , 11 | Trace.Write(null) 12 | ] -------------------------------------------------------------------------------- /src/Tests/UsingStatic.Overwrites.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | from Console, 3 | from Debug, 4 | from Trace 5 | ] -------------------------------------------------------------------------------- /src/Tests/UsingStatic.Split_Lines.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | A, 3 | BC 4 | 5 | DE 6 | F, 7 | , 8 | , 9 | , 10 | , 11 | G, 12 | H, 13 | I, 14 | , 15 | J 16 | ] -------------------------------------------------------------------------------- /src/Tests/UsingStatic.Write_lines.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | part1 part2, 3 | part3 4 | ] -------------------------------------------------------------------------------- /src/Tests/UsingStatic.cs: -------------------------------------------------------------------------------- 1 | public class UsingStatic 2 | { 3 | [Fact] 4 | public Task Overwrites() 5 | { 6 | Console.WriteLine("from Console"); 7 | Debug.WriteLine("from Debug"); 8 | Trace.WriteLine("from Trace"); 9 | var logs = XunitContext.Flush(false); 10 | return Verify(logs); 11 | } 12 | 13 | //[Fact] 14 | //public void CurrentTest() 15 | //{ 16 | // Assert.Equal("CurrentTest", XunitContext.Context.Test.DisplayName); 17 | //} 18 | 19 | [Fact] 20 | public Task Null() 21 | { 22 | XunitContext.WriteLine("XunitLogger.WriteLine"); 23 | XunitContext.WriteLine(); 24 | XunitContext.WriteLine("Console.WriteLine(null)"); 25 | #pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. 26 | Console.WriteLine((string) null); 27 | #pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. 28 | XunitContext.WriteLine("Debug.WriteLine(null)"); 29 | Debug.WriteLine(null); 30 | XunitContext.WriteLine("Debug.Write(null)"); 31 | Debug.Write(null); 32 | XunitContext.WriteLine("Trace.WriteLine(null)"); 33 | Trace.WriteLine(null); 34 | XunitContext.WriteLine("Trace.Write(null)"); 35 | Trace.Write(null); 36 | var logs = XunitContext.Flush(false); 37 | return Verify(logs); 38 | } 39 | 40 | [Fact] 41 | public Task Write_lines() 42 | { 43 | XunitContext.Write("part1"); 44 | XunitContext.Write(" part2"); 45 | XunitContext.WriteLine(); 46 | XunitContext.WriteLine("part3"); 47 | var logs = XunitContext.Flush(false); 48 | return Verify(logs); 49 | } 50 | 51 | [Fact] 52 | public async Task Async() 53 | { 54 | await Task.Delay(1); 55 | XunitContext.WriteLine("part1"); 56 | await Task.Delay(1); 57 | XunitContext.WriteLine("part2"); 58 | await Task.Delay(1); 59 | var logs = XunitContext.Flush(false); 60 | await Verify(logs); 61 | } 62 | 63 | [Fact] 64 | public Task Split_Lines() 65 | { 66 | XunitContext.Write("A\nB"); 67 | XunitContext.Write("C\r\nD"); 68 | XunitContext.Write("E\rF\r"); 69 | XunitContext.Write("\n"); 70 | XunitContext.Write('\n'); 71 | XunitContext.Write('\n'); 72 | XunitContext.Write('\r'); 73 | XunitContext.Write('\n'); 74 | XunitContext.Write("\r\n"); 75 | XunitContext.Write("G\r\nH\nI"); 76 | XunitContext.Write("\n\r\n"); 77 | XunitContext.Write('J'); 78 | var logs = XunitContext.Flush(false); 79 | // Verifier normalises '\r' characters, so we need to manually detect embedded '\r' from above 80 | Assert.Equal("DE\rF", logs[2]); 81 | return Verify(logs); 82 | } 83 | 84 | public UsingStatic(ITestOutputHelper testOutput) => 85 | XunitContext.Register(testOutput); 86 | } -------------------------------------------------------------------------------- /src/Tests/UsingTestBase.Write_lines.verified.txt: -------------------------------------------------------------------------------- 1 | [ 2 | part1 part2, 3 | part3 4 | ] -------------------------------------------------------------------------------- /src/Tests/UsingTestBase.cs: -------------------------------------------------------------------------------- 1 | public class UsingTestBase(ITestOutputHelper testOutput) : 2 | XunitContextBase(testOutput) 3 | { 4 | static UsingTestBase() => 5 | Filters.Add(_ => _ != "ignored"); 6 | 7 | [Fact] 8 | public Task Write_lines() 9 | { 10 | Write("part1"); 11 | Write(" part2"); 12 | WriteLine(); 13 | WriteLine("part3"); 14 | WriteLine("ignored"); 15 | return Verify(Logs); 16 | } 17 | 18 | [Fact] 19 | public void CurrentTest() 20 | { 21 | Assert.Equal("UsingTestBase", Context.ClassName); 22 | Assert.Equal("CurrentTest", Context.MethodName); 23 | Assert.EndsWith("UsingTestBase.cs", Context.SourceFile); 24 | Assert.True(File.Exists(Context.SourceFile)); 25 | Assert.EndsWith("Tests", Context.SourceDirectory); 26 | Assert.True(Directory.Exists(Context.SourceDirectory)); 27 | Assert.EndsWith("src", Context.SolutionDirectory); 28 | Assert.True(Directory.Exists(Context.SolutionDirectory)); 29 | Assert.EndsWith("UsingTestBase.CurrentTest", Context.UniqueTestName); 30 | } 31 | } -------------------------------------------------------------------------------- /src/XunitContext.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29201.188 5 | MinimumVisualStudioVersion = 16.0.29201.188 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XunitContext", "XunitContext\XunitContext.csproj", "{C35E31DE-40A9-45C0-817B-41BE5B7D076E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests", "Tests\Tests.csproj", "{76FE87D9-EF6B-4D31-99F2-7099C719AE99}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A6C4741-C83D-4E05-A62F-8049F65B9B6C}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | appveyor.yml = appveyor.yml 14 | Directory.Build.props = Directory.Build.props 15 | Directory.Packages.props = Directory.Packages.props 16 | mdsnippets.json = mdsnippets.json 17 | ..\readme.md = ..\readme.md 18 | EndProjectSection 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {C35E31DE-40A9-45C0-817B-41BE5B7D076E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {C35E31DE-40A9-45C0-817B-41BE5B7D076E}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {C35E31DE-40A9-45C0-817B-41BE5B7D076E}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {C35E31DE-40A9-45C0-817B-41BE5B7D076E}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {76FE87D9-EF6B-4D31-99F2-7099C719AE99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {76FE87D9-EF6B-4D31-99F2-7099C719AE99}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {76FE87D9-EF6B-4D31-99F2-7099C719AE99}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {76FE87D9-EF6B-4D31-99F2-7099C719AE99}.Release|Any CPU.Build.0 = Release|Any CPU 34 | EndGlobalSection 35 | GlobalSection(SolutionProperties) = preSolution 36 | HideSolutionNode = FALSE 37 | EndGlobalSection 38 | GlobalSection(ExtensibilityGlobals) = postSolution 39 | SolutionGuid = {B0B94980-5385-4EC6-A0EE-C9A191DDFC05} 40 | EndGlobalSection 41 | EndGlobal 42 | -------------------------------------------------------------------------------- /src/XunitContext.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | ..\Shared.sln.DotSettings 3 | True 4 | True 5 | 1 6 | -------------------------------------------------------------------------------- /src/XunitContext/Context.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | public partial class Context 4 | { 5 | /// 6 | /// The current . 7 | /// 8 | public ITestOutputHelper? TestOutput { get; internal set; } 9 | 10 | /// 11 | /// The source file that the current test exists in. 12 | /// 13 | public string SourceFile { get; internal set; } = null!; 14 | 15 | /// 16 | /// The source directory that the current test exists in. 17 | /// 18 | public string SourceDirectory => Path.GetDirectoryName(SourceFile)!; 19 | 20 | string? solutionDirectory; 21 | 22 | /// 23 | /// The current solution directory. Obtained by walking up the directory tree from . 24 | /// 25 | public string SolutionDirectory => solutionDirectory ??= SolutionDirectoryFinder.Find(SourceDirectory); 26 | 27 | List logMessages = []; 28 | object locker = new(); 29 | 30 | /// 31 | /// All log message that have been written to the current . 32 | /// 33 | public IReadOnlyList LogMessages => logMessages; 34 | 35 | internal Exception? Exception; 36 | 37 | /// 38 | /// The for the current test if it failed. 39 | /// 40 | public Exception? TestException 41 | { 42 | get 43 | { 44 | if (!XunitContext.enableExceptionCapture) 45 | { 46 | throw new("XunitContext.EnableExceptionCapture() must be called inside a [ModuleInitializer] to use TestException."); 47 | } 48 | 49 | if (Exception == null) 50 | { 51 | return null; 52 | } 53 | 54 | if (Exception is XunitException) 55 | { 56 | return Exception; 57 | } 58 | 59 | var outerTrace = new StackTrace(Exception, false); 60 | var firstFrame = outerTrace.GetFrame(outerTrace.FrameCount - 1)!; 61 | var firstMethod = firstFrame.GetMethod()!; 62 | 63 | // firstMethod.DeclaringType can be null if the member was generated with reflection. 64 | var root = firstMethod.DeclaringType?.DeclaringType; 65 | if (root != null && root == typeof(ExceptionAggregator)) 66 | { 67 | if (Exception is TargetInvocationException targetInvocationException) 68 | { 69 | return targetInvocationException.InnerException; 70 | } 71 | 72 | return Exception; 73 | } 74 | 75 | return null; 76 | } 77 | } 78 | 79 | public StringBuilder? Builder; 80 | 81 | public void ThrowIfFlushed(string logText) 82 | { 83 | if (flushed) 84 | { 85 | throw new($"Context has been flushed. Could not write the string: {logText}"); 86 | } 87 | } 88 | 89 | internal bool flushed; 90 | 91 | internal Context(ITestOutputHelper testOutput, string sourceFile) 92 | { 93 | TestOutput = testOutput; 94 | SourceFile = sourceFile; 95 | } 96 | 97 | internal Context() 98 | { 99 | } 100 | 101 | void InitBuilder() => 102 | Builder ??= new(); 103 | 104 | /// 105 | /// Writes a value to the current . 106 | /// 107 | public void Write(object? value) 108 | { 109 | if (value == null) 110 | { 111 | return; 112 | } 113 | 114 | Write(value.ToString()); 115 | } 116 | 117 | /// 118 | /// Writes a string to the current . 119 | /// 120 | public void Write(string? value) 121 | { 122 | if (value == null) 123 | { 124 | return; 125 | } 126 | 127 | lock (locker) 128 | { 129 | ThrowIfFlushed(value); 130 | 131 | // Split on '\n' 132 | var start = 0; 133 | do 134 | { 135 | var pos = value.IndexOf('\n', start); 136 | if (pos < 0) 137 | { 138 | // No more '\n' characters. 139 | break; 140 | } 141 | 142 | if (pos < 1) 143 | { 144 | // Trim any trailing '\r' in Builder 145 | var end = (Builder?.Length ?? 0) - 1; 146 | if (end > -1 && Builder![Builder.Length - 1] == '\r') 147 | { 148 | Builder.Remove(end, 1); 149 | } 150 | } 151 | 152 | var count = (pos > start && value[pos - 1] == '\r' ? pos - 1 : pos) - start; 153 | 154 | WriteLine(count > 0 ? value.Substring(start, count) : string.Empty); 155 | start = pos + 1; 156 | } while (start < value.Length); 157 | 158 | if (start >= value.Length) 159 | { 160 | return; 161 | } 162 | 163 | InitBuilder(); 164 | Builder?.Append(start > 0 ? value[start..] : value); 165 | } 166 | } 167 | 168 | /// 169 | /// Writes a to the current . 170 | /// 171 | public void Write(char value) 172 | { 173 | lock (locker) 174 | { 175 | ThrowIfFlushed(value.ToString()); 176 | if (value == '\n') 177 | { 178 | // Trim any trailing '\r' 179 | var end = (Builder?.Length ?? 0) - 1; 180 | if (end > -1 && Builder![Builder.Length - 1] == '\r') 181 | { 182 | Builder.Remove(end, 1); 183 | } 184 | 185 | WriteLine(); 186 | return; 187 | } 188 | 189 | InitBuilder(); 190 | Builder?.Append(value); 191 | } 192 | } 193 | 194 | /// 195 | /// Writes a line to the current . 196 | /// 197 | public void WriteLine() 198 | { 199 | lock (locker) 200 | { 201 | ThrowIfFlushed("Environment.NewLine"); 202 | 203 | if (Builder == null && TestOutput == null) 204 | { 205 | Builder = new(); 206 | Builder.AppendLine(); 207 | logMessages.Add(string.Empty); 208 | return; 209 | } 210 | 211 | if (Builder != null && TestOutput != null) 212 | { 213 | var message = Builder.ToString(); 214 | Builder = null; 215 | if (Filters.ShouldFilterOut(message)) 216 | { 217 | return; 218 | } 219 | 220 | logMessages.Add(message); 221 | TestOutput.WriteLine(message); 222 | return; 223 | } 224 | 225 | if (Builder == null && TestOutput != null) 226 | { 227 | logMessages.Add(string.Empty); 228 | TestOutput.WriteLine(string.Empty); 229 | return; 230 | } 231 | 232 | if (Builder != null && TestOutput == null) 233 | { 234 | Builder.AppendLine(); 235 | } 236 | } 237 | } 238 | 239 | /// 240 | /// Writes a line to the current . 241 | /// 242 | public void WriteLine(object value) 243 | { 244 | Guard.AgainstNull(value, nameof(value)); 245 | WriteLine(value.ToString()); 246 | } 247 | 248 | /// 249 | /// Writes a line to the current . 250 | /// 251 | public void WriteLine(string? value) 252 | { 253 | if (value == null) 254 | { 255 | return; 256 | } 257 | 258 | lock (locker) 259 | { 260 | ThrowIfFlushed(value); 261 | 262 | if (Builder == null && TestOutput == null) 263 | { 264 | if (Filters.ShouldFilterOut(value)) 265 | { 266 | return; 267 | } 268 | 269 | Builder = new(); 270 | Builder.AppendLine(value); 271 | logMessages.Add(value); 272 | return; 273 | } 274 | 275 | if (Builder != null && TestOutput != null) 276 | { 277 | Builder.Append(value); 278 | var message = Builder.ToString(); 279 | Builder = null; 280 | if (Filters.ShouldFilterOut(message)) 281 | { 282 | return; 283 | } 284 | 285 | logMessages.Add(message); 286 | TestOutput.WriteLine(message); 287 | return; 288 | } 289 | 290 | if (Builder == null && TestOutput != null) 291 | { 292 | if (Filters.ShouldFilterOut(value)) 293 | { 294 | return; 295 | } 296 | 297 | logMessages.Add(value); 298 | TestOutput.WriteLine(value); 299 | return; 300 | } 301 | 302 | if (Builder != null && TestOutput == null) 303 | { 304 | Builder.AppendLine(value); 305 | } 306 | } 307 | } 308 | 309 | public void Flush() 310 | { 311 | lock (locker) 312 | { 313 | if (flushed) 314 | { 315 | return; 316 | } 317 | 318 | flushed = true; 319 | if (Builder == null) 320 | { 321 | return; 322 | } 323 | 324 | var message = Builder.ToString(); 325 | Builder = null; 326 | if (Filters.ShouldFilterOut(message)) 327 | { 328 | return; 329 | } 330 | 331 | logMessages.Add(message); 332 | if (TestOutput == null) 333 | { 334 | throw new("No ITestOutputHelper to flush to."); 335 | } 336 | 337 | TestOutput.WriteLine(message); 338 | } 339 | } 340 | } -------------------------------------------------------------------------------- /src/XunitContext/Context_CurrentTest.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | public partial class Context 4 | { 5 | ITest? test; 6 | 7 | 8 | public ITest Test 9 | { 10 | get 11 | { 12 | InitTest(); 13 | 14 | return test!; 15 | } 16 | } 17 | 18 | MethodInfo? methodInfo; 19 | 20 | public MethodInfo MethodInfo 21 | { 22 | get 23 | { 24 | InitTest(); 25 | return methodInfo!; 26 | } 27 | } 28 | 29 | Type? testType; 30 | 31 | public Type TestType 32 | { 33 | get 34 | { 35 | InitTest(); 36 | return testType!; 37 | } 38 | } 39 | 40 | void InitTest() 41 | { 42 | if (test != null) 43 | { 44 | return; 45 | } 46 | 47 | if (TestOutput == null) 48 | { 49 | throw new(MissingTestOutput); 50 | } 51 | 52 | #if NET8_0_OR_GREATER 53 | [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "test")] 54 | static extern ref ITest GetTest(TestOutputHelper? c); 55 | test = GetTest((TestOutputHelper) TestOutput); 56 | #else 57 | test = (ITest) GetTestMethod(TestOutput) 58 | .GetValue(TestOutput)!; 59 | #endif 60 | var method = (ReflectionMethodInfo) test.TestCase.TestMethod.Method; 61 | var type = (ReflectionTypeInfo) test.TestCase.TestMethod.TestClass.Class; 62 | methodInfo = method.MethodInfo; 63 | testType = type.Type; 64 | } 65 | 66 | public const string MissingTestOutput = "ITestOutputHelper has not been set. It is possible that the call to `XunitContext.Register()` is missing, or the current test does not inherit from `XunitContextBase`."; 67 | 68 | #if !NET8_0_OR_GREATER 69 | static FieldInfo? cachedTestMember; 70 | 71 | static FieldInfo GetTestMethod(ITestOutputHelper testOutput) 72 | { 73 | if (cachedTestMember != null) 74 | { 75 | return cachedTestMember; 76 | } 77 | 78 | var testOutputType = testOutput.GetType(); 79 | cachedTestMember = testOutputType.GetField("test", BindingFlags.Instance | BindingFlags.NonPublic); 80 | if (cachedTestMember == null) 81 | { 82 | throw new($"Unable to find 'test' field on {testOutputType.FullName}"); 83 | } 84 | 85 | return cachedTestMember; 86 | } 87 | #endif 88 | } -------------------------------------------------------------------------------- /src/XunitContext/Context_Parameters.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | public partial class Context 4 | { 5 | List? parameters; 6 | 7 | public IReadOnlyList Parameters => parameters ??= GetParameters(Test.TestCase); 8 | 9 | /// 10 | /// Override the default parameter resolution. 11 | /// 12 | public void UseParameters(params object[] parameters) 13 | { 14 | Guard.AgainstNull(parameters, nameof(parameters)); 15 | this.parameters = GetParameters(Test.TestCase, parameters); 16 | } 17 | 18 | static List empty = []; 19 | 20 | #region Parameters 21 | 22 | static List GetParameters(ITestCase testCase) => 23 | GetParameters(testCase, testCase.TestMethodArguments); 24 | 25 | static List GetParameters(ITestCase testCase, object[] arguments) 26 | { 27 | var method = testCase.TestMethod; 28 | var infos = method 29 | .Method.GetParameters() 30 | .ToList(); 31 | if (arguments == null || arguments.Length == 0) 32 | { 33 | if (infos.Count == 0) 34 | { 35 | return empty; 36 | } 37 | 38 | throw NewNoArgumentsDetectedException(); 39 | } 40 | 41 | List items = []; 42 | 43 | for (var index = 0; index < infos.Count; index++) 44 | { 45 | items.Add(new(infos[index], arguments[index])); 46 | } 47 | 48 | return items; 49 | } 50 | 51 | #endregion 52 | 53 | static Exception NewNoArgumentsDetectedException() => 54 | new(""" 55 | No arguments detected for method with parameters. 56 | This is most likely caused by using a parameter that Xunit cannot serialize. 57 | Instead pass in a simple type as a parameter and construct the complex object inside the test. 58 | Alternatively; override the current parameters using `UseParameters()` via the current test base class, or via `XunitContext.Current.UseParameters()`. 59 | """); 60 | } -------------------------------------------------------------------------------- /src/XunitContext/Context_TestName.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract 2 | 3 | namespace Xunit; 4 | 5 | public partial class Context 6 | { 7 | string? uniqueTestName; 8 | 9 | public string ClassName => Test.TestCase.TestMethod.TestClass.Class.ClassName(); 10 | 11 | public string MethodName => Test.TestCase.TestMethod.Method.Name; 12 | 13 | public string UniqueTestName 14 | { 15 | get 16 | { 17 | if (uniqueTestName == null) 18 | { 19 | uniqueTestName = GetUniqueTestName(Test.TestCase); 20 | } 21 | 22 | return uniqueTestName; 23 | } 24 | } 25 | 26 | #region UniqueTestName 27 | 28 | string GetUniqueTestName(ITestCase testCase) 29 | { 30 | var method = testCase.TestMethod; 31 | var name = $"{method.TestClass.Class.ClassName()}.{method.Method.Name}"; 32 | if (!Parameters.Any()) 33 | { 34 | return name; 35 | } 36 | 37 | var builder = new StringBuilder($"{name}_"); 38 | foreach (var parameter in Parameters) 39 | { 40 | builder.Append($"{parameter.Info.Name}="); 41 | builder.Append(string.Join(",", SplitParams(parameter.Value))); 42 | builder.Append('_'); 43 | } 44 | 45 | builder.Length -= 1; 46 | 47 | return builder.ToString(); 48 | } 49 | 50 | static IEnumerable SplitParams(object? parameter) 51 | { 52 | if (parameter == null) 53 | { 54 | yield return "null"; 55 | yield break; 56 | } 57 | 58 | if (parameter is string stringValue) 59 | { 60 | yield return stringValue; 61 | yield break; 62 | } 63 | 64 | if (parameter is IEnumerable enumerable) 65 | { 66 | foreach (var item in enumerable) 67 | { 68 | foreach (var sub in SplitParams(item)) 69 | { 70 | yield return sub; 71 | } 72 | } 73 | 74 | yield break; 75 | } 76 | 77 | var toString = parameter.ToString(); 78 | if (toString == null) 79 | { 80 | yield return "null"; 81 | } 82 | else 83 | { 84 | yield return toString; 85 | } 86 | } 87 | 88 | #endregion 89 | } -------------------------------------------------------------------------------- /src/XunitContext/DebugPoker.cs: -------------------------------------------------------------------------------- 1 | static class DebugPoker 2 | { 3 | public static void Overwrite(Action action) 4 | { 5 | var flags = BindingFlags.Static | BindingFlags.NonPublic; 6 | var writeCoreHook = typeof(Debug).GetField("s_WriteCore", flags); 7 | if (writeCoreHook != null) 8 | { 9 | writeCoreHook.SetValue(null, action); 10 | return; 11 | } 12 | 13 | var debugProviderType = Type.GetType("System.Diagnostics.DebugProvider"); 14 | if (debugProviderType != null) 15 | { 16 | writeCoreHook = debugProviderType.GetField("s_WriteCore", flags); 17 | if (writeCoreHook != null) 18 | { 19 | writeCoreHook.SetValue(null, action); 20 | return; 21 | } 22 | } 23 | 24 | throw new("Unable to find s_WriteCore field in either Debug nor DebugProvider. It is possible the current runtime is not supported."); 25 | } 26 | } -------------------------------------------------------------------------------- /src/XunitContext/Extensions.cs: -------------------------------------------------------------------------------- 1 | static class Extensions 2 | { 3 | public static string TrimTrailingNewline(this string value) => 4 | value[..^Environment.NewLine.Length]; 5 | 6 | public static MethodBase GetRealMethod(this MethodBase method) 7 | { 8 | var declaringType = method.DeclaringType!; 9 | if (!typeof(IAsyncStateMachine).IsAssignableFrom(declaringType)) 10 | { 11 | return method; 12 | } 13 | 14 | var realType = declaringType.DeclaringType!; 15 | foreach (var methodInfo in realType.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) 16 | { 17 | var stateMachineAttribute = methodInfo.GetCustomAttribute(); 18 | if (stateMachineAttribute == null) 19 | { 20 | continue; 21 | } 22 | 23 | if (stateMachineAttribute.StateMachineType == declaringType) 24 | { 25 | return methodInfo; 26 | } 27 | } 28 | 29 | return method; 30 | } 31 | 32 | public static string ClassName(this ITypeInfo value) 33 | { 34 | var name = value.Name; 35 | var indexOf = name.LastIndexOf('.'); 36 | if (indexOf == -1) 37 | { 38 | return name; 39 | } 40 | 41 | return name.Substring(indexOf + 1, name.Length - indexOf - 1); 42 | } 43 | } -------------------------------------------------------------------------------- /src/XunitContext/Filters.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | public static class Filters 4 | { 5 | static ConcurrentBag> items = []; 6 | 7 | public static void Add(Func filter) 8 | { 9 | Guard.AgainstNull(filter, nameof(filter)); 10 | items.Add(filter); 11 | } 12 | 13 | public static void Clear() 14 | { 15 | while (!items.IsEmpty) 16 | { 17 | items.TryTake(out _); 18 | } 19 | } 20 | 21 | internal static bool ShouldFilterOut(string? message) 22 | { 23 | foreach (var filter in items) 24 | { 25 | if (!filter(message)) 26 | { 27 | return true; 28 | } 29 | } 30 | 31 | return false; 32 | } 33 | } -------------------------------------------------------------------------------- /src/XunitContext/Fixture/ContextFixture.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | public class ContextFixture : 4 | IDisposable 5 | { 6 | Context? context; 7 | 8 | public Context Start(ITestOutputHelper outputHelper, [CallerFilePath] string sourceFile = "") => 9 | context = XunitContext.Register(outputHelper, sourceFile); 10 | 11 | public void Dispose() => 12 | context?.Flush(); 13 | } -------------------------------------------------------------------------------- /src/XunitContext/Fixture/IContextFixture.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | public interface IContextFixture : 4 | IClassFixture; -------------------------------------------------------------------------------- /src/XunitContext/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using System.Diagnostics.CodeAnalysis; 2 | global using Xunit; 3 | global using Xunit.Abstractions; 4 | global using Xunit.Sdk; 5 | global using CharSpan = System.ReadOnlySpan; -------------------------------------------------------------------------------- /src/XunitContext/Guard.cs: -------------------------------------------------------------------------------- 1 | static class Guard 2 | { 3 | // ReSharper disable UnusedParameter.Global 4 | public static void AgainstNull(object value, string argumentName) 5 | { 6 | if (value == null) 7 | { 8 | throw new ArgumentNullException(argumentName); 9 | } 10 | } 11 | 12 | public static void AgainstNullOrEmpty(string value, string argumentName) 13 | { 14 | if (string.IsNullOrWhiteSpace(value)) 15 | { 16 | throw new ArgumentNullException(argumentName); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/XunitContext/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | static class ModuleInit 2 | { 3 | [ModuleInitializer] 4 | public static void Initialize() => 5 | XunitContext.Init(); 6 | } -------------------------------------------------------------------------------- /src/XunitContext/Parameter.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | public class Parameter 4 | { 5 | public IParameterInfo Info { get; } 6 | public object? Value { get; } 7 | 8 | public Parameter(IParameterInfo info, object? value) 9 | { 10 | Guard.AgainstNull(info, nameof(info)); 11 | Info = info; 12 | Value = value; 13 | } 14 | } -------------------------------------------------------------------------------- /src/XunitContext/SolutionDirectoryFinder.cs: -------------------------------------------------------------------------------- 1 | static class SolutionDirectoryFinder 2 | { 3 | public static string Find(string testDirectory) 4 | { 5 | if (!TryFind(testDirectory, out var solutionDirectory)) 6 | { 7 | throw new("Could not find solution directory"); 8 | } 9 | 10 | return solutionDirectory; 11 | } 12 | 13 | public static bool TryFind(string testDirectory, [NotNullWhen(true)] out string? path) 14 | { 15 | var currentDirectory = testDirectory; 16 | do 17 | { 18 | if (Directory 19 | .GetFiles(currentDirectory, "*.sln") 20 | .Length != 0) 21 | { 22 | path = currentDirectory; 23 | return true; 24 | } 25 | 26 | var parent = Directory.GetParent(currentDirectory); 27 | if (parent == null) 28 | { 29 | path = null; 30 | return false; 31 | } 32 | 33 | currentDirectory = parent.FullName; 34 | } while (true); 35 | } 36 | } -------------------------------------------------------------------------------- /src/XunitContext/TestWriter.cs: -------------------------------------------------------------------------------- 1 | class TestWriter : 2 | TextWriter 3 | { 4 | public override Encoding Encoding { get; } = Encoding.UTF8; 5 | 6 | public override void Write(char value) => 7 | XunitContext.Write(value); 8 | 9 | public override void Write(string? value) => 10 | XunitContext.Write(value); 11 | 12 | public override void WriteLine() => 13 | XunitContext.WriteLine(); 14 | 15 | public override void WriteLine(string? value) 16 | { 17 | if (value == null) 18 | { 19 | XunitContext.WriteLine(); 20 | } 21 | else 22 | { 23 | XunitContext.WriteLine(value); 24 | } 25 | } 26 | 27 | public override Task WriteAsync(char value) 28 | { 29 | Write(value); 30 | return Task.CompletedTask; 31 | } 32 | 33 | public override Task WriteAsync(string? value) 34 | { 35 | Write(value); 36 | return Task.CompletedTask; 37 | } 38 | 39 | public override Task WriteLineAsync(string? value) 40 | { 41 | WriteLine(value); 42 | return Task.CompletedTask; 43 | } 44 | } -------------------------------------------------------------------------------- /src/XunitContext/TraceListener.cs: -------------------------------------------------------------------------------- 1 | class TraceListener : 2 | System.Diagnostics.TraceListener 3 | { 4 | public override void Write(string? value) 5 | { 6 | if (value != null) 7 | { 8 | XunitContext.Write(value); 9 | } 10 | } 11 | 12 | public override void WriteLine(string? value) 13 | { 14 | if (value == null) 15 | { 16 | XunitContext.WriteLine(); 17 | } 18 | else 19 | { 20 | XunitContext.WriteLine(value); 21 | } 22 | } 23 | 24 | public override bool IsThreadSafe => true; 25 | } -------------------------------------------------------------------------------- /src/XunitContext/XunitContext.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | public static class XunitContext 4 | { 5 | static AsyncLocal local = new(); 6 | internal static bool enableExceptionCapture; 7 | 8 | public static void EnableExceptionCapture() 9 | { 10 | if (enableExceptionCapture) 11 | { 12 | return; 13 | } 14 | 15 | enableExceptionCapture = true; 16 | AppDomain.CurrentDomain.FirstChanceException += (_, e) => 17 | { 18 | if (local.Value == null) 19 | { 20 | return; 21 | } 22 | 23 | if (local.Value.flushed) 24 | { 25 | return; 26 | } 27 | 28 | local.Value.Exception = e.Exception; 29 | }; 30 | } 31 | 32 | 33 | public static void Init() 34 | { 35 | var useGlobalLock = Trace.UseGlobalLock; 36 | Trace.UseGlobalLock = true; 37 | InnerInit(); 38 | Trace.UseGlobalLock = useGlobalLock; 39 | } 40 | 41 | static void InnerInit() 42 | { 43 | #region writeRedirects 44 | 45 | Trace.Listeners.Clear(); 46 | Trace.Listeners.Add(new TraceListener()); 47 | #if (NETFRAMEWORK) 48 | Debug.Listeners.Clear(); 49 | Debug.Listeners.Add(new TraceListener()); 50 | #else 51 | DebugPoker.Overwrite( 52 | text => 53 | { 54 | if (string.IsNullOrEmpty(text)) 55 | { 56 | return; 57 | } 58 | 59 | if (text.EndsWith(Environment.NewLine)) 60 | { 61 | WriteLine(text.TrimTrailingNewline()); 62 | return; 63 | } 64 | 65 | Write(text); 66 | }); 67 | #endif 68 | TestWriter writer = new(); 69 | Console.SetOut(writer); 70 | Console.SetError(writer); 71 | 72 | #endregion 73 | } 74 | 75 | public static void Write(string? value) => 76 | Context.Write(value); 77 | 78 | public static void Write(object value) => 79 | Context.Write(value); 80 | 81 | public static IReadOnlyList Logs 82 | { 83 | get 84 | { 85 | var context = local.Value; 86 | if (context == null) 87 | { 88 | throw new("No current context."); 89 | } 90 | 91 | return context.LogMessages; 92 | } 93 | } 94 | 95 | public static void Write(char value) => 96 | Context.Write(value); 97 | 98 | public static void WriteLine() => 99 | Context.WriteLine(); 100 | 101 | public static void WriteLine(string value) => 102 | Context.WriteLine(value); 103 | 104 | public static void WriteLine(object value) => 105 | Context.WriteLine(value); 106 | 107 | public static IReadOnlyList Flush(bool clearAsyncLocal = true) 108 | { 109 | var context = local.Value; 110 | if (context == null) 111 | { 112 | throw new("No context to flush."); 113 | } 114 | 115 | context.Flush(); 116 | var messages = context.LogMessages; 117 | if (clearAsyncLocal) 118 | { 119 | local.Value = null; 120 | } 121 | 122 | return messages; 123 | } 124 | 125 | public static Context Context 126 | { 127 | get 128 | { 129 | var context = local.Value; 130 | if (context != null) 131 | { 132 | return context; 133 | } 134 | 135 | context = new(); 136 | local.Value = context; 137 | return context; 138 | } 139 | } 140 | 141 | public static Context Register( 142 | ITestOutputHelper output, 143 | [CallerFilePath] string sourceFile = "") 144 | { 145 | Guard.AgainstNull(output, nameof(output)); 146 | Guard.AgainstNullOrEmpty(sourceFile, nameof(sourceFile)); 147 | var existingContext = local.Value; 148 | 149 | if (existingContext == null) 150 | { 151 | Context context = new(output, sourceFile); 152 | local.Value = context; 153 | return context; 154 | } 155 | 156 | if (existingContext.TestOutput != null) 157 | { 158 | throw new($"A ITestOutputHelper has already been registered. Existing SourceFile: {existingContext.SourceFile}"); 159 | } 160 | 161 | existingContext.TestOutput = output; 162 | existingContext.SourceFile = sourceFile; 163 | return existingContext; 164 | } 165 | } -------------------------------------------------------------------------------- /src/XunitContext/XunitContext.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net472;net5.0;net6.0;net7.0;net8.0 4 | Context, Logging, xUnit 5 | Extends xUnit to expose extra context and simplify logging. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/XunitContext/XunitContextBase.cs: -------------------------------------------------------------------------------- 1 | namespace Xunit; 2 | 3 | public abstract class XunitContextBase : 4 | IDisposable 5 | { 6 | /// 7 | /// The current . 8 | /// 9 | public ITestOutputHelper Output { get; } 10 | 11 | public Context Context { get; } 12 | 13 | protected XunitContextBase( 14 | ITestOutputHelper output, 15 | [CallerFilePath] string sourceFile = "") 16 | { 17 | Guard.AgainstNull(output, nameof(output)); 18 | Guard.AgainstNullOrEmpty(sourceFile, nameof(sourceFile)); 19 | 20 | Output = output; 21 | Context = XunitContext.Register(output, sourceFile); 22 | } 23 | 24 | /// 25 | /// Writes a value to the current . 26 | /// 27 | public void WriteLine(object value) => 28 | Context.WriteLine(value); 29 | 30 | /// 31 | /// Writes a line to the current . 32 | /// 33 | public void WriteLine(string value) => 34 | Context.WriteLine(value); 35 | 36 | /// 37 | /// Writes a to the current . 38 | /// 39 | public void Write(char value) => 40 | Context.Write(value); 41 | 42 | /// 43 | /// Writes a line to the current . 44 | /// 45 | public void WriteLine() => 46 | Context.WriteLine(); 47 | 48 | /// 49 | /// Writes a value to the current . 50 | /// 51 | public void Write(object value) => 52 | Context.Write(value); 53 | 54 | /// 55 | /// Writes a string to the current . 56 | /// 57 | public void Write(string value) => 58 | Context.Write(value); 59 | 60 | /// 61 | /// All log message that have been written to the current . 62 | /// 63 | public IReadOnlyList Logs => Context.LogMessages; 64 | 65 | public virtual void Dispose() => 66 | Context.Flush(); 67 | 68 | /// 69 | /// The for the current test if it failed. 70 | /// 71 | public Exception? TestException => Context.TestException; 72 | 73 | /// 74 | /// The source file that the current test exists in. 75 | /// 76 | public string SourceFile => Context.SourceFile; 77 | 78 | /// 79 | /// The source directory that the current test exists in. 80 | /// 81 | public string SourceDirectory => Context.SourceDirectory; 82 | 83 | /// 84 | /// The current solution directory. Obtained by walking up the directory tree from . 85 | /// 86 | public string SolutionDirectory => Context.SolutionDirectory; 87 | 88 | public string UniqueTestName => Context.UniqueTestName; 89 | 90 | /// 91 | /// Override the default parameter resolution. 92 | /// 93 | public void UseParameters(params object[] parameters) 94 | { 95 | Guard.AgainstNull(parameters, nameof(parameters)); 96 | Context.UseParameters(parameters); 97 | } 98 | } -------------------------------------------------------------------------------- /src/XunitContext/build/XunitContext.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/XunitContext/build/XunitContext.targets: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(BuildDependsOn); 5 | Validate_NetFx_472_Or_Higher 6 | 7 | 8 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/appveyor.yml: -------------------------------------------------------------------------------- 1 | image: Visual Studio 2022 2 | environment: 3 | DOTNET_NOLOGO: true 4 | DOTNET_CLI_TELEMETRY_OPTOUT: true 5 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 6 | build_script: 7 | - pwsh: | 8 | Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./dotnet-install.ps1" 9 | ./dotnet-install.ps1 -JSonFile src/global.json -Architecture x64 -InstallDir 'C:\Program Files\dotnet' 10 | - dotnet build src --configuration Release 11 | - dotnet test src --configuration Release --no-build --no-restore --filter Category!=Integration 12 | test: off 13 | on_failure: 14 | - ps: Get-ChildItem *.received.* -recurse | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name } 15 | artifacts: 16 | - path: nugets\*.nupkg -------------------------------------------------------------------------------- /src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "9.0.301", 4 | "allowPrerelease": true, 5 | "rollForward": "latestFeature" 6 | } 7 | } -------------------------------------------------------------------------------- /src/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonCropp/XunitContext/bc551e5add1a590daf961665bc5e87aa29db8b54/src/icon.png -------------------------------------------------------------------------------- /src/key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SimonCropp/XunitContext/bc551e5add1a590daf961665bc5e87aa29db8b54/src/key.snk -------------------------------------------------------------------------------- /src/mdsnippets.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/SimonCropp/MarkdownSnippets/master/schema.json", 3 | "TocExcludes": [ "NuGet package", "Release Notes", "Icon" ], 4 | "MaxWidth": 80, 5 | "ValidateContent": true, 6 | "Convention": "InPlaceOverwrite" 7 | } -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 20 | 21 | 24 | 25 | 26 | --------------------------------------------------------------------------------