diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index d86752265b..18b528665f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,14 +3,14 @@ "isRoot": true, "tools": { "microsoft.templateengine.authoring.cli": { - "version": "10.0.100", + "version": "10.0.101", "commands": [ "dotnet-template-authoring" ], "rollForward": false }, "verify.tool": { - "version": "0.6.0", + "version": "0.7.0", "commands": [ "dotnet-verify" ], diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2ddfe817a4..e2fd24a4a7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -34,7 +34,7 @@ If you then still feel the need to ask a question and need clarification, we rec - Open an [Issue](https://github.com/thomhurst/TUnit/issues/new). - Provide as much context as you can about what you're running into. -- Provide project and platform versions (nodejs, npm, etc), depending on what seems relevant. +- Provide project and platform versions (.NET SDK, TUnit version, OS), depending on what seems relevant. We will then take care of the issue as soon as possible. @@ -50,7 +50,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform - Make sure that you are using the latest version. - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://tunit.dev/). If you are looking for support, you might want to check [this section](#i-have-a-question)). -- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/thomhurst/TUnitissues?q=label%3Abug). +- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/thomhurst/TUnit/issues?q=label%3Abug). - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. - Collect information about the bug: - Stack trace (Traceback) @@ -72,7 +72,7 @@ Once it's filed: - The project team will label the issue accordingly. - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. -- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). +- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be implemented. ### Suggesting Enhancements @@ -103,3 +103,31 @@ The documentation is generated from files within this repository, so you can for The relevant files are at `docs > docs > tutorials-[basics|extras|assertions]` If want to provide sample code for complicated or useful different test suite set-ups, that's also very welcome, as this can help other users get started a lot quicker! + +### Code Contributions + +When contributing code to TUnit, please keep these important requirements in mind: + +#### Dual-Mode Implementation +TUnit supports both source-generated and reflection-based test discovery. **All changes that affect test discovery or execution must work identically in both modes:** +- Source Generator path: `TUnit.Core.SourceGenerator` +- Reflection path: `TUnit.Engine` + +#### Snapshot Testing +If your changes affect the source generator output or public APIs: +1. Run the relevant tests: `dotnet test TUnit.Core.SourceGenerator.Tests` or `dotnet test TUnit.PublicAPI` +2. Review any `.received.txt` files generated +3. If the changes are intentional, rename them to `.verified.txt` +4. Commit the `.verified.txt` files with your changes + +#### Performance Considerations +TUnit is designed to handle millions of tests. When contributing: +- Minimize allocations in hot paths (test discovery, execution) +- Avoid LINQ in performance-critical code +- Cache reflection results +- Use `ValueTask` for potentially-sync operations + +#### AOT Compatibility +All code must work with Native AOT and IL trimming. Add appropriate `[DynamicallyAccessedMembers]` annotations when using reflection. + +For detailed development guidelines, see the [CLAUDE.md](https://github.com/thomhurst/TUnit/blob/main/CLAUDE.md) file in the repository root. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 9866b562e7..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,9 +0,0 @@ -### Please check the following before raising an issue - -- If this is a question, have you checked the documentation at https://tunit.dev ? - - If you can't find anything there, start a discussion instead at https://github.com/thomhurst/TUnit/discussions -- If your issue is with a specific piece of functionality, please include code that reproduces the problem. This will make fixes much quicker. -- Ensure you've checked any existing issues so you're not creating a duplicate. If you find an existing issue, feel free to contribute on that thread, and provide any more context if you can. -- Is your issue with an IDE such as Rider or Visual Studio? I generally can't fix these, so issues should be reported directly to those teams. E.g. Jetbrains or Microsoft. -- Do you want a new feature or functionality? Start a discussion first so we can decide together. -- Have you read the [Contributing Guidelines](../tree/main/.github/CONTRIBUTING.md) ? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000..8c6896d7b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,136 @@ +name: Bug Report +description: Report a bug or unexpected behavior in TUnit +title: "[Bug]: " +labels: ["bug", "needs-triage"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug! Please fill out the information below to help us investigate. + + **Before submitting**, please check: + - [ ] I've searched [existing issues](https://github.com/thomhurst/TUnit/issues) to ensure this isn't a duplicate + - [ ] I've read the [documentation](https://tunit.dev/) and this isn't expected behavior + - [ ] I'm using the latest version of TUnit + + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of what the bug is. + placeholder: Describe the bug... + validations: + required: true + + - type: textarea + id: expected + attributes: + label: Expected Behavior + description: What did you expect to happen? + placeholder: I expected... + validations: + required: true + + - type: textarea + id: actual + attributes: + label: Actual Behavior + description: What actually happened? + placeholder: Instead, what happened was... + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Steps to Reproduce + description: | + Please provide a minimal code example that reproduces the issue. + Include test code and any relevant configuration. + placeholder: | + 1. Create a test class with... + 2. Run the test using... + 3. Observe that... + + ```csharp + [Test] + public async Task MyTest() + { + // Minimal reproduction code + } + ``` + validations: + required: true + + - type: input + id: tunit-version + attributes: + label: TUnit Version + description: What version of TUnit are you using? + placeholder: "e.g., 0.15.0" + validations: + required: true + + - type: input + id: dotnet-version + attributes: + label: .NET Version + description: What .NET version are you targeting? + placeholder: "e.g., .NET 8.0, .NET 9.0" + validations: + required: true + + - type: dropdown + id: os + attributes: + label: Operating System + description: What operating system are you using? + options: + - Windows + - macOS + - Linux + - Other (please specify in additional context) + validations: + required: true + + - type: dropdown + id: ide + attributes: + label: IDE / Test Runner + description: How are you running the tests? + options: + - dotnet CLI (dotnet test / dotnet run) + - Visual Studio + - JetBrains Rider + - VS Code + - Other (please specify in additional context) + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Error Output / Stack Trace + description: | + If applicable, include the error message or stack trace. + This will be automatically formatted as code. + render: shell + + - type: textarea + id: additional + attributes: + label: Additional Context + description: | + Add any other context about the problem here. + - Are you using AOT/trimming? + - Any custom configuration? + - Does this happen with specific test data? + + - type: checkboxes + id: ide-issue + attributes: + label: IDE-Specific Issue? + description: | + If this issue only occurs in a specific IDE (not via `dotnet test`), it may need to be reported to that IDE's team instead. + options: + - label: I've confirmed this issue occurs when running via `dotnet test` or `dotnet run`, not just in my IDE diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..ad0b44e7b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Questions & Help + url: https://github.com/thomhurst/TUnit/discussions/categories/q-a + about: Ask questions and get help from the community + - name: Ideas & Feature Discussion + url: https://github.com/thomhurst/TUnit/discussions/categories/ideas + about: Discuss new features before creating a formal request + - name: Documentation + url: https://tunit.dev/ + about: Check the official documentation for guides and API reference diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000..3d07c58d7f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,102 @@ +name: Feature Request +description: Suggest a new feature or enhancement for TUnit +title: "[Feature]: " +labels: ["enhancement", "needs-triage"] +body: + - type: markdown + attributes: + value: | + Thanks for suggesting a feature! We appreciate your input in making TUnit better. + + **Before submitting**, please: + - [ ] Search [existing issues](https://github.com/thomhurst/TUnit/issues) and [discussions](https://github.com/thomhurst/TUnit/discussions) to ensure this hasn't been suggested before + - [ ] Check the [documentation](https://tunit.dev/) to confirm this feature doesn't already exist + - [ ] Consider starting a [discussion](https://github.com/thomhurst/TUnit/discussions/new?category=ideas) first for larger features to get community feedback + + - type: textarea + id: problem + attributes: + label: Problem Statement + description: | + Is your feature request related to a problem? Please describe. + A clear description of what problem this feature would solve. + placeholder: "I'm frustrated when... / It would be helpful if... / Currently I have to..." + validations: + required: true + + - type: textarea + id: solution + attributes: + label: Proposed Solution + description: | + Describe the solution you'd like. + Include example code/syntax if applicable. + placeholder: | + I would like TUnit to... + + Example usage: + ```csharp + [Test] + [ProposedAttribute("value")] + public async Task MyTest() + { + // How I envision using this feature + } + ``` + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: | + Have you considered any alternative solutions or workarounds? + What do you currently do instead? + placeholder: "I've tried... / Currently I work around this by..." + + - type: dropdown + id: category + attributes: + label: Feature Category + description: What area of TUnit does this feature relate to? + options: + - Test Discovery / Attributes + - Test Execution / Lifecycle + - Assertions + - Data-Driven Testing (Arguments, DataSources) + - Parallel Execution + - Test Output / Reporting + - IDE Integration + - Performance + - Documentation + - Other + validations: + required: true + + - type: dropdown + id: priority + attributes: + label: How important is this feature to you? + description: Help us prioritize by indicating the impact on your workflow. + options: + - Nice to have - would improve my experience + - Important - significantly impacts my workflow + - Critical - blocking my adoption/usage of TUnit + + - type: textarea + id: additional + attributes: + label: Additional Context + description: | + Add any other context, screenshots, or examples about the feature request. + - Links to similar features in other test frameworks + - Use cases that would benefit from this feature + + - type: checkboxes + id: contribution + attributes: + label: Contribution + description: Would you be interested in contributing this feature? + options: + - label: I'm willing to submit a pull request for this feature diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 48206b5edf..c315d096ec 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,57 @@ -### Please check the following before creating a Pull Request +## Description -- If this is a new feature or piece of functionality, have you started a discussion and gotten agreement on it? -- If it fixes a bug or problem, is there an issue to track it? If not, create one first and link it please so there's clear visibility. -- Did you write tests to ensure you code works properly? \ No newline at end of file + + +## Related Issue + + + +Fixes # + +## Type of Change + + + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update +- [ ] Performance improvement +- [ ] Refactoring (no functional changes) + +## Checklist + +### Required + +- [ ] I have read the [Contributing Guidelines](https://github.com/thomhurst/TUnit/blob/main/.github/CONTRIBUTING.md) +- [ ] If this is a new feature, I started a [discussion](https://github.com/thomhurst/TUnit/discussions) first and received agreement +- [ ] My code follows the project's code style (modern C# syntax, proper naming conventions) +- [ ] I have written tests that prove my fix is effective or my feature works + +### TUnit-Specific Requirements + + + +- [ ] **Dual-Mode Implementation**: If this change affects test discovery/execution, I have implemented it in BOTH: + - [ ] Source Generator path (`TUnit.Core.SourceGenerator`) + - [ ] Reflection path (`TUnit.Engine`) +- [ ] **Snapshot Tests**: If I changed source generator output or public APIs: + - [ ] I ran `TUnit.Core.SourceGenerator.Tests` and/or `TUnit.PublicAPI` tests + - [ ] I reviewed the `.received.txt` files and accepted them as `.verified.txt` + - [ ] I committed the updated `.verified.txt` files +- [ ] **Performance**: If this change affects hot paths (test discovery, execution, assertions): + - [ ] I minimized allocations and avoided LINQ in hot paths + - [ ] I cached reflection results where appropriate +- [ ] **AOT Compatibility**: If this change uses reflection: + - [ ] I added appropriate `[DynamicallyAccessedMembers]` annotations + - [ ] I verified the change works with `dotnet publish -p:PublishAot=true` + +### Testing + +- [ ] All existing tests pass (`dotnet test`) +- [ ] I have added tests that cover my changes +- [ ] I have tested both source-generated and reflection modes (if applicable) + +## Additional Notes + + diff --git a/.github/release.yml b/.github/release.yml index 7b6ae04ec9..649761babd 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -2,18 +2,46 @@ changelog: exclude: labels: - ignore-for-release + - duplicate + - invalid + - wontfix categories: - - title: Breaking Changes 🛠 + - title: Breaking Changes labels: - Semver-Major - breaking-change - breaking - - title: 🏕 Changes + - title: New Features + labels: + - enhancement + - feature + - title: Bug Fixes + labels: + - bug + - fix + - title: Performance Improvements + labels: + - performance + - title: Documentation + labels: + - documentation + - docs + - title: Other Changes labels: - '*' exclude: labels: - dependencies - - title: 👒 Dependencies + - enhancement + - feature + - bug + - fix + - performance + - documentation + - docs + - Semver-Major + - breaking-change + - breaking + - title: Dependencies labels: - - dependencies \ No newline at end of file + - dependencies diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index dc3652b5b1..8452b0f2ff 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -17,62 +17,41 @@ jobs: # github.event.pull_request.user.login == 'external-contributor' || # github.event.pull_request.user.login == 'new-developer' || # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' - + runs-on: ubuntu-latest permissions: contents: read pull-requests: read issues: read id-token: write - + steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v4 with: fetch-depth: 1 - name: Run Claude Code Review id: claude-review - uses: anthropics/claude-code-action@beta + uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + REPO: ${{ github.repository }} + PR NUMBER: ${{ github.event.pull_request.number }} - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" - - # Direct prompt for automated review (no @claude mention needed) - direct_prompt: | Please review this pull request and provide feedback on: - Code quality and best practices - Potential bugs or issues - Performance considerations - Security concerns - Test coverage - - Be constructive and helpful in your feedback. - # Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR - # use_sticky_comment: true - - # Optional: Customize review based on file types - # direct_prompt: | - # Review this PR focusing on: - # - For TypeScript files: Type safety and proper interface usage - # - For API endpoints: Security, input validation, and error handling - # - For React components: Performance, accessibility, and best practices - # - For tests: Coverage, edge cases, and test quality - - # Optional: Different prompts for different authors - # direct_prompt: | - # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && - # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || - # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} - - # Optional: Add specific tools for running tests or linting - # allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)" - - # Optional: Skip review for certain conditions - # if: | - # !contains(github.event.pull_request.title, '[skip-review]') && - # !contains(github.event.pull_request.title, '[WIP]') + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 6de0bdfc3e..d300267f18 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -26,39 +26,25 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v4 with: fetch-depth: 1 - name: Run Claude Code id: claude - uses: anthropics/claude-code-action@beta + uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # This is an optional setting that allows Claude to read CI results on PRs additional_permissions: | actions: read - - # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) - # model: "claude-opus-4-20250514" - - # Optional: Customize the trigger phrase (default: @claude) - # trigger_phrase: "/claude" - - # Optional: Trigger when specific user is assigned to an issue - # assignee_trigger: "claude-bot" - - # Optional: Allow Claude to run specific commands - allowed_tools: "Bash(pwsh run-all-engine-tests.ps1)" - - # Optional: Add custom instructions for Claude to customize its behavior for your project - # custom_instructions: | - # Follow our coding standards - # Ensure all new code has tests - # Use TypeScript for new files - - # Optional: Custom environment variables for Claude - # claude_env: | - # NODE_ENV: test + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4dde9743e2..3d2840bc6d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -55,7 +55,7 @@ jobs: # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Setup .NET 9 uses: actions/setup-dotnet@v5 diff --git a/.github/workflows/deploy-pages-test.yml b/.github/workflows/deploy-pages-test.yml index 261a136ea7..f7dddc272e 100644 --- a/.github/workflows/deploy-pages-test.yml +++ b/.github/workflows/deploy-pages-test.yml @@ -13,7 +13,7 @@ jobs: run: working-directory: docs steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-node@v6 diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index e41288cdef..2040177ff8 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -16,7 +16,7 @@ jobs: run: working-directory: docs steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 - uses: actions/setup-node@v6 diff --git a/.github/workflows/dotnet-build-different-locale.yml b/.github/workflows/dotnet-build-different-locale.yml index fcb846d76d..21309e9d00 100644 --- a/.github/workflows/dotnet-build-different-locale.yml +++ b/.github/workflows/dotnet-build-different-locale.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 60785d933d..1550c66730 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -26,7 +26,7 @@ jobs: runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -54,7 +54,7 @@ jobs: dotnet-version: 10.0.x - name: Cache NuGet packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: | ~/.nuget/packages @@ -66,10 +66,10 @@ jobs: - name: Docker Setup Docker if: matrix.os == 'ubuntu-latest' - uses: docker/setup-docker-action@v4.5.0 + uses: docker/setup-docker-action@v4.6.0 - name: Cache Playwright Browsers - uses: actions/cache@v4 + uses: actions/cache@v5 id: playwright-cache with: path: | @@ -106,21 +106,21 @@ jobs: publish-packages: ${{ (github.event.inputs.publish-packages || false) && matrix.os == 'ubuntu-latest' }} - name: Upload Diagnostic Logs - uses: actions/upload-artifact@v5.0.0 + uses: actions/upload-artifact@v6.0.0 if: always() with: name: TestingPlatformDiagnosticLogs${{matrix.os}} path: '**/log_*.diag' - name: Upload Hang Dumps - uses: actions/upload-artifact@v5.0.0 + uses: actions/upload-artifact@v6.0.0 if: always() with: name: HangDump${{matrix.os}} path: '**/hangdump*' - name: NuGet Packages Artifacts - uses: actions/upload-artifact@v5.0.0 + uses: actions/upload-artifact@v6.0.0 if: always() with: name: 'NuGetPackages-${{matrix.os}}' diff --git a/.github/workflows/generate-readme.yml b/.github/workflows/generate-readme.yml index 9b06bcf623..f7a36d093f 100644 --- a/.github/workflows/generate-readme.yml +++ b/.github/workflows/generate-readme.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 diff --git a/.github/workflows/speed-comparison.yml b/.github/workflows/speed-comparison.yml index 27c762a3e4..eaa23914da 100644 --- a/.github/workflows/speed-comparison.yml +++ b/.github/workflows/speed-comparison.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -34,7 +34,7 @@ jobs: working-directory: "tools/speed-comparison/UnifiedTests" - name: Upload Build Artifacts - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: test-builds-ubuntu path: tools/speed-comparison/UnifiedTests/bin/ @@ -53,7 +53,7 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -63,7 +63,7 @@ jobs: dotnet-version: 10.0.x - name: Download Build Artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: name: test-builds-ubuntu path: tools/speed-comparison/UnifiedTests/bin/ @@ -81,7 +81,7 @@ jobs: CLASS_NAME: ${{ matrix.class }} - name: Upload Markdown - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: ubuntu_markdown_run_time_${{ matrix.class }} @@ -97,7 +97,7 @@ jobs: cancel-in-progress: true steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 @@ -116,7 +116,7 @@ jobs: working-directory: "tools/speed-comparison/Tests.Benchmark" - name: Upload Markdown - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: ubuntu_markdown_build_time @@ -132,20 +132,20 @@ jobs: pull-requests: write steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 0 token: ${{ secrets.ADMIN_TOKEN }} - name: Download All Runtime Benchmark Artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: path: benchmark-results/runtime/ pattern: ubuntu_markdown_run_time_* merge-multiple: false - name: Download Build Time Benchmark Artifacts - uses: actions/download-artifact@v6 + uses: actions/download-artifact@v7 with: path: benchmark-results/build/ pattern: ubuntu_markdown_build_time @@ -161,7 +161,7 @@ jobs: node .github/scripts/process-benchmarks.js - name: Upload Individual Runtime Benchmarks - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: benchmark-DataDrivenTests @@ -171,7 +171,7 @@ jobs: retention-days: 90 - name: Upload Individual Runtime Benchmarks - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: benchmark-AsyncTests @@ -181,7 +181,7 @@ jobs: retention-days: 90 - name: Upload Individual Runtime Benchmarks - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: benchmark-ScaleTests @@ -191,7 +191,7 @@ jobs: retention-days: 90 - name: Upload Individual Runtime Benchmarks - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: benchmark-MatrixTests @@ -201,7 +201,7 @@ jobs: retention-days: 90 - name: Upload Individual Runtime Benchmarks - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: benchmark-MassiveParallelTests @@ -211,7 +211,7 @@ jobs: retention-days: 90 - name: Upload Individual Runtime Benchmarks - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: benchmark-SetupTeardownTests @@ -221,7 +221,7 @@ jobs: retention-days: 90 - name: Upload Build Benchmark - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: benchmark-BuildTime @@ -231,7 +231,7 @@ jobs: retention-days: 90 - name: Upload Summary Files - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: benchmark-summary @@ -278,7 +278,7 @@ jobs: - name: Create Pull Request if: steps.check_changes.outputs.has_changes == 'true' id: create_pr - uses: peter-evans/create-pull-request@v7 + uses: peter-evans/create-pull-request@v8 with: token: ${{ secrets.ADMIN_TOKEN }} commit-message: 'chore: update benchmark results' diff --git a/.gitignore b/.gitignore index 0e3c8cfe18..149ec04978 100644 --- a/.gitignore +++ b/.gitignore @@ -419,8 +419,8 @@ TUnit.TestProject/TestSession*.txt .mcp.json requirements -TESTPROJECT_AOT -TESTPROJECT_SINGLEFILE +TESTPROJECT_AOT* +TESTPROJECT_SINGLEFILE* nul diff --git a/.idea/.idea.TUnit/.idea/copilot.data.migration.ask.xml b/.idea/.idea.TUnit/.idea/copilot.data.migration.ask.xml deleted file mode 100644 index 7ef04e2ea0..0000000000 --- a/.idea/.idea.TUnit/.idea/copilot.data.migration.ask.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.TUnit/.idea/copilot.data.migration.ask2agent.xml b/.idea/.idea.TUnit/.idea/copilot.data.migration.ask2agent.xml deleted file mode 100644 index 1f2ea11e7f..0000000000 --- a/.idea/.idea.TUnit/.idea/copilot.data.migration.ask2agent.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.TUnit/.idea/indexLayout.xml b/.idea/.idea.TUnit/.idea/indexLayout.xml deleted file mode 100644 index 7b08163ceb..0000000000 --- a/.idea/.idea.TUnit/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.serena/.gitignore b/.serena/.gitignore deleted file mode 100644 index 14d86ad623..0000000000 --- a/.serena/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/cache diff --git a/.serena/cache/csharp/document_symbols_cache_v20-05-25.pkl b/.serena/cache/csharp/document_symbols_cache_v20-05-25.pkl deleted file mode 100644 index 091ba63e6d..0000000000 Binary files a/.serena/cache/csharp/document_symbols_cache_v20-05-25.pkl and /dev/null differ diff --git a/.serena/cache/csharp/document_symbols_cache_v23-06-25.pkl b/.serena/cache/csharp/document_symbols_cache_v23-06-25.pkl deleted file mode 100644 index 1116471997..0000000000 Binary files a/.serena/cache/csharp/document_symbols_cache_v23-06-25.pkl and /dev/null differ diff --git a/.serena/memories/code_style_conventions.md b/.serena/memories/code_style_conventions.md deleted file mode 100644 index f8715af5f1..0000000000 --- a/.serena/memories/code_style_conventions.md +++ /dev/null @@ -1,30 +0,0 @@ -# TUnit Code Style and Conventions - -## General Guidelines -- **Self-Descriptive Code**: Write self-descriptive code instead of adding redundant comments -- **Clean Architecture**: Maintain separation between source generation (data only) and runtime (logic) -- **Async Best Practices**: Never use `.Result`, `.Wait()`, or `GetAwaiter().GetResult()` - always use proper async/await - -## Async Guidelines (Critical) -- **Never Block on Async**: Avoid `GetAwaiter().GetResult()`, `.Result`, or `.Wait()` to prevent deadlocks -- **Async All the Way**: When in sync context needing async, refactor method signatures to be async -- **Embrace Async**: TUnit supports async throughout the stack - use it properly -- **Proper Cancellation**: Use CancellationToken properly for long-running operations - -## Architecture Patterns -- **Source Generators**: Should only emit data structures (TestMetadata), never execution logic -- **Runtime Phase**: TestBuilder handles complex logic (data expansion, tuple unwrapping, etc.) -- **Performance Focus**: Use expression compilation and caching in TestBuilder -- **Extension Points**: Support custom executors, data sources, hooks, and assertions - -## Project Structure -- Core logic in `TUnit.Core` -- Source generation in `TUnit.Core.SourceGenerator` -- Test execution in `TUnit.Engine` -- Assertions in `TUnit.Assertions` -- Tests in `TUnit.UnitTests` (framework) and `TUnit.TestProject` (integration) - -## Testing Conventions -- Use TUnit for framework testing -- Add analyzer rules when adding new features -- Ensure Microsoft.Testing.Platform compatibility \ No newline at end of file diff --git a/.serena/memories/project_overview.md b/.serena/memories/project_overview.md deleted file mode 100644 index 01500708de..0000000000 --- a/.serena/memories/project_overview.md +++ /dev/null @@ -1,31 +0,0 @@ -# TUnit Project Overview - -## Purpose -TUnit is a modern, source-generated testing framework (with a reflection-based fallback) for .NET that provides: -- Compile-time test discovery through source generators -- Parallel test execution by default -- Native AOT and trimming support -- Built on Microsoft.Testing.Platform -- Fluent assertion library -- Rich extensibility model - -## Tech Stack -- **Language**: C# -- **Target Frameworks**: .NET 8.0 and .NET 9.0 -- **SDK Requirement**: .NET SDK 9.0.301 or later (see global.json) -- **Architecture**: Source generation + runtime execution engine -- **Testing Platform**: Microsoft.Testing.Platform integration - -## Core Components -1. **TUnit.Core**: Core abstractions and attributes -2. **TUnit.Core.SourceGenerator**: Source generators for compile-time test discovery -3. **TUnit.Engine**: Test execution engine with simplified architecture -4. **TUnit.Assertions**: Fluent assertion library with async support -5. **Extension Projects**: Playwright, F#, Analyzers, Templates - -## Key Features -- **Clean Architecture**: Separation between source generation (data) and runtime (logic) -- **Parallel-First**: Tests run in parallel by default with smart scheduling -- **Async Support**: All public APIs support async operations -- **Extensible**: Custom executors, data sources, hooks, and assertions -- **Dual Mode**: TUnit supports Source Generated mode and Reflection Mode, and both should maintain feature parity, with the exception of the source generated code path MUST support Native AOT and trimming diff --git a/.serena/memories/suggested_commands.md b/.serena/memories/suggested_commands.md deleted file mode 100644 index fc9781b920..0000000000 --- a/.serena/memories/suggested_commands.md +++ /dev/null @@ -1,52 +0,0 @@ -# Suggested Commands for TUnit Development - -## Build Commands -```bash -# Debug build -dotnet build - -# Release build -dotnet build -c Release - -# Clean build artifacts -./clean.ps1 - -# Build NuGet packages -dotnet pack -c Release -``` - -## Test Execution Commands -```bash -# Run tests in a project (3 equivalent ways) -cd [TestProjectDirectory] -dotnet run -c Release -dotnet test -c Release -dotnet exec bin/Release/net8.0/TestProject.dll - -# Test options -dotnet run -- --list-tests # List all tests -dotnet run -- --fail-fast # Stop on first failure -dotnet run -- --maximum-parallel-tests 10 # Control parallelism -dotnet run -- --report-trx --coverage # Generate reports -dotnet run -- --treenode-filter "/*/*/*/TestName" # Run specific test by exact name -dotnet run -- --treenode-filter/*/*/*PartialName*/* # Filter tests by partial name pattern -``` - -## Development Commands -```bash -# Run full pipeline -dotnet run --project TUnit.Pipeline/TUnit.Pipeline.csproj - -# Documentation site -cd docs -npm install # First time only -npm start # Run locally at localhost:3000 -npm run build # Build static site -``` - -## System Commands (Linux) -- `ls` - List directory contents -- `find` - Search for files/directories -- `grep` - Search text in files -- `git` - Version control operations -- `cd` - Change directory diff --git a/.serena/memories/task_completion_checklist.md b/.serena/memories/task_completion_checklist.md deleted file mode 100644 index 99dcdb05be..0000000000 --- a/.serena/memories/task_completion_checklist.md +++ /dev/null @@ -1,41 +0,0 @@ -# Task Completion Checklist - -When completing a coding task in TUnit: - -## Code Quality -- [ ] Follow async best practices (no `.Result`, `.Wait()`, `GetAwaiter().GetResult()`) -- [ ] Write self-descriptive code without redundant comments -- [ ] Maintain clean architecture separation (source gen vs runtime) -- [ ] Use proper error handling and logging -- [ ] Do not over-engineer -- [ ] Follow C# best practices, as well as principles such as DRY, KISS, SRP and SOLID - -## Testing -- [ ] Run relevant tests: `dotnet run -c Release` in test project directory -- [ ] Add/update unit tests in `TUnit.UnitTests` for framework changes -- [ ] Add/update integration tests in `TUnit.TestProject` for end-to-end scenarios and add the [EngineTest(ExpectedResult.Pass)] attribute so the pipeline knows to run these tests and they must pass -- [ ] Verify no hanging processes or infinite loops - -## Architecture Compliance -- [ ] Source generators only emit data, not execution logic -- [ ] Runtime components handle all complex logic -- [ ] Maintain Microsoft.Testing.Platform compatibility -- [ ] Ensure proper async support throughout -- [ ] Ensure feature parity between source generation mode and reflection mode - -## Performance & Reliability -- [ ] Check for potential deadlocks or blocking operations -- [ ] Verify proper resource disposal -- [ ] Test cancellation token handling -- [ ] Validate timeout mechanisms work correctly -- [ ] Ensure code is performant - -## Documentation (if applicable) -- [ ] Update relevant documentation in `docs/` if behavior changes -- [ ] If building a new feature, add new documentation in `docs/` in the relevant location with clear, easy to read language and code examples -- [ ] Update CLAUDE.md if architectural patterns change - -## Final Verification -- [ ] Build solution: `dotnet build -c Release` -- [ ] Run full test suite -- [ ] Check for memory leaks or hanging processes diff --git a/.serena/project.yml b/.serena/project.yml deleted file mode 100644 index 5a63a94911..0000000000 --- a/.serena/project.yml +++ /dev/null @@ -1,66 +0,0 @@ -# language of the project (csharp, python, rust, java, typescript, javascript, go, cpp, or ruby) -# Special requirements: -# * csharp: Requires the presence of a .sln file in the project folder. -language: csharp - -# whether to use the project's gitignore file to ignore files -# Added on 2025-04-07 -ignore_all_files_in_gitignore: true -# list of additional paths to ignore -# same syntax as gitignore, so you can use * and ** -# Was previously called `ignored_dirs`, please update your config if you are using that. -# Added (renamed)on 2025-04-07 -ignored_paths: [] - -# whether the project is in read-only mode -# If set to true, all editing tools will be disabled and attempts to use them will result in an error -# Added on 2025-04-18 -read_only: false - - -# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details. -# Below is the complete list of tools for convenience. -# To make sure you have the latest list of tools, and to view their descriptions, -# execute `uv run scripts/print_tool_overview.py`. -# -# * `activate_project`: Activates a project by name. -# * `check_onboarding_performed`: Checks whether project onboarding was already performed. -# * `create_text_file`: Creates/overwrites a file in the project directory. -# * `delete_lines`: Deletes a range of lines within a file. -# * `delete_memory`: Deletes a memory from Serena's project-specific memory store. -# * `execute_shell_command`: Executes a shell command. -# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced. -# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type). -# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type). -# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. -# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file or directory. -# * `initial_instructions`: Gets the initial instructions for the current project. -# Should only be used in settings where the system prompt cannot be set, -# e.g. in clients you have no control over, like Claude Desktop. -# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. -# * `insert_at_line`: Inserts content at a given line in a file. -# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. -# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). -# * `list_memories`: Lists memories in Serena's project-specific memory store. -# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). -# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context). -# * `read_file`: Reads a file within the project directory. -# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store. -# * `remove_project`: Removes a project from the Serena configuration. -# * `replace_lines`: Replaces a range of lines within a file with new content. -# * `replace_symbol_body`: Replaces the full definition of a symbol. -# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen. -# * `search_for_pattern`: Performs a search for a pattern in the project. -# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase. -# * `switch_modes`: Activates modes by providing a list of their names -# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information. -# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task. -# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed. -# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store. -excluded_tools: [] - -# initial prompt for the project. It will always be given to the LLM upon activating the project -# (contrary to the memories, which are loaded on demand). -initial_prompt: "" - -project_name: "TUnit" diff --git a/CLAUDE.md b/CLAUDE.md index 1ed9227dd1..925f3571f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -147,6 +147,17 @@ dotnet test TUnit.Core.SourceGenerator.Tests **Rule**: Only run TUnit.TestProject with explicit `--treenode-filter` to target specific tests or classes. +**IMPORTANT: Run filters ONE AT A TIME!** Using OR patterns (`Pattern1|Pattern2`) can match thousands of unintended tests. Always run one specific filter per command: + +```bash +# ❌ WRONG - OR patterns can match too broadly +--treenode-filter "/*/*/ClassA/*|/*/*/ClassB/*" + +# ✅ CORRECT - Run separate commands for each class +dotnet run -- --treenode-filter "/*/*/ClassA/*" +dotnet run -- --treenode-filter "/*/*/ClassB/*" +``` + --- ### Most Common Commands diff --git a/Directory.Packages.props b/Directory.Packages.props index cc33abe666..11f44f281e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,18 +7,18 @@ - - + + - + - - - + + + - - - + + + @@ -32,18 +32,18 @@ - - + + - - + + - + @@ -52,15 +52,15 @@ - + - + - - - + + + @@ -73,26 +73,26 @@ - + - - - + + + - - - - - - - + + + + + + + - - - + + + \ No newline at end of file diff --git a/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs b/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs index 3683793e41..e28299d735 100644 --- a/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs +++ b/TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier.cs @@ -5,6 +5,7 @@ using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Text; namespace TUnit.Analyzers.Tests.Verifiers; @@ -16,6 +17,12 @@ public class Test : CSharpCodeFixTest { var project = solution.GetProject(projectId); diff --git a/TUnit.Analyzers.Tests/Verifiers/LineEndingNormalizingVerifier.cs b/TUnit.Analyzers.Tests/Verifiers/LineEndingNormalizingVerifier.cs index e12d6722ca..cecbb774c4 100644 --- a/TUnit.Analyzers.Tests/Verifiers/LineEndingNormalizingVerifier.cs +++ b/TUnit.Analyzers.Tests/Verifiers/LineEndingNormalizingVerifier.cs @@ -60,7 +60,17 @@ public void NotEmpty(string collectionName, IEnumerable collection) public void SequenceEqual(IEnumerable expected, IEnumerable actual, IEqualityComparer? equalityComparer = null, string? message = null) { - _defaultVerifier.SequenceEqual(expected, actual, equalityComparer, message); + // Normalize line endings for string sequence comparisons + if (typeof(T) == typeof(string)) + { + var normalizedExpected = expected.Cast().Select(NormalizeLineEndings).Cast(); + var normalizedActual = actual.Cast().Select(NormalizeLineEndings).Cast(); + _defaultVerifier.SequenceEqual(normalizedExpected, normalizedActual, equalityComparer, message); + } + else + { + _defaultVerifier.SequenceEqual(expected, actual, equalityComparer, message); + } } public IVerifier PushContext(string context) diff --git a/TUnit.Assertions.FSharp/TUnit.Assertions.FSharp.fsproj b/TUnit.Assertions.FSharp/TUnit.Assertions.FSharp.fsproj index 7eeceee31c..2c94708a04 100644 --- a/TUnit.Assertions.FSharp/TUnit.Assertions.FSharp.fsproj +++ b/TUnit.Assertions.FSharp/TUnit.Assertions.FSharp.fsproj @@ -6,6 +6,7 @@ + diff --git a/TUnit.Assertions.FSharp/TaskAssert.fs b/TUnit.Assertions.FSharp/TaskAssert.fs new file mode 100644 index 0000000000..3689e47d70 --- /dev/null +++ b/TUnit.Assertions.FSharp/TaskAssert.fs @@ -0,0 +1,15 @@ +module TUnit.Assertions.FSharp.TaskAssert + +open TUnit.Assertions.Core + +[] +module TaskAssertBuilder = + let taskAssert = task + +[] +module TaskAssertCEExtensions = + type TaskBuilderBase with + #nowarn "FS1204" + member inline x.Bind(assertion: IAssertion, continuation: Unit -> TaskCode<'TOverall, 'TResult2>) : TaskCode<'TOverall, 'TResult2> = + let task = assertion.AssertAsync() + x.Bind(task, continuation) diff --git a/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs b/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs index 1d44b17e48..0c4c88137f 100644 --- a/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs +++ b/TUnit.Assertions.Tests/AssertConditions/BecauseTests.cs @@ -1,4 +1,4 @@ -namespace TUnit.Assertions.Tests.AssertConditions; +namespace TUnit.Assertions.Tests.AssertConditions; public class BecauseTests { @@ -68,7 +68,7 @@ at Assert.That(variable).IsFalse() }; var exception = await Assert.ThrowsAsync(action); - await Assert.That(exception.Message).IsEqualTo(expectedMessage); + await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } [Test] @@ -91,7 +91,7 @@ await Assert.That(variable).IsTrue().Because(because) }; var exception = await Assert.ThrowsAsync(action); - await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); + await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage.NormalizeLineEndings()); } [Test] diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs index c3292ec78e..35d744499e 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExactlyTests.cs @@ -13,15 +13,15 @@ public async Task Fails_For_Code_With_Other_Exceptions() but threw TUnit.Assertions.Tests.Assertions.Delegates.Throws+OtherException at Assert.That(action).ThrowsExactly() - """; + """.NormalizeLineEndings(); Exception exception = CreateOtherException(); Action action = () => throw exception; var sut = async () => await Assert.That(action).ThrowsExactly(); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] @@ -32,15 +32,15 @@ public async Task Fails_For_Code_With_Subtype_Exceptions() but wrong exception type: SubCustomException instead of exactly CustomException at Assert.That(action).ThrowsExactly() - """; + """.NormalizeLineEndings(); Exception exception = CreateSubCustomException(); Action action = () => throw exception; var sut = async () => await Assert.That(action).ThrowsExactly(); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] @@ -51,14 +51,14 @@ public async Task Fails_For_Code_Without_Exceptions() but no exception was thrown at Assert.That(action).ThrowsExactly() - """; + """.NormalizeLineEndings(); var action = () => { }; var sut = async () => await Assert.That(action).ThrowsExactly(); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] @@ -117,10 +117,11 @@ public async Task Conversion_To_Value_Assertion_Builder_On_Casted_Exception_Type await Assert.That((object)ex).IsAssignableTo(); }); - await Assert.That(assertionException).HasMessageStartingWith(""" - Expected to throw exactly Exception - but wrong exception type: CustomException instead of exactly Exception - """); + var expectedPrefix = """ + Expected to throw exactly Exception + but wrong exception type: CustomException instead of exactly Exception + """.NormalizeLineEndings(); + await Assert.That(assertionException.Message.NormalizeLineEndings()).StartsWith(expectedPrefix); } [Test] diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExceptionTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExceptionTests.cs index cedfa0b176..3b51f1e29e 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExceptionTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.ExceptionTests.cs @@ -14,14 +14,14 @@ public async Task Fails_For_Code_Without_Exceptions() but no exception was thrown at Assert.That(action).ThrowsException() - """; + """.NormalizeLineEndings(); var action = () => { }; var sut = async () => await Assert.That(action).ThrowsException(); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.NothingTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.NothingTests.cs index 48077a848d..29fc9e7d33 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.NothingTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.NothingTests.cs @@ -12,15 +12,15 @@ public async Task Fails_For_Code_With_Exceptions() but threw TUnit.Assertions.Tests.Assertions.Delegates.Throws+CustomException: {nameof(Fails_For_Code_With_Exceptions)} at Assert.That(action).ThrowsNothing() - """; + """.NormalizeLineEndings(); Exception exception = CreateCustomException(); Action action = () => throw exception; var sut = async () => await Assert.That(action).ThrowsNothing(); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.OfTypeTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.OfTypeTests.cs index a900ceaff2..ef4c853221 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.OfTypeTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.OfTypeTests.cs @@ -13,15 +13,15 @@ public async Task Fails_For_Code_With_Other_Exceptions() but threw TUnit.Assertions.Tests.Assertions.Delegates.Throws+OtherException at Assert.That(action).Throws() - """; + """.NormalizeLineEndings(); Exception exception = CreateOtherException(); Action action = () => throw exception; var sut = async () => await Assert.That(action).Throws(); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] @@ -32,15 +32,15 @@ public async Task Fails_For_Code_With_Supertype_Exceptions() but threw TUnit.Assertions.Tests.Assertions.Delegates.Throws+CustomException at Assert.That(action).Throws() - """; + """.NormalizeLineEndings(); Exception exception = CreateCustomException(); Action action = () => throw exception; var sut = async () => await Assert.That(action).Throws(); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] @@ -51,14 +51,14 @@ public async Task Fails_For_Code_Without_Exceptions() but no exception was thrown at Assert.That(action).Throws() - """; + """.NormalizeLineEndings(); var action = () => { }; var sut = async () => await Assert.That(action).Throws(); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionTests.cs index 88386844cd..2b167014c5 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithInnerExceptionTests.cs @@ -15,7 +15,7 @@ Expected exception message to equal "bar" but exception message was "some different inner message" at Assert.That(action).ThrowsException().WithInnerException().WithMessage("bar") - """; + """.NormalizeLineEndings(); Exception exception = CreateCustomException(outerMessage, CreateCustomException("some different inner message")); Action action = () => throw exception; @@ -24,8 +24,8 @@ at Assert.That(action).ThrowsException().WithInnerException().WithMessage("bar") => await Assert.That(action).ThrowsException() .WithInnerException().WithMessage(expectedInnerMessage); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithMessageMatchingTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithMessageMatchingTests.cs index 8401907110..0d8e0b37ff 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithMessageMatchingTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithMessageMatchingTests.cs @@ -42,15 +42,15 @@ Expected exception message to match pattern "bar" but exception message "foo" does not match pattern "bar" at Assert.That(action).ThrowsExactly().WithMessageMatching("bar") - """; + """.NormalizeLineEndings(); Exception exception = CreateCustomException(message1); Action action = () => throw exception; var sut = async () => await Assert.That(action).ThrowsExactly().WithMessageMatching(message2); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithMessageTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithMessageTests.cs index d444dc3372..293d109d1e 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithMessageTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithMessageTests.cs @@ -15,15 +15,15 @@ Expected exception message to equal "bar" but exception message was "foo" at Assert.That(action).ThrowsExactly().WithMessage("bar") - """; + """.NormalizeLineEndings(); Exception exception = CreateCustomException(message1); Action action = () => throw exception; var sut = async () => await Assert.That(action).ThrowsExactly().WithMessage(message2); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] diff --git a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithParameterNameTests.cs b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithParameterNameTests.cs index 41c2ca56c0..70c0c658f9 100644 --- a/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithParameterNameTests.cs +++ b/TUnit.Assertions.Tests/Assertions/Delegates/Throws.WithParameterNameTests.cs @@ -15,15 +15,15 @@ public async Task Fails_For_Different_Parameter_Name() but ArgumentException parameter name was "foo" at Assert.That(action).ThrowsExactly().WithParameterName("bar") - """; + """.NormalizeLineEndings(); ArgumentException exception = new(string.Empty, paramName1); Action action = () => throw exception; var sut = async () => await Assert.That(action).ThrowsExactly().WithParameterName(paramName2); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var thrownException = await Assert.That(sut).ThrowsException(); + await Assert.That(thrownException.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] diff --git a/TUnit.Assertions.Tests/Bugs/Tests2117.cs b/TUnit.Assertions.Tests/Bugs/Tests2117.cs index df5fcad7cb..6fe04798e3 100644 --- a/TUnit.Assertions.Tests/Bugs/Tests2117.cs +++ b/TUnit.Assertions.Tests/Bugs/Tests2117.cs @@ -28,12 +28,13 @@ at Assert.That(a).IsEquivalentTo(b) """)] public async Task IsEquivalent_Fail(int[] a, int[] b, CollectionOrdering? collectionOrdering, string expectedError) { - await Assert.That(async () => + var exception = await Assert.That(async () => await (collectionOrdering is null ? Assert.That(a).IsEquivalentTo(b) : Assert.That(a).IsEquivalentTo(b, collectionOrdering.Value)) - ).Throws() - .WithMessage(expectedError); + ).Throws(); + + await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedError.NormalizeLineEndings()); } [Test] @@ -60,11 +61,12 @@ at Assert.That(a).IsNotEquivalentTo(b) """)] public async Task IsNotEquivalent_Fail(int[] a, int[] b, CollectionOrdering? collectionOrdering, string expectedError) { - await Assert.That(async () => + var exception = await Assert.That(async () => await (collectionOrdering is null ? Assert.That(a).IsNotEquivalentTo(b) : Assert.That(a).IsNotEquivalentTo(b, collectionOrdering.Value)) - ).Throws() - .WithMessage(expectedError); + ).Throws(); + + await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedError.NormalizeLineEndings()); } } diff --git a/TUnit.Assertions.Tests/CollectionAssertionTests.cs b/TUnit.Assertions.Tests/CollectionAssertionTests.cs new file mode 100644 index 0000000000..ffb9a9b92d --- /dev/null +++ b/TUnit.Assertions.Tests/CollectionAssertionTests.cs @@ -0,0 +1,273 @@ +using TUnit.Assertions.Extensions; + +namespace TUnit.Assertions.Tests; + +public class CollectionAssertionTests +{ + [Test] + public async Task IsEmpty() + { + var items = new List(); + + await Assert.That(items).IsEmpty(); + } + + [Test] + public async Task IsEmpty2() + { + var items = new List(); + + await Assert.That(() => items).IsEmpty(); + } + + [Test] + public async Task Count() + { + var items = new List(); + + await Assert.That(items).Count().IsEqualTo(0); + } + + [Test] + public async Task Count2() + { + var items = new List(); + + await Assert.That(() => items).Count().IsEqualTo(0); + } + + [Test] + public async Task Count_WithInnerAssertion_IsGreaterThan() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items where item > 2 using inner assertion builder + await Assert.That(items).Count(item => item.IsGreaterThan(2)).IsEqualTo(3); + } + + [Test] + public async Task Count_WithInnerAssertion_IsLessThan() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items where item < 4 using inner assertion builder + await Assert.That(items).Count(item => item.IsLessThan(4)).IsEqualTo(3); + } + + [Test] + public async Task Count_WithInnerAssertion_IsBetween() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items where 2 <= item <= 4 using inner assertion builder + await Assert.That(items).Count(item => item.IsBetween(2, 4)).IsEqualTo(3); + } + + [Test] + public async Task Count_WithInnerAssertion_String_Contains() + { + var items = new List { "apple", "banana", "apricot", "cherry" }; + + // Count items that contain "ap" using inner assertion builder + await Assert.That(items).Count(item => item.Contains("ap")).IsEqualTo(2); + } + + [Test] + public async Task Count_WithInnerAssertion_String_StartsWith() + { + var items = new List { "apple", "banana", "apricot", "cherry" }; + + // Count items that start with "a" using inner assertion builder + await Assert.That(items).Count(item => item.StartsWith("a")).IsEqualTo(2); + } + + [Test] + public async Task Count_WithInnerAssertion_EmptyCollection() + { + var items = new List(); + + // Count on empty collection should return 0 + await Assert.That(items).Count(item => item.IsGreaterThan(0)).IsEqualTo(0); + } + + [Test] + public async Task Count_WithInnerAssertion_NoneMatch() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items > 10 (none match) + await Assert.That(items).Count(item => item.IsGreaterThan(10)).IsEqualTo(0); + } + + [Test] + public async Task Count_WithInnerAssertion_AllMatch() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count items > 0 (all match) + await Assert.That(items).Count(item => item.IsGreaterThan(0)).IsEqualTo(5); + } + + [Test] + public async Task Count_WithInnerAssertion_Lambda_Collection() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Test with lambda-wrapped collection + await Assert.That(() => items).Count(item => item.IsGreaterThan(2)).IsEqualTo(3); + } + + // Tests for collection chaining after Count assertions + + [Test] + public async Task Count_ThenAnd_Contains() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count and then chain with Contains + await Assert.That(items) + .Count().IsEqualTo(5) + .And.Contains(3); + } + + [Test] + public async Task Count_ThenAnd_IsNotEmpty() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count and then chain with IsNotEmpty + await Assert.That(items) + .Count().IsGreaterThan(0) + .And.IsNotEmpty(); + } + + [Test] + public async Task Count_WithInnerAssertion_ThenAnd_Contains() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count with inner assertion and then chain with Contains + await Assert.That(items) + .Count(item => item.IsGreaterThan(2)).IsEqualTo(3) + .And.Contains(5); + } + + [Test] + public async Task Count_ThenAnd_All() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count and then chain with All + await Assert.That(items) + .Count().IsEqualTo(5) + .And.All(x => x > 0); + } + + [Test] + public async Task Count_ThenAnd_Count() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Chain multiple Count assertions + await Assert.That(items) + .Count().IsGreaterThan(3) + .And.Count().IsLessThan(10); + } + + [Test] + public async Task Count_WithInnerAssertion_ThenAnd_IsInOrder() + { + var items = new List { 1, 2, 3, 4, 5 }; + + // Count with inner assertion and then check ordering + await Assert.That(items) + .Count(item => item.IsGreaterThan(0)).IsEqualTo(5) + .And.IsInOrder(); + } + + [Test] + public async Task Count_IsGreaterThan() + { + var items = new List { 1, 2, 3, 4, 5 }; + + await Assert.That(items).Count().IsGreaterThan(3); + } + + [Test] + public async Task Count_IsLessThan() + { + var items = new List { 1, 2, 3 }; + + await Assert.That(items).Count().IsLessThan(5); + } + + [Test] + public async Task Count_IsGreaterThanOrEqualTo() + { + var items = new List { 1, 2, 3, 4, 5 }; + + await Assert.That(items).Count().IsGreaterThanOrEqualTo(5); + } + + [Test] + public async Task Count_IsLessThanOrEqualTo() + { + var items = new List { 1, 2, 3, 4, 5 }; + + await Assert.That(items).Count().IsLessThanOrEqualTo(5); + } + + [Test] + public async Task Count_IsZero() + { + var items = new List(); + + await Assert.That(items).Count().IsZero(); + } + + [Test] + public async Task Count_IsPositive() + { + var items = new List { 1 }; + + await Assert.That(items).Count().IsPositive(); + } + + [Test] + public async Task Count_IsNotEqualTo() + { + var items = new List { 1, 2, 3 }; + + await Assert.That(items).Count().IsNotEqualTo(5); + } + + [Test] + public async Task Chained_Collection_Assertions() + { + var numbers = new[] { 1, 2, 3, 4, 5 }; + + // For collections of int, use Count().IsEqualTo(5) instead of Count(c => c.IsEqualTo(5)) + // to avoid ambiguity with item-filtering + await Assert.That(numbers) + .IsNotEmpty() + .And.Count().IsEqualTo(5) + .And.Contains(3) + .And.DoesNotContain(10) + .And.IsInOrder() + .And.All(n => n > 0) + .And.Any(n => n == 5); + } + + [Test] + public async Task Chained_Collection_Assertions_WithStrings() + { + var names = new[] { "Alice", "Bob", "Charlie" }; + + // For non-int collections, Count(c => c.IsEqualTo(3)) works unambiguously + await Assert.That(names) + .IsNotEmpty() + .And.Count(c => c.IsEqualTo(3)) + .And.Contains("Bob") + .And.DoesNotContain("Dave"); + } +} diff --git a/TUnit.Assertions.Tests/FuncCollectionAssertionTests.cs b/TUnit.Assertions.Tests/FuncCollectionAssertionTests.cs new file mode 100644 index 0000000000..ffa4fee039 --- /dev/null +++ b/TUnit.Assertions.Tests/FuncCollectionAssertionTests.cs @@ -0,0 +1,229 @@ +using TUnit.Assertions.Exceptions; + +namespace TUnit.Assertions.Tests; + +/// +/// Tests for FuncCollectionAssertion - verifies that collection assertions work when +/// collections are wrapped in lambdas. Addresses GitHub issue #3910. +/// +public class FuncCollectionAssertionTests +{ + [Test] + public async Task Lambda_Collection_IsEmpty_Passes_For_Empty_Collection() + { + var items = new List(); + await Assert.That(() => items).IsEmpty(); + } + + [Test] + public async Task Lambda_Collection_IsEmpty_Fails_For_NonEmpty_Collection() + { + var items = new List { 1, 2, 3 }; + await Assert.That(async () => await Assert.That(() => items).IsEmpty()) + .Throws(); + } + + [Test] + public async Task Lambda_Collection_IsNotEmpty_Passes_For_NonEmpty_Collection() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).IsNotEmpty(); + } + + [Test] + public async Task Lambda_Collection_IsNotEmpty_Fails_For_Empty_Collection() + { + var items = new List(); + await Assert.That(async () => await Assert.That(() => items).IsNotEmpty()) + .Throws(); + } + + [Test] + public async Task Lambda_Collection_Count_IsEqualTo_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).Count().IsEqualTo(3); + } + + [Test] + public async Task Lambda_Collection_Count_IsGreaterThan_Passes() + { + var items = new List { 1, 2, 3, 4, 5 }; + await Assert.That(() => items).Count().IsGreaterThan(3); + } + + [Test] + public async Task Lambda_Collection_Contains_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).Contains(2); + } + + [Test] + public async Task Lambda_Collection_Contains_Fails_When_Item_Not_Present() + { + var items = new List { 1, 2, 3 }; + await Assert.That(async () => await Assert.That(() => items).Contains(99)) + .Throws(); + } + + [Test] + public async Task Lambda_Collection_DoesNotContain_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).DoesNotContain(99); + } + + [Test] + public async Task Lambda_Collection_HasSingleItem_Passes() + { + var items = new List { 42 }; + await Assert.That(() => items).HasSingleItem(); + } + + [Test] + public async Task Lambda_Collection_All_Passes() + { + var items = new List { 2, 4, 6 }; + await Assert.That(() => items).All(x => x % 2 == 0); + } + + [Test] + public async Task Lambda_Collection_Any_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).Any(x => x > 2); + } + + [Test] + public async Task Lambda_Collection_IsInOrder_Passes() + { + var items = new List { 1, 2, 3, 4, 5 }; + await Assert.That(() => items).IsInOrder(); + } + + [Test] + public async Task Lambda_Collection_IsInDescendingOrder_Passes() + { + var items = new List { 5, 4, 3, 2, 1 }; + await Assert.That(() => items).IsInDescendingOrder(); + } + + [Test] + public async Task Lambda_Collection_HasDistinctItems_Passes() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items).HasDistinctItems(); + } + + [Test] + public async Task Lambda_Collection_Throws_Passes_When_Exception_Thrown() + { + await Assert.That(() => ThrowingMethod()).Throws(); + } + + [Test] + public async Task Lambda_Collection_Chaining_With_And() + { + var items = new List { 1, 2, 3 }; + await Assert.That(() => items) + .IsNotEmpty() + .And.Contains(2) + .And.HasDistinctItems(); + } + + [Test] + public async Task Lambda_Array_IsEmpty_Passes() + { + var items = Array.Empty(); + await Assert.That(() => items).IsEmpty(); + } + + [Test] + public async Task Lambda_Array_IsNotEmpty_Passes() + { + var items = new[] { "a", "b", "c" }; + await Assert.That(() => items).IsNotEmpty(); + } + + [Test] + public async Task Lambda_Enumerable_IsEmpty_Passes() + { + IEnumerable items = Enumerable.Empty(); + await Assert.That(() => items).IsEmpty(); + } + + [Test] + public async Task Lambda_HashSet_Contains_Passes() + { + var items = new HashSet { 1, 2, 3 }; + await Assert.That(() => items).Contains(2); + } + + private static IEnumerable ThrowingMethod() + { + throw new InvalidOperationException("Test exception"); + } + + // Async lambda tests + [Test] + public async Task AsyncLambda_Collection_IsEmpty_Passes() + { + await Assert.That(async () => await GetEmptyCollectionAsync()).IsEmpty(); + } + + [Test] + public async Task AsyncLambda_Collection_IsNotEmpty_Passes() + { + await Assert.That(async () => await GetCollectionAsync()).IsNotEmpty(); + } + + [Test] + public async Task AsyncLambda_Collection_Count_IsEqualTo_Passes() + { + await Assert.That(async () => await GetCollectionAsync()).Count().IsEqualTo(3); + } + + [Test] + public async Task AsyncLambda_Collection_Contains_Passes() + { + await Assert.That(async () => await GetCollectionAsync()).Contains(2); + } + + [Test] + public async Task AsyncLambda_Collection_All_Passes() + { + await Assert.That(async () => await GetCollectionAsync()).All(x => x > 0); + } + + [Test] + public async Task AsyncLambda_Collection_Throws_Passes() + { + await Assert.That(async () => await ThrowingMethodAsync()).Throws(); + } + + [Test] + public async Task AsyncLambda_Collection_Chaining_With_And() + { + await Assert.That(async () => await GetCollectionAsync()) + .IsNotEmpty() + .And.Contains(2) + .And.HasDistinctItems(); + } + + private static Task> GetEmptyCollectionAsync() + { + return Task.FromResult>(new List()); + } + + private static Task> GetCollectionAsync() + { + return Task.FromResult>(new List { 1, 2, 3 }); + } + + private static async Task> ThrowingMethodAsync() + { + await Task.Yield(); + throw new InvalidOperationException("Test exception"); + } +} diff --git a/TUnit.Assertions.Tests/Helpers/StringDifferenceTests.cs b/TUnit.Assertions.Tests/Helpers/StringDifferenceTests.cs index 8aee42cc8e..18d1ef01d7 100644 --- a/TUnit.Assertions.Tests/Helpers/StringDifferenceTests.cs +++ b/TUnit.Assertions.Tests/Helpers/StringDifferenceTests.cs @@ -10,15 +10,15 @@ Expected to be equal to "some text" but found "" at Assert.That(actual).IsEqualTo(expected) - """; + """.NormalizeLineEndings(); var actual = ""; var expected = "some text"; var sut = async () => await Assert.That(actual).IsEqualTo(expected); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var exception = await Assert.That(sut).ThrowsException(); + await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] @@ -29,15 +29,15 @@ Expected to be equal to "" but found "actual text" at Assert.That(actual).IsEqualTo(expected) - """; + """.NormalizeLineEndings(); var actual = "actual text"; var expected = ""; var sut = async () => await Assert.That(actual).IsEqualTo(expected); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var exception = await Assert.That(sut).ThrowsException(); + await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] @@ -48,15 +48,15 @@ Expected to be equal to "some text" but found "some" at Assert.That(actual).IsEqualTo(expected) - """; + """.NormalizeLineEndings(); var actual = "some"; var expected = "some text"; var sut = async () => await Assert.That(actual).IsEqualTo(expected); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var exception = await Assert.That(sut).ThrowsException(); + await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } [Test] @@ -67,14 +67,14 @@ Expected to be equal to "some" but found "some text" at Assert.That(actual).IsEqualTo(expected) - """; + """.NormalizeLineEndings(); var actual = "some text"; var expected = "some"; var sut = async () => await Assert.That(actual).IsEqualTo(expected); - await Assert.That(sut).ThrowsException() - .WithMessage(expectedMessage); + var exception = await Assert.That(sut).ThrowsException(); + await Assert.That(exception.Message.NormalizeLineEndings()).IsEqualTo(expectedMessage); } } diff --git a/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs b/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs index a12e4a5421..8f7ff94050 100644 --- a/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs +++ b/TUnit.Assertions.Tests/Old/AssertMultipleTests.cs @@ -33,35 +33,35 @@ Expected to be 2 but found 1 at Assert.That(1).IsEqualTo(2) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(exception2.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 3 but found 2 at Assert.That(2).IsEqualTo(3) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(exception3.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 4 but found 3 at Assert.That(3).IsEqualTo(4) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(exception4.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 5 but found 4 at Assert.That(4).IsEqualTo(5) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(exception5.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 6 but found 5 at Assert.That(5).IsEqualTo(6) - """); + """.NormalizeLineEndings()); } [Test] @@ -93,7 +93,7 @@ or to be 3 but found 1 at Assert.That(1).IsEqualTo(2).Or.IsEqualTo(3) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(exception2.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 3 @@ -101,7 +101,7 @@ and to be 4 but found 2 at Assert.That(2).IsEqualTo(3).And.IsEqualTo(4) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(exception3.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 4 @@ -109,7 +109,7 @@ or to be 5 but found 3 at Assert.That(3).IsEqualTo(4).Or.IsEqualTo(5) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(exception4.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 5 @@ -117,7 +117,7 @@ and to be 6 but found 4 at Assert.That(4).IsEqualTo(5).And.IsEqualTo(6) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(exception5.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 6 @@ -125,7 +125,7 @@ or to be 7 but found 5 at Assert.That(5).IsEqualTo(6).Or.IsEqualTo(7) - """); + """.NormalizeLineEndings()); } [Test] @@ -176,48 +176,48 @@ Expected to be 2 but found 1 at Assert.That(1).IsEqualTo(2) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(assertionException2.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 3 but found 2 at Assert.That(2).IsEqualTo(3) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(assertionException3.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 4 but found 3 at Assert.That(3).IsEqualTo(4) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(assertionException4.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 5 but found 4 at Assert.That(4).IsEqualTo(5) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(assertionException5.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 6 but found 5 at Assert.That(5).IsEqualTo(6) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(assertionException6.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 7 but found 6 at Assert.That(6).IsEqualTo(7) - """); + """.NormalizeLineEndings()); await TUnitAssert.That(assertionException7.Message.NormalizeLineEndings()).IsEqualTo(""" Expected to be 8 but found 7 at Assert.That(7).IsEqualTo(8) - """); + """.NormalizeLineEndings()); } } diff --git a/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs b/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs index 841660fe8d..a420ea73d1 100644 --- a/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs +++ b/TUnit.Assertions.Tests/Old/EquivalentAssertionTests.cs @@ -136,7 +136,7 @@ await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( but collection item at index 1 does not match: expected 2, but was 5 at Assert.That(array).IsEquivalentTo(list, CollectionOrdering.Matching) - """ + """.NormalizeLineEndings() ); } @@ -155,7 +155,7 @@ await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( but collection item at index 1 does not match: expected 2, but was 5 at Assert.That(array).IsEquivalentTo(list, CollectionOrdering.Matching) - """ + """.NormalizeLineEndings() ); } diff --git a/TUnit.Assertions.Tests/Old/StringRegexAssertionTests.cs b/TUnit.Assertions.Tests/Old/StringRegexAssertionTests.cs index 3c1bffb616..98b2bcae9f 100644 --- a/TUnit.Assertions.Tests/Old/StringRegexAssertionTests.cs +++ b/TUnit.Assertions.Tests/Old/StringRegexAssertionTests.cs @@ -56,13 +56,13 @@ public async Task Matches_WithInvalidPattern_StringPattern_Throws(Type exception return; } - await TUnitAssert.That(exception!.Message).IsEqualTo( + await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( $""" Expected text match pattern but The regex "^\d+$" does not match with "{text}" at Assert.That(text).Matches(pattern) - """ + """.NormalizeLineEndings() ); } @@ -81,13 +81,13 @@ public async Task Matches_WithInvalidPattern_RegexPattern_Throws(Type exceptionT return; } - await TUnitAssert.That(exception!.Message).IsEqualTo( + await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( $""" Expected text match pattern but The regex "^\d+$" does not match with "{text}" at Assert.That(text).Matches(pattern) - """ + """.NormalizeLineEndings() ); } @@ -110,13 +110,13 @@ public async Task Matches_WithInvalidPattern_GeneratedRegexPattern_Throws(Type e return; } - await TUnitAssert.That(exception!.Message).IsEqualTo( + await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( $""" Expected text match regex but The regex "^\d+$" does not match with "Hello123World" at Assert.That(text).Matches(regex) - """ + """.NormalizeLineEndings() ); } #endif @@ -192,13 +192,13 @@ public async Task DoesNotMatch_WithInvalidPattern_StringPattern_Throws(Type exce return; } - await TUnitAssert.That(exception!.Message).IsEqualTo( + await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( $""" Expected text to not match with pattern but The regex "^\d+$" matches with "{text}" at Assert.That(text).DoesNotMatch(pattern) - """ + """.NormalizeLineEndings() ); } @@ -217,13 +217,13 @@ public async Task DoesNotMatch_WithInvalidPattern_RegexPattern_Throws(Type excep return; } - await TUnitAssert.That(exception!.Message).IsEqualTo( + await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( $""" Expected text to not match with pattern but The regex "^\d+$" matches with "{text}" at Assert.That(text).DoesNotMatch(pattern) - """ + """.NormalizeLineEndings() ); } @@ -246,13 +246,13 @@ public async Task DoesNotMatch_WithInvalidPattern_GeneratedRegexPattern_Throws(T return; } - await TUnitAssert.That(exception!.Message).IsEqualTo( + await TUnitAssert.That(exception!.Message.NormalizeLineEndings()).IsEqualTo( $""" Expected text to not match with regex but The regex "^\d+$" matches with "{text}" at Assert.That(text).DoesNotMatch(regex) - """ + """.NormalizeLineEndings() ); } #endif diff --git a/TUnit.Assertions.Tests/StringLengthAssertionTests.cs b/TUnit.Assertions.Tests/StringLengthAssertionTests.cs new file mode 100644 index 0000000000..87fd7e2ae2 --- /dev/null +++ b/TUnit.Assertions.Tests/StringLengthAssertionTests.cs @@ -0,0 +1,164 @@ +using TUnit.Assertions.Exceptions; + +namespace TUnit.Assertions.Tests; + +public class StringLengthAssertionTests +{ + [Test] + public async Task Length_IsEqualTo_Passes_When_Length_Matches() + { + var str = "Hello"; + await Assert.That(str).Length().IsEqualTo(5); + } + + [Test] + public async Task Length_IsEqualTo_Fails_When_Length_DoesNotMatch() + { + var str = "Hello"; + await Assert.That(async () => await Assert.That(str).Length().IsEqualTo(10)) + .Throws() + .And.HasMessageContaining("10"); + } + + [Test] + public async Task Length_IsGreaterThan_Passes_When_Length_IsGreater() + { + var str = "Hello, World!"; + await Assert.That(str).Length().IsGreaterThan(5); + } + + [Test] + public async Task Length_IsGreaterThan_Fails_When_Length_IsNotGreater() + { + var str = "Hi"; + await Assert.That(async () => await Assert.That(str).Length().IsGreaterThan(5)) + .Throws() + .And.HasMessageContaining("greater than") + .And.HasMessageContaining("5"); + } + + [Test] + public async Task Length_IsLessThan_Passes_When_Length_IsLess() + { + var str = "Hi"; + await Assert.That(str).Length().IsLessThan(10); + } + + [Test] + public async Task Length_IsLessThan_Fails_When_Length_IsNotLess() + { + var str = "Hello, World!"; + await Assert.That(async () => await Assert.That(str).Length().IsLessThan(5)) + .Throws() + .And.HasMessageContaining("less than") + .And.HasMessageContaining("5"); + } + + [Test] + public async Task Length_IsGreaterThanOrEqualTo_Passes_When_Equal() + { + var str = "Hello"; + await Assert.That(str).Length().IsGreaterThanOrEqualTo(5); + } + + [Test] + public async Task Length_IsGreaterThanOrEqualTo_Passes_When_Greater() + { + var str = "Hello, World!"; + await Assert.That(str).Length().IsGreaterThanOrEqualTo(5); + } + + [Test] + public async Task Length_IsGreaterThanOrEqualTo_Fails_When_Less() + { + var str = "Hi"; + await Assert.That(async () => await Assert.That(str).Length().IsGreaterThanOrEqualTo(5)) + .Throws() + .And.HasMessageContaining("greater than or equal to") + .And.HasMessageContaining("5"); + } + + [Test] + public async Task Length_IsLessThanOrEqualTo_Passes_When_Equal() + { + var str = "Hello"; + await Assert.That(str).Length().IsLessThanOrEqualTo(5); + } + + [Test] + public async Task Length_IsLessThanOrEqualTo_Passes_When_Less() + { + var str = "Hi"; + await Assert.That(str).Length().IsLessThanOrEqualTo(5); + } + + [Test] + public async Task Length_IsLessThanOrEqualTo_Fails_When_Greater() + { + var str = "Hello, World!"; + await Assert.That(async () => await Assert.That(str).Length().IsLessThanOrEqualTo(5)) + .Throws() + .And.HasMessageContaining("less than or equal to") + .And.HasMessageContaining("5"); + } + + [Test] + public async Task Length_IsBetween_Passes_When_InRange() + { + var str = "Hello"; + await Assert.That(str).Length().IsBetween(1, 10); + } + + [Test] + public async Task Length_IsBetween_Fails_When_OutOfRange() + { + var str = "Hello, World!"; + await Assert.That(async () => await Assert.That(str).Length().IsBetween(1, 5)) + .Throws() + .And.HasMessageContaining("between"); + } + + [Test] + public async Task Length_IsPositive_Passes_For_NonEmptyString() + { + var str = "Hello"; + await Assert.That(str).Length().IsPositive(); + } + + [Test] + public async Task Length_IsGreaterThanOrEqualTo_Zero_Passes_Always() + { + var str = ""; + await Assert.That(str).Length().IsGreaterThanOrEqualTo(0); + } + + [Test] + public async Task Length_IsZero_Passes_For_EmptyString() + { + var str = ""; + await Assert.That(str).Length().IsZero(); + } + + [Test] + public async Task Length_IsNotZero_Passes_For_NonEmptyString() + { + var str = "Hello"; + await Assert.That(str).Length().IsNotZero(); + } + + [Test] + public async Task Length_WithNullString_Returns_Zero() + { + string? str = null; + await Assert.That(str).Length().IsZero(); + } + + [Test] + public async Task Length_Chained_With_And() + { + var str = "Hello"; + await Assert.That(str) + .Length().IsGreaterThan(3) + .And.IsLessThan(10); + } +} diff --git a/TUnit.Assertions.Tests/ThrowInDelegateValueAssertionTests.cs b/TUnit.Assertions.Tests/ThrowInDelegateValueAssertionTests.cs index 86cec759e2..c776362562 100644 --- a/TUnit.Assertions.Tests/ThrowInDelegateValueAssertionTests.cs +++ b/TUnit.Assertions.Tests/ThrowInDelegateValueAssertionTests.cs @@ -5,18 +5,19 @@ public class ThrowInDelegateValueAssertionTests [Test] public async Task ThrowInDelegateValueAssertion_ReturnsExpectedErrorMessage() { + var expectedContains = """ + Expected to be equal to True + but threw System.Exception + """.NormalizeLineEndings(); var assertion = async () => await Assert.That(() => { throw new Exception("No"); return true; }).IsEqualTo(true); - await Assert.That(assertion) - .Throws() - .WithMessageContaining(""" - Expected to be equal to True - but threw System.Exception - """); + var exception = await Assert.That(assertion) + .Throws(); + await Assert.That(exception.Message.NormalizeLineEndings()).Contains(expectedContains); } [Test] diff --git a/TUnit.Assertions/Chaining/AndAssertion.cs b/TUnit.Assertions/Chaining/AndAssertion.cs index b9186e64ee..486c947b73 100644 --- a/TUnit.Assertions/Chaining/AndAssertion.cs +++ b/TUnit.Assertions/Chaining/AndAssertion.cs @@ -139,10 +139,10 @@ private string BuildCombinedExpectation() var becausePrefix = firstBecause.StartsWith("because ", StringComparison.OrdinalIgnoreCase) ? firstBecause : $"because {firstBecause}"; - return $"{firstExpectation}, {becausePrefix}{Environment.NewLine}and {secondExpectation}"; + return $"{firstExpectation}, {becausePrefix}\nand {secondExpectation}"; } - return $"{firstExpectation}{Environment.NewLine}and {secondExpectation}"; + return $"{firstExpectation}\nand {secondExpectation}"; } protected override string GetExpectation() => "both conditions"; diff --git a/TUnit.Assertions/Chaining/OrAssertion.cs b/TUnit.Assertions/Chaining/OrAssertion.cs index 47aa2e87c2..15b042a531 100644 --- a/TUnit.Assertions/Chaining/OrAssertion.cs +++ b/TUnit.Assertions/Chaining/OrAssertion.cs @@ -140,10 +140,10 @@ private string BuildCombinedExpectation() var becausePrefix = firstBecause.StartsWith("because ", StringComparison.OrdinalIgnoreCase) ? firstBecause : $"because {firstBecause}"; - return $"{firstExpectation}, {becausePrefix}{Environment.NewLine}or {secondExpectation}"; + return $"{firstExpectation}, {becausePrefix}\nor {secondExpectation}"; } - return $"{firstExpectation}{Environment.NewLine}or {secondExpectation}"; + return $"{firstExpectation}\nor {secondExpectation}"; } protected override string GetExpectation() => "either condition"; diff --git a/TUnit.Assertions/Conditions/CollectionCountSource.cs b/TUnit.Assertions/Conditions/CollectionCountSource.cs new file mode 100644 index 0000000000..4ffd07fd24 --- /dev/null +++ b/TUnit.Assertions/Conditions/CollectionCountSource.cs @@ -0,0 +1,322 @@ +using System.Runtime.CompilerServices; +using TUnit.Assertions.Core; +using TUnit.Assertions.Sources; + +namespace TUnit.Assertions.Conditions; + +/// +/// Provides count assertions that preserve collection type for further chaining. +/// This enables patterns like: Assert.That(list).Count(item => item.IsGreaterThan(3)).IsEqualTo(2).And.Contains(5) +/// +public class CollectionCountSource + where TCollection : IEnumerable +{ + private readonly AssertionContext _collectionContext; + private readonly Func, Assertion?>? _assertion; + + public CollectionCountSource( + AssertionContext collectionContext, + Func, Assertion?>? assertion) + { + _collectionContext = collectionContext; + _assertion = assertion; + } + + /// + /// Asserts that the count is equal to the expected value. + /// Returns a collection-aware assertion that allows further collection chaining. + /// + public CollectionCountEqualsAssertion IsEqualTo( + int expected, + [CallerArgumentExpression(nameof(expected))] string? expression = null) + { + _collectionContext.ExpressionBuilder.Append($".IsEqualTo({expression})"); + return new CollectionCountEqualsAssertion( + _collectionContext, _assertion, expected, CountComparison.Equal); + } + + /// + /// Asserts that the count is not equal to the expected value. + /// Returns a collection-aware assertion that allows further collection chaining. + /// + public CollectionCountEqualsAssertion IsNotEqualTo( + int expected, + [CallerArgumentExpression(nameof(expected))] string? expression = null) + { + _collectionContext.ExpressionBuilder.Append($".IsNotEqualTo({expression})"); + return new CollectionCountEqualsAssertion( + _collectionContext, _assertion, expected, CountComparison.NotEqual); + } + + /// + /// Asserts that the count is greater than the expected value. + /// Returns a collection-aware assertion that allows further collection chaining. + /// + public CollectionCountEqualsAssertion IsGreaterThan( + int expected, + [CallerArgumentExpression(nameof(expected))] string? expression = null) + { + _collectionContext.ExpressionBuilder.Append($".IsGreaterThan({expression})"); + return new CollectionCountEqualsAssertion( + _collectionContext, _assertion, expected, CountComparison.GreaterThan); + } + + /// + /// Asserts that the count is greater than or equal to the expected value. + /// Returns a collection-aware assertion that allows further collection chaining. + /// + public CollectionCountEqualsAssertion IsGreaterThanOrEqualTo( + int expected, + [CallerArgumentExpression(nameof(expected))] string? expression = null) + { + _collectionContext.ExpressionBuilder.Append($".IsGreaterThanOrEqualTo({expression})"); + return new CollectionCountEqualsAssertion( + _collectionContext, _assertion, expected, CountComparison.GreaterThanOrEqual); + } + + /// + /// Asserts that the count is less than the expected value. + /// Returns a collection-aware assertion that allows further collection chaining. + /// + public CollectionCountEqualsAssertion IsLessThan( + int expected, + [CallerArgumentExpression(nameof(expected))] string? expression = null) + { + _collectionContext.ExpressionBuilder.Append($".IsLessThan({expression})"); + return new CollectionCountEqualsAssertion( + _collectionContext, _assertion, expected, CountComparison.LessThan); + } + + /// + /// Asserts that the count is less than or equal to the expected value. + /// Returns a collection-aware assertion that allows further collection chaining. + /// + public CollectionCountEqualsAssertion IsLessThanOrEqualTo( + int expected, + [CallerArgumentExpression(nameof(expected))] string? expression = null) + { + _collectionContext.ExpressionBuilder.Append($".IsLessThanOrEqualTo({expression})"); + return new CollectionCountEqualsAssertion( + _collectionContext, _assertion, expected, CountComparison.LessThanOrEqual); + } + + /// + /// Asserts that the count is zero. + /// Returns a collection-aware assertion that allows further collection chaining. + /// + public CollectionCountEqualsAssertion IsZero() + { + _collectionContext.ExpressionBuilder.Append(".IsZero()"); + return new CollectionCountEqualsAssertion( + _collectionContext, _assertion, 0, CountComparison.Equal); + } + + /// + /// Asserts that the count is positive (greater than zero). + /// Returns a collection-aware assertion that allows further collection chaining. + /// + public CollectionCountEqualsAssertion IsPositive() + { + _collectionContext.ExpressionBuilder.Append(".IsPositive()"); + return new CollectionCountEqualsAssertion( + _collectionContext, _assertion, 0, CountComparison.GreaterThan); + } +} + +internal enum CountComparison +{ + Equal, + NotEqual, + GreaterThan, + GreaterThanOrEqual, + LessThan, + LessThanOrEqual +} + +/// +/// Collection-aware count assertion that preserves the collection type for further chaining. +/// Inherits from CollectionAssertionBase to enable .And.Contains(), .And.IsNotEmpty(), etc. +/// +public class CollectionCountEqualsAssertion : CollectionAssertionBase + where TCollection : IEnumerable +{ + private readonly Func, Assertion?>? _itemAssertion; + private readonly int _expected; + private readonly CountComparison _comparison; + private int _actualCount; + + internal CollectionCountEqualsAssertion( + AssertionContext context, + Func, Assertion?>? itemAssertion, + int expected, + CountComparison comparison) + : base(context) + { + _itemAssertion = itemAssertion; + _expected = expected; + _comparison = comparison; + } + + protected override async Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return AssertionResult.Failed($"threw {exception.GetType().Name}"); + } + + if (value == null) + { + return AssertionResult.Failed("collection was null"); + } + + // Calculate count + if (_itemAssertion == null) + { + // Simple count without filtering + _actualCount = value switch + { + System.Collections.ICollection c => c.Count, + _ => System.Linq.Enumerable.Count(value) + }; + } + else + { + // Count items that satisfy the inner assertion + _actualCount = 0; + int index = 0; + + foreach (var item in value) + { + var itemAssertionSource = new ValueAssertion(item, $"item[{index}]"); + var resultingAssertion = _itemAssertion(itemAssertionSource); + + if (resultingAssertion != null) + { + try + { + await resultingAssertion.AssertAsync(); + _actualCount++; + } + catch + { + // Item did not satisfy the assertion, don't count it + } + } + else + { + // Null assertion means no constraint, count all items + _actualCount++; + } + + index++; + } + } + + // Check the comparison + var passed = _comparison switch + { + CountComparison.Equal => _actualCount == _expected, + CountComparison.NotEqual => _actualCount != _expected, + CountComparison.GreaterThan => _actualCount > _expected, + CountComparison.GreaterThanOrEqual => _actualCount >= _expected, + CountComparison.LessThan => _actualCount < _expected, + CountComparison.LessThanOrEqual => _actualCount <= _expected, + _ => false + }; + + if (passed) + { + return AssertionResult.Passed; + } + + return AssertionResult.Failed($"found {_actualCount}"); + } + + protected override string GetExpectation() + { + var comparisonText = _comparison switch + { + CountComparison.Equal => $"to have count equal to {_expected}", + CountComparison.NotEqual => $"to have count not equal to {_expected}", + CountComparison.GreaterThan => $"to have count greater than {_expected}", + CountComparison.GreaterThanOrEqual => $"to have count greater than or equal to {_expected}", + CountComparison.LessThan => $"to have count less than {_expected}", + CountComparison.LessThanOrEqual => $"to have count less than or equal to {_expected}", + _ => $"to have count {_expected}" + }; + + return comparisonText; + } +} + +/// +/// Collection-aware count assertion that executes an inline count assertion lambda. +/// Preserves the collection type for further chaining. +/// Example: Assert.That(list).Count(c => c.IsEqualTo(5)).And.Contains(1) +/// +public class CollectionCountWithInlineAssertionAssertion : CollectionAssertionBase + where TCollection : IEnumerable +{ + private readonly Func, Assertion?> _countAssertion; + private int _actualCount; + + internal CollectionCountWithInlineAssertionAssertion( + AssertionContext context, + Func, Assertion?> countAssertion) + : base(context) + { + _countAssertion = countAssertion; + } + + protected override async Task CheckAsync(EvaluationMetadata metadata) + { + var value = metadata.Value; + var exception = metadata.Exception; + + if (exception != null) + { + return AssertionResult.Failed($"threw {exception.GetType().Name}"); + } + + if (value == null) + { + return AssertionResult.Failed("collection was null"); + } + + // Calculate count + _actualCount = value switch + { + System.Collections.ICollection c => c.Count, + _ => System.Linq.Enumerable.Count(value) + }; + + // Create an assertion source for the count and run the inline assertion + var countSource = new ValueAssertion(_actualCount, "count"); + var resultingAssertion = _countAssertion(countSource); + + if (resultingAssertion != null) + { + try + { + await resultingAssertion.AssertAsync(); + return AssertionResult.Passed; + } + catch + { + // Count assertion failed + return AssertionResult.Failed($"count was {_actualCount}"); + } + } + + // Null assertion means no constraint, always pass + return AssertionResult.Passed; + } + + protected override string GetExpectation() + { + return "to satisfy count assertion"; + } +} diff --git a/TUnit.Assertions/Conditions/CollectionCountValueAssertion.cs b/TUnit.Assertions/Conditions/CollectionCountValueAssertion.cs deleted file mode 100644 index 2b1c5a9670..0000000000 --- a/TUnit.Assertions/Conditions/CollectionCountValueAssertion.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Collections; -using TUnit.Assertions.Core; - -namespace TUnit.Assertions.Conditions; - -/// -/// Assertion that evaluates the count of a collection and provides numeric assertions on that count. -/// Implements IAssertionSource<int> to enable all numeric assertion methods. -/// -public class CollectionCountValueAssertion : Sources.ValueAssertion - where TCollection : IEnumerable -{ - public CollectionCountValueAssertion( - AssertionContext collectionContext, - Func? predicate) - : base(CreateIntContext(collectionContext, predicate)) - { - } - - private static AssertionContext CreateIntContext( - AssertionContext collectionContext, - Func? predicate) - { - return collectionContext.Map(collection => - { - if (collection == null) - { - return 0; - } - - // Calculate count efficiently - if (predicate == null) - { - return collection switch - { - ICollection c => c.Count, - _ => System.Linq.Enumerable.Count(collection) - }; - } - - return System.Linq.Enumerable.Count(collection, predicate); - }); - } -} diff --git a/TUnit.Assertions/Conditions/EqualsAssertion.cs b/TUnit.Assertions/Conditions/EqualsAssertion.cs index 821a2b4e7f..350c439fad 100644 --- a/TUnit.Assertions/Conditions/EqualsAssertion.cs +++ b/TUnit.Assertions/Conditions/EqualsAssertion.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Text; using TUnit.Assertions.Attributes; +using TUnit.Assertions.Conditions.Helpers; using TUnit.Assertions.Core; namespace TUnit.Assertions.Conditions; @@ -84,7 +85,7 @@ protected override Task CheckAsync(EvaluationMetadata m if (_ignoredTypes.Count > 0) { // Use reference-based tracking to detect cycles - var visited = new HashSet(new ReferenceEqualityComparer()); + var visited = new HashSet(ReferenceEqualityComparer.Instance); var result = DeepEquals(value, _expected, _ignoredTypes, visited); if (result.IsSuccess) { @@ -213,15 +214,4 @@ private static (bool IsSuccess, string? Message) DeepEquals(object? actual, obje } protected override string GetExpectation() => $"to be equal to {(_expected is string s ? $"\"{s}\"" : _expected)}"; - - /// - /// Comparer that uses reference equality instead of value equality. - /// Used for cycle detection in deep comparison. - /// - private sealed class ReferenceEqualityComparer : IEqualityComparer - { - public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); - - public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); - } } diff --git a/TUnit.Assertions/Conditions/Helpers/ExpressionHelper.cs b/TUnit.Assertions/Conditions/Helpers/ExpressionHelper.cs new file mode 100644 index 0000000000..4070c1c6a5 --- /dev/null +++ b/TUnit.Assertions/Conditions/Helpers/ExpressionHelper.cs @@ -0,0 +1,43 @@ +namespace TUnit.Assertions.Conditions.Helpers; + +/// +/// Helper methods for parsing and extracting information from assertion expressions. +/// Consolidates expression parsing logic to ensure consistent behavior across assertion classes. +/// +internal static class ExpressionHelper +{ + /// + /// Extracts the source variable name from an assertion expression string. + /// + /// The expression string, e.g., "Assert.That(variableName).IsEquivalentTo(...)" + /// The variable name, or "value" if it cannot be extracted or is a lambda expression. + /// + /// Input: "Assert.That(myObject).IsEquivalentTo(expected)" + /// Output: "myObject" + /// + /// Input: "Assert.That(async () => GetValue()).IsEquivalentTo(expected)" + /// Output: "value" + /// + public static string ExtractSourceVariable(string expression) + { + // Extract variable name from "Assert.That(variableName)" or similar + var thatIndex = expression.IndexOf(".That(", StringComparison.Ordinal); + if (thatIndex >= 0) + { + var startIndex = thatIndex + 6; // Length of ".That(" + var endIndex = expression.IndexOf(')', startIndex); + if (endIndex > startIndex) + { + var variable = expression.Substring(startIndex, endIndex - startIndex); + // Handle lambda expressions like "async () => ..." by returning "value" + if (variable.Contains("=>") || variable.StartsWith("()", StringComparison.Ordinal)) + { + return "value"; + } + return variable; + } + } + + return "value"; + } +} diff --git a/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs b/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs new file mode 100644 index 0000000000..c6c53162c5 --- /dev/null +++ b/TUnit.Assertions/Conditions/Helpers/ReflectionHelper.cs @@ -0,0 +1,63 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace TUnit.Assertions.Conditions.Helpers; + +/// +/// Helper methods for reflection-based member access. +/// Consolidates reflection logic to ensure consistent behavior and reduce code duplication. +/// +internal static class ReflectionHelper +{ + /// + /// Gets all public instance properties and fields to compare for structural equivalency. + /// + /// The type to get members from. + /// A list of PropertyInfo and FieldInfo members. + public static List GetMembersToCompare( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] + Type type) + { + var members = new List(); + members.AddRange(type.GetProperties(BindingFlags.Public | BindingFlags.Instance)); + members.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.Instance)); + return members; + } + + /// + /// Gets the value of a member (property or field) from an object. + /// + /// The object to get the value from. + /// The member (PropertyInfo or FieldInfo) to read. + /// The value of the member. + /// Thrown if the member is not a PropertyInfo or FieldInfo. + public static object? GetMemberValue(object obj, MemberInfo member) + { + return member switch + { + PropertyInfo prop => prop.GetValue(obj), + FieldInfo field => field.GetValue(obj), + _ => throw new InvalidOperationException($"Unknown member type: {member.GetType()}") + }; + } + + /// + /// Gets a member (property or field) by name from a type. + /// + /// The type to search. + /// The member name to find. + /// The MemberInfo if found; null otherwise. + public static MemberInfo? GetMemberInfo( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] + Type type, + string name) + { + var property = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance); + if (property != null) + { + return property; + } + + return type.GetField(name, BindingFlags.Public | BindingFlags.Instance); + } +} diff --git a/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs b/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs index af1a1876e9..13273b4a45 100644 --- a/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs +++ b/TUnit.Assertions/Conditions/Helpers/StructuralEqualityComparer.cs @@ -1,6 +1,5 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; -using System.Reflection; namespace TUnit.Assertions.Conditions.Helpers; @@ -36,12 +35,12 @@ public bool Equals(T? x, T? y) var type = typeof(T); - if (IsPrimitiveType(type)) + if (TypeHelper.IsPrimitiveOrWellKnownType(type)) { return EqualityComparer.Default.Equals(x, y); } - return CompareStructurally(x, y, new HashSet(new ReferenceEqualityComparer())); + return CompareStructurally(x, y, new HashSet(ReferenceEqualityComparer.Instance)); } public int GetHashCode(T obj) @@ -54,23 +53,6 @@ public int GetHashCode(T obj) return EqualityComparer.Default.GetHashCode(obj); } - private static bool IsPrimitiveType(Type type) - { - return type.IsPrimitive - || type.IsEnum - || type == typeof(string) - || type == typeof(decimal) - || type == typeof(DateTime) - || type == typeof(DateTimeOffset) - || type == typeof(TimeSpan) - || type == typeof(Guid) -#if NET6_0_OR_GREATER - || type == typeof(DateOnly) - || type == typeof(TimeOnly) -#endif - ; - } - [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "GetType() is acceptable for runtime structural comparison")] private bool CompareStructurally(object? x, object? y, HashSet visited) { @@ -87,7 +69,7 @@ private bool CompareStructurally(object? x, object? y, HashSet visited) var xType = x.GetType(); var yType = y.GetType(); - if (IsPrimitiveType(xType)) + if (TypeHelper.IsPrimitiveOrWellKnownType(xType)) { return Equals(x, y); } @@ -121,12 +103,12 @@ private bool CompareStructurally(object? x, object? y, HashSet visited) return true; } - var members = GetMembersToCompare(xType); + var members = ReflectionHelper.GetMembersToCompare(xType); foreach (var member in members) { - var xValue = GetMemberValue(x, member); - var yValue = GetMemberValue(y, member); + var xValue = ReflectionHelper.GetMemberValue(x, member); + var yValue = ReflectionHelper.GetMemberValue(y, member); if (!CompareStructurally(xValue, yValue, visited)) { @@ -136,28 +118,4 @@ private bool CompareStructurally(object? x, object? y, HashSet visited) return true; } - - private static List GetMembersToCompare([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] Type type) - { - var members = new List(); - members.AddRange(type.GetProperties(BindingFlags.Public | BindingFlags.Instance)); - members.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.Instance)); - return members; - } - - private static object? GetMemberValue(object obj, MemberInfo member) - { - return member switch - { - PropertyInfo prop => prop.GetValue(obj), - FieldInfo field => field.GetValue(obj), - _ => throw new InvalidOperationException($"Unknown member type: {member.GetType()}") - }; - } - - private sealed class ReferenceEqualityComparer : IEqualityComparer - { - public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); - public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); - } } diff --git a/TUnit.Assertions/Conditions/Helpers/TypeHelper.cs b/TUnit.Assertions/Conditions/Helpers/TypeHelper.cs new file mode 100644 index 0000000000..856a05d9e6 --- /dev/null +++ b/TUnit.Assertions/Conditions/Helpers/TypeHelper.cs @@ -0,0 +1,84 @@ +using System.Collections.Concurrent; + +namespace TUnit.Assertions.Conditions.Helpers; + +/// +/// Helper methods for type checking and classification. +/// Consolidates type checking logic to ensure consistent behavior across assertion classes. +/// +internal static class TypeHelper +{ + /// + /// Thread-safe registry of user-defined types that should be treated as primitives + /// (using value equality rather than structural comparison). + /// + private static readonly ConcurrentDictionary CustomPrimitiveTypes = new(); + + /// + /// Registers a type to be treated as a primitive for structural equivalency comparisons. + /// Once registered, instances of this type will use value equality (via Equals) rather + /// than having their properties compared individually. + /// + /// The type to register as a primitive. + public static void RegisterAsPrimitive() + { + CustomPrimitiveTypes.TryAdd(typeof(T), 0); + } + + /// + /// Registers a type to be treated as a primitive for structural equivalency comparisons. + /// + /// The type to register as a primitive. + public static void RegisterAsPrimitive(Type type) + { + CustomPrimitiveTypes.TryAdd(type, 0); + } + + /// + /// Removes a previously registered custom primitive type. + /// + /// The type to unregister. + /// True if the type was removed; false if it wasn't registered. + public static bool UnregisterPrimitive() + { + return CustomPrimitiveTypes.TryRemove(typeof(T), out _); + } + + /// + /// Clears all registered custom primitive types. + /// Useful for test cleanup between tests. + /// + public static void ClearCustomPrimitives() + { + CustomPrimitiveTypes.Clear(); + } + + /// + /// Determines if a type is a primitive or well-known immutable type that should use + /// value equality rather than structural comparison. + /// + /// The type to check. + /// True if the type should use value equality; false for structural comparison. + public static bool IsPrimitiveOrWellKnownType(Type type) + { + // Check user-defined primitives first (fast path for common case) + if (CustomPrimitiveTypes.ContainsKey(type)) + { + return true; + } + + return type.IsPrimitive + || type.IsEnum + || type == typeof(string) + || type == typeof(decimal) + || type == typeof(DateTime) + || type == typeof(DateTimeOffset) + || type == typeof(TimeSpan) + || type == typeof(Guid) +#if NET6_0_OR_GREATER + || type == typeof(DateOnly) + || type == typeof(TimeOnly) +#endif + ; + } +} diff --git a/TUnit.Assertions/Conditions/NotStructuralEquivalencyAssertion.cs b/TUnit.Assertions/Conditions/NotStructuralEquivalencyAssertion.cs index 4e58fd0bb6..2d8c30306a 100644 --- a/TUnit.Assertions/Conditions/NotStructuralEquivalencyAssertion.cs +++ b/TUnit.Assertions/Conditions/NotStructuralEquivalencyAssertion.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Text; +using TUnit.Assertions.Conditions.Helpers; using TUnit.Assertions.Core; namespace TUnit.Assertions.Conditions; @@ -90,7 +91,12 @@ protected override Task CheckAsync(EvaluationMetadata m foreach (var type in _ignoredTypes) tempAssertion.IgnoringType(type); - var result = tempAssertion.CompareObjects(value, _notExpected, "", new HashSet(new ReferenceEqualityComparer())); + var result = tempAssertion.CompareObjects( + value, + _notExpected, + "", + new HashSet(ReferenceEqualityComparer.Instance), + new HashSet(ReferenceEqualityComparer.Instance)); // Invert the result - we want them to NOT be equivalent if (result.IsPassed) @@ -101,43 +107,14 @@ protected override Task CheckAsync(EvaluationMetadata m return Task.FromResult(AssertionResult.Passed); } - private sealed class ReferenceEqualityComparer : IEqualityComparer - { - public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); - public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); - } - protected override string GetExpectation() { // Extract the source variable name from the expression builder // Format: "Assert.That(variableName).IsNotEquivalentTo(...)" var expressionString = Context.ExpressionBuilder.ToString(); - var sourceVariable = ExtractSourceVariable(expressionString); + var sourceVariable = ExpressionHelper.ExtractSourceVariable(expressionString); var notExpectedDesc = _notExpectedExpression ?? "expected value"; return $"{sourceVariable} to not be equivalent to {notExpectedDesc}"; } - - private static string ExtractSourceVariable(string expression) - { - // Extract variable name from "Assert.That(variableName)" or similar - var thatIndex = expression.IndexOf(".That("); - if (thatIndex >= 0) - { - var startIndex = thatIndex + 6; // Length of ".That(" - var endIndex = expression.IndexOf(')', startIndex); - if (endIndex > startIndex) - { - var variable = expression.Substring(startIndex, endIndex - startIndex); - // Handle lambda expressions like "async () => ..." by returning "value" - if (variable.Contains("=>") || variable.StartsWith("()")) - { - return "value"; - } - return variable; - } - } - - return "value"; - } } diff --git a/TUnit.Assertions/Conditions/StringLengthValueAssertion.cs b/TUnit.Assertions/Conditions/StringLengthValueAssertion.cs new file mode 100644 index 0000000000..faf24a95dc --- /dev/null +++ b/TUnit.Assertions/Conditions/StringLengthValueAssertion.cs @@ -0,0 +1,16 @@ +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Conditions; + +/// +/// Assertion that evaluates the length of a string and provides numeric assertions on that length. +/// Implements IAssertionSource<int> to enable all numeric assertion methods. +/// Example: await Assert.That(str).Length().IsGreaterThan(5); +/// +public class StringLengthValueAssertion : Sources.ValueAssertion +{ + public StringLengthValueAssertion(AssertionContext stringContext) + : base(stringContext.Map(s => s?.Length ?? 0)) + { + } +} diff --git a/TUnit.Assertions/Conditions/StructuralEquivalencyAssertion.cs b/TUnit.Assertions/Conditions/StructuralEquivalencyAssertion.cs index 8591bf4da9..2f1086a1a7 100644 --- a/TUnit.Assertions/Conditions/StructuralEquivalencyAssertion.cs +++ b/TUnit.Assertions/Conditions/StructuralEquivalencyAssertion.cs @@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; +using TUnit.Assertions.Conditions.Helpers; using TUnit.Assertions.Core; namespace TUnit.Assertions.Conditions; @@ -77,11 +78,21 @@ protected override Task CheckAsync(EvaluationMetadata m return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}: {exception.Message}")); } - var result = CompareObjects(value, _expected, "", new HashSet(new ReferenceEqualityComparer())); + var result = CompareObjects( + value, + _expected, + "", + new HashSet(ReferenceEqualityComparer.Instance), + new HashSet(ReferenceEqualityComparer.Instance)); return Task.FromResult(result); } - internal AssertionResult CompareObjects(object? actual, object? expected, string path, HashSet visited) + internal AssertionResult CompareObjects( + object? actual, + object? expected, + string path, + HashSet visitedActual, + HashSet? visitedExpected = null) { // Check for ignored paths if (_ignoredMembers.Contains(path)) @@ -109,7 +120,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string var expectedType = expected.GetType(); // Handle primitive types and strings - if (IsPrimitiveType(actualType)) + if (TypeHelper.IsPrimitiveOrWellKnownType(actualType)) { if (!Equals(actual, expected)) { @@ -118,13 +129,25 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string return AssertionResult.Passed; } - // Handle cycles - if (visited.Contains(actual)) + // Handle cycles - check both actual and expected to prevent infinite recursion + // from cycles in either object graph + if (visitedActual.Contains(actual)) { return AssertionResult.Passed; } - visited.Add(actual); + visitedActual.Add(actual); + + // Also track expected objects to handle cycles in the expected graph + if (visitedExpected != null) + { + if (visitedExpected.Contains(expected)) + { + return AssertionResult.Passed; + } + + visitedExpected.Add(expected); + } // Handle enumerables if (actual is IEnumerable actualEnumerable && expected is IEnumerable expectedEnumerable @@ -156,7 +179,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string return AssertionResult.Failed($"{itemPath} did not match{Environment.NewLine}Expected: null{Environment.NewLine}Received: {FormatValue(actualList[i])}"); } - var result = CompareObjects(actualList[i], expectedList[i], itemPath, visited); + var result = CompareObjects(actualList[i], expectedList[i], itemPath, visitedActual, visitedExpected); if (!result.IsPassed) { return result; @@ -167,7 +190,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string } // Compare properties and fields - var expectedMembers = GetMembersToCompare(expectedType); + var expectedMembers = ReflectionHelper.GetMembersToCompare(expectedType); foreach (var member in expectedMembers) { @@ -178,7 +201,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string continue; } - var expectedValue = GetMemberValue(expected, member); + var expectedValue = ReflectionHelper.GetMemberValue(expected, member); // Check if this member's type should be ignored var memberType = member switch @@ -198,25 +221,25 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string // In partial equivalency mode, skip members that don't exist on actual if (_usePartialEquivalency) { - var actualMember = GetMemberInfo(actualType, member.Name); + var actualMember = ReflectionHelper.GetMemberInfo(actualType, member.Name); if (actualMember == null) { continue; } - actualValue = GetMemberValue(actual, actualMember); + actualValue = ReflectionHelper.GetMemberValue(actual, actualMember); } else { - var actualMember = GetMemberInfo(actualType, member.Name); + var actualMember = ReflectionHelper.GetMemberInfo(actualType, member.Name); if (actualMember == null) { return AssertionResult.Failed($"Property {memberPath} did not match{Environment.NewLine}Expected: {FormatValue(expectedValue)}{Environment.NewLine}Received: null"); } - actualValue = GetMemberValue(actual, actualMember); + actualValue = ReflectionHelper.GetMemberValue(actual, actualMember); } - var result = CompareObjects(actualValue, expectedValue, memberPath, visited); + var result = CompareObjects(actualValue, expectedValue, memberPath, visitedActual, visitedExpected); if (!result.IsPassed) { return result; @@ -226,7 +249,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string // In non-partial mode, check for extra properties on actual if (!_usePartialEquivalency) { - var actualMembers = GetMembersToCompare(actualType); + var actualMembers = ReflectionHelper.GetMembersToCompare(actualType); var expectedMemberNames = new HashSet(expectedMembers.Select(m => m.Name)); foreach (var member in actualMembers) @@ -247,7 +270,7 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string } var memberPath = string.IsNullOrEmpty(path) ? member.Name : $"{path}.{member.Name}"; - var actualValue = GetMemberValue(actual, member); + var actualValue = ReflectionHelper.GetMemberValue(actual, member); // Skip properties with null values - they're equivalent to not having the property if (actualValue == null) @@ -263,13 +286,6 @@ internal AssertionResult CompareObjects(object? actual, object? expected, string return AssertionResult.Passed; } - private static bool IsPrimitiveType(Type type) - { - return type.IsPrimitive || type.IsEnum || type == typeof(string) || type == typeof(decimal) - || type == typeof(DateTime) || type == typeof(DateTimeOffset) || type == typeof(TimeSpan) - || type == typeof(Guid); - } - private bool ShouldIgnoreType(Type type) { // Check if the type itself should be ignored @@ -288,36 +304,6 @@ private bool ShouldIgnoreType(Type type) return false; } - private static List GetMembersToCompare([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] Type type) - { - var members = new List(); - members.AddRange(type.GetProperties(BindingFlags.Public | BindingFlags.Instance)); - members.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.Instance)); - return members; - } - - private static MemberInfo? GetMemberInfo([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] Type type, string name) - { - var property = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance); - if (property != null) - { - return property; - } - - var field = type.GetField(name, BindingFlags.Public | BindingFlags.Instance); - return field; - } - - private static object? GetMemberValue(object obj, MemberInfo member) - { - return member switch - { - PropertyInfo prop => prop.GetValue(obj), - FieldInfo field => field.GetValue(obj), - _ => throw new InvalidOperationException($"Unknown member type: {member.GetType()}") - }; - } - private static string FormatValue(object? value) { if (value == null) @@ -338,38 +324,9 @@ protected override string GetExpectation() // Extract the source variable name from the expression builder // Format: "Assert.That(variableName).IsEquivalentTo(...)" var expressionString = Context.ExpressionBuilder.ToString(); - var sourceVariable = ExtractSourceVariable(expressionString); + var sourceVariable = ExpressionHelper.ExtractSourceVariable(expressionString); var expectedDesc = _expectedExpression ?? "expected value"; return $"{sourceVariable} to be equivalent to {expectedDesc}"; } - - private static string ExtractSourceVariable(string expression) - { - // Extract variable name from "Assert.That(variableName)" or similar - var thatIndex = expression.IndexOf(".That("); - if (thatIndex >= 0) - { - var startIndex = thatIndex + 6; // Length of ".That(" - var endIndex = expression.IndexOf(')', startIndex); - if (endIndex > startIndex) - { - var variable = expression.Substring(startIndex, endIndex - startIndex); - // Handle lambda expressions like "async () => ..." by returning "value" - if (variable.Contains("=>") || variable.StartsWith("()")) - { - return "value"; - } - return variable; - } - } - - return "value"; - } - - private sealed class ReferenceEqualityComparer : IEqualityComparer - { - public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); - public int GetHashCode(object obj) => System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj); - } } diff --git a/TUnit.Assertions/Extensions/Assert.cs b/TUnit.Assertions/Extensions/Assert.cs index 2e4a731f2e..c1835304b7 100644 --- a/TUnit.Assertions/Extensions/Assert.cs +++ b/TUnit.Assertions/Extensions/Assert.cs @@ -86,6 +86,20 @@ public static ValueAssertion That( return new ValueAssertion(value, expression); } + /// + /// Creates an assertion for a synchronous function that returns a collection. + /// This overload enables collection-specific assertions (IsEmpty, IsNotEmpty, HasCount, etc.) on lambda-wrapped collections. + /// Example: await Assert.That(() => GetItems()).IsEmpty(); + /// Example: await Assert.That(() => GetItems()).HasCount(5); + /// + [OverloadResolutionPriority(1)] + public static FuncCollectionAssertion That( + Func?> func, + [CallerArgumentExpression(nameof(func))] string? expression = null) + { + return new FuncCollectionAssertion(func!, expression); + } + /// /// Creates an assertion for a synchronous function. /// Example: await Assert.That(() => GetValue()).IsGreaterThan(10); @@ -97,6 +111,20 @@ public static FuncAssertion That( return new FuncAssertion(func, expression); } + /// + /// Creates an assertion for an asynchronous function that returns a collection. + /// This overload enables collection-specific assertions (IsEmpty, IsNotEmpty, HasCount, etc.) on async lambda-wrapped collections. + /// Example: await Assert.That(async () => await GetItemsAsync()).IsEmpty(); + /// Example: await Assert.That(async () => await GetItemsAsync()).HasCount(5); + /// + [OverloadResolutionPriority(1)] + public static AsyncFuncCollectionAssertion That( + Func?>> func, + [CallerArgumentExpression(nameof(func))] string? expression = null) + { + return new AsyncFuncCollectionAssertion(func!, expression); + } + /// /// Creates an assertion for an asynchronous function. /// Example: await Assert.That(async () => await GetValueAsync()).IsEqualTo(expected); diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs index 8287c6ebdd..d065942ad1 100644 --- a/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -683,10 +683,22 @@ private static string GetMemberPath(Expression 0 ? string.Join(".", parts) : "Unknown"; } + /// + /// Gets the length of the string as an integer for numeric assertions. + /// Example: await Assert.That(str).Length().IsGreaterThan(5); + /// + public static StringLengthValueAssertion Length( + this IAssertionSource source) + { + source.Context.ExpressionBuilder.Append(".Length()"); + return new StringLengthValueAssertion(source.Context); + } + /// /// Returns a wrapper for string length assertions. /// Example: await Assert.That(str).HasLength().EqualTo(5); /// + [Obsolete("Use Length() instead, which provides all numeric assertion methods. Example: Assert.That(str).Length().IsGreaterThan(5)")] public static LengthWrapper HasLength( this IAssertionSource source) { @@ -698,6 +710,7 @@ public static LengthWrapper HasLength( /// Asserts that the string has the expected length. /// Example: await Assert.That(str).HasLength(5); /// + [Obsolete("Use Length().IsEqualTo(expectedLength) instead.")] public static StringLengthAssertion HasLength( this IAssertionSource source, int expectedLength, diff --git a/TUnit.Assertions/Sources/AsyncFuncCollectionAssertion.cs b/TUnit.Assertions/Sources/AsyncFuncCollectionAssertion.cs new file mode 100644 index 0000000000..1e0512e4e7 --- /dev/null +++ b/TUnit.Assertions/Sources/AsyncFuncCollectionAssertion.cs @@ -0,0 +1,60 @@ +using System.Text; +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Sources; + +/// +/// Source assertion for asynchronous functions that return collections. +/// This is the entry point for: Assert.That(async () => await GetCollectionAsync()) +/// Combines the lazy evaluation of AsyncFuncAssertion with the collection methods of CollectionAssertionBase. +/// Enables collection assertions like IsEmpty(), IsNotEmpty(), HasCount() on async lambda-wrapped collections. +/// +public class AsyncFuncCollectionAssertion : CollectionAssertionBase, TItem>, IDelegateAssertionSource> +{ + public AsyncFuncCollectionAssertion(Func?>> func, string? expression) + : base(CreateContext(func, expression)) + { + } + + private static AssertionContext> CreateContext(Func?>> func, string? expression) + { + var expressionBuilder = new StringBuilder(); + expressionBuilder.Append($"Assert.That({expression ?? "?"})"); + var evaluationContext = new EvaluationContext>(async () => + { + try + { + var result = await func().ConfigureAwait(false); + return (result, null); + } + catch (Exception ex) + { + return (default, ex); + } + }); + return new AssertionContext>(evaluationContext, expressionBuilder); + } + + /// + /// Asserts that the function throws the specified exception type (or subclass). + /// Example: await Assert.That(async () => await GetItemsAsync()).Throws<InvalidOperationException>(); + /// + public ThrowsAssertion Throws() where TException : Exception + { + Context.ExpressionBuilder.Append($".Throws<{typeof(TException).Name}>()"); + var mappedContext = Context.MapException(); + return new ThrowsAssertion(mappedContext!); + } + + /// + /// Asserts that the function throws exactly the specified exception type (not subclasses). + /// Example: await Assert.That(async () => await GetItemsAsync()).ThrowsExactly<InvalidOperationException>(); + /// + public ThrowsExactlyAssertion ThrowsExactly() where TException : Exception + { + Context.ExpressionBuilder.Append($".ThrowsExactly<{typeof(TException).Name}>()"); + var mappedContext = Context.MapException(); + return new ThrowsExactlyAssertion(mappedContext!); + } +} diff --git a/TUnit.Assertions/Sources/CollectionAssertionBase.cs b/TUnit.Assertions/Sources/CollectionAssertionBase.cs index 233e954ca4..3025455b68 100644 --- a/TUnit.Assertions/Sources/CollectionAssertionBase.cs +++ b/TUnit.Assertions/Sources/CollectionAssertionBase.cs @@ -109,6 +109,7 @@ public CollectionContainsPredicateAssertion Contains( /// This instance method enables calling HasCount with proper type inference. /// Example: await Assert.That(list).HasCount(5); /// + [Obsolete("Use Count().IsEqualTo(expectedCount) instead.")] public CollectionCountAssertion HasCount( int expectedCount, [CallerArgumentExpression(nameof(expectedCount))] string? expression = null) @@ -122,6 +123,7 @@ public CollectionCountAssertion HasCount( /// This enables the pattern: .HasCount().GreaterThan(5) /// Example: await Assert.That(list).HasCount().EqualTo(5); /// + [Obsolete("Use Count() instead, which provides all numeric assertion methods. Example: Assert.That(list).Count().IsGreaterThan(5)")] public CountWrapper HasCount() { Context.ExpressionBuilder.Append(".HasCount()"); @@ -130,26 +132,43 @@ public CountWrapper HasCount() /// /// Gets the count of items in the collection for further numeric assertions. - /// This enables fluent assertions on the count itself. - /// Example: await Assert.That(list).Count().IsGreaterThan(5); + /// This enables fluent assertions on the count itself while preserving collection type for chaining. + /// Example: await Assert.That(list).Count().IsGreaterThan(5).And.Contains(1); /// - public CollectionCountValueAssertion Count() + public CollectionCountSource Count() { Context.ExpressionBuilder.Append(".Count()"); - return new CollectionCountValueAssertion(Context, null); + return new CollectionCountSource(Context, null); } /// - /// Gets the count of items matching the predicate for further numeric assertions. - /// This enables fluent assertions on filtered counts. - /// Example: await Assert.That(list).Count(x => x > 10).IsEqualTo(3); + /// Asserts on the count of items using an inline assertion lambda. + /// This enables compact count assertions while preserving collection type for chaining. + /// Note: For collections of int, use Count().IsEqualTo(5) instead of Count(c => c.IsEqualTo(5)) + /// to avoid ambiguity with item-filtering. + /// Example: await Assert.That(list).Count(c => c.IsEqualTo(5)).And.Contains(1); /// - public CollectionCountValueAssertion Count( - Func predicate, - [CallerArgumentExpression(nameof(predicate))] string? expression = null) + [OverloadResolutionPriority(-1)] + public CollectionCountWithInlineAssertionAssertion Count( + Func, Assertion?> countAssertion, + [CallerArgumentExpression(nameof(countAssertion))] string? expression = null) + { + Context.ExpressionBuilder.Append($".Count({expression})"); + return new CollectionCountWithInlineAssertionAssertion(Context, countAssertion); + } + + /// + /// Gets the count of items satisfying the given assertion for further numeric assertions. + /// This enables fluent assertions on filtered counts using the full assertion builder. + /// The result preserves collection type for further chaining. + /// Example: await Assert.That(list).Count(item => item.IsGreaterThan(10)).IsEqualTo(3).And.Contains(1); + /// + public CollectionCountSource Count( + Func, Assertion?> itemAssertion, + [CallerArgumentExpression(nameof(itemAssertion))] string? expression = null) { Context.ExpressionBuilder.Append($".Count({expression})"); - return new CollectionCountValueAssertion(Context, predicate); + return new CollectionCountSource(Context, itemAssertion); } /// diff --git a/TUnit.Assertions/Sources/FuncCollectionAssertion.cs b/TUnit.Assertions/Sources/FuncCollectionAssertion.cs new file mode 100644 index 0000000000..dc4117846a --- /dev/null +++ b/TUnit.Assertions/Sources/FuncCollectionAssertion.cs @@ -0,0 +1,60 @@ +using System.Text; +using TUnit.Assertions.Conditions; +using TUnit.Assertions.Core; + +namespace TUnit.Assertions.Sources; + +/// +/// Source assertion for synchronous functions that return collections. +/// This is the entry point for: Assert.That(() => GetCollection()) +/// Combines the lazy evaluation of FuncAssertion with the collection methods of CollectionAssertionBase. +/// Enables collection assertions like IsEmpty(), IsNotEmpty(), HasCount() on lambda-wrapped collections. +/// +public class FuncCollectionAssertion : CollectionAssertionBase, TItem>, IDelegateAssertionSource> +{ + public FuncCollectionAssertion(Func?> func, string? expression) + : base(CreateContext(func, expression)) + { + } + + private static AssertionContext> CreateContext(Func?> func, string? expression) + { + var expressionBuilder = new StringBuilder(); + expressionBuilder.Append($"Assert.That({expression ?? "?"})"); + var evaluationContext = new EvaluationContext>(() => + { + try + { + var result = func(); + return Task.FromResult<(IEnumerable?, Exception?)>((result, null)); + } + catch (Exception ex) + { + return Task.FromResult<(IEnumerable?, Exception?)>((default, ex)); + } + }); + return new AssertionContext>(evaluationContext, expressionBuilder); + } + + /// + /// Asserts that the function throws the specified exception type (or subclass). + /// Example: await Assert.That(() => GetItems()).Throws<InvalidOperationException>(); + /// + public ThrowsAssertion Throws() where TException : Exception + { + Context.ExpressionBuilder.Append($".Throws<{typeof(TException).Name}>()"); + var mappedContext = Context.MapException(); + return new ThrowsAssertion(mappedContext!); + } + + /// + /// Asserts that the function throws exactly the specified exception type (not subclasses). + /// Example: await Assert.That(() => GetItems()).ThrowsExactly<InvalidOperationException>(); + /// + public ThrowsExactlyAssertion ThrowsExactly() where TException : Exception + { + Context.ExpressionBuilder.Append($".ThrowsExactly<{typeof(TException).Name}>()"); + var mappedContext = Context.MapException(); + return new ThrowsExactlyAssertion(mappedContext!); + } +} diff --git a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt index adcb5e7769..7cb140d823 100644 --- a/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ArgsAsArrayTests.Test.verified.txt @@ -74,26 +74,40 @@ internal sealed class TUnit_TestProject_ArgsAsArrayTests_Params__string___TestSo switch (args.Length) { case 0: - instance.Params(new string[0]); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Params(new string[0]); + return default(global::System.Threading.Tasks.ValueTask); + } case 1: - instance.Params((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Params((args[0] is null ? null : args[0] is string[] arr ? arr : new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]) })); + return default(global::System.Threading.Tasks.ValueTask); + } case 2: - instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 3: - instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 4: - instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 5: - instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 6: - instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Params(new string[] { TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected between 0 and 1 arguments, but got {args.Length}"); } @@ -197,26 +211,40 @@ internal sealed class TUnit_TestProject_ArgsAsArrayTests_ParamsEnumerable__IEnum switch (args.Length) { case 0: - instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } case 1: - instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } case 2: - instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } case 3: - instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } case 4: - instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } case 5: - instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } case 6: - instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.ParamsEnumerable(TUnit.Core.Helpers.CastHelper.Cast>(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected between 0 and 1 arguments, but got {args.Length}"); } @@ -327,26 +355,40 @@ internal sealed class TUnit_TestProject_ArgsAsArrayTests_Following_Non_Params__i switch (args.Length) { case 1: - instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + return default(global::System.Threading.Tasks.ValueTask); + } case 2: - instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + return default(global::System.Threading.Tasks.ValueTask); + } case 3: - instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + return default(global::System.Threading.Tasks.ValueTask); + } case 4: - instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + return default(global::System.Threading.Tasks.ValueTask); + } case 5: - instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + return default(global::System.Threading.Tasks.ValueTask); + } case 6: - instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + return default(global::System.Threading.Tasks.ValueTask); + } case 7: - instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Following_Non_Params(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast>(args[1])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected between 1 and 2 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt index e574acda8e..bb479a9d9d 100644 --- a/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ArgumentWithImplicitConverterTests.Test.verified.txt @@ -77,8 +77,10 @@ internal sealed class TUnit_TestProject_ArgumentWithImplicitConverterTests_Expli switch (args.Length) { case 1: - instance.Explicit(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Explicit(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -185,8 +187,10 @@ internal sealed class TUnit_TestProject_ArgumentWithImplicitConverterTests_Impli switch (args.Length) { case 1: - instance.Implicit(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Implicit(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/AsyncMethodDataSourceDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/AsyncMethodDataSourceDrivenTests.Test.verified.txt index a4e5d2b5ae..aeee5efd13 100644 --- a/TUnit.Core.SourceGenerator.Tests/AsyncMethodDataSourceDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/AsyncMethodDataSourceDrivenTests.Test.verified.txt @@ -96,7 +96,9 @@ internal sealed class TUnit_TestProject_AsyncMethodDataSourceDrivenTests_AsyncMe switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_SingleValue(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_SingleValue(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -229,7 +231,9 @@ internal sealed class TUnit_TestProject_AsyncMethodDataSourceDrivenTests_AsyncMe switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_Tuples(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_Tuples(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -355,7 +359,9 @@ internal sealed class TUnit_TestProject_AsyncMethodDataSourceDrivenTests_AsyncMe switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_Enumerable(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_Enumerable(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -481,7 +487,9 @@ internal sealed class TUnit_TestProject_AsyncMethodDataSourceDrivenTests_AsyncMe switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_Func(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_Func(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -607,7 +615,9 @@ internal sealed class TUnit_TestProject_AsyncMethodDataSourceDrivenTests_AsyncMe switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_WithArguments(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_WithArguments(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -733,7 +743,9 @@ internal sealed class TUnit_TestProject_AsyncMethodDataSourceDrivenTests_AsyncMe switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_ExternalClass(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.AsyncMethodDataSource_ExternalClass(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -859,7 +871,9 @@ internal sealed class TUnit_TestProject_AsyncMethodDataSourceDrivenTests_ValueTa switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.ValueTaskMethodDataSource_SingleValue(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.ValueTaskMethodDataSource_SingleValue(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/ClassAndMethodArgumentsTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ClassAndMethodArgumentsTests.Test.verified.txt index 9354f67153..1d7ccc4407 100644 --- a/TUnit.Core.SourceGenerator.Tests/ClassAndMethodArgumentsTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ClassAndMethodArgumentsTests.Test.verified.txt @@ -197,7 +197,9 @@ internal sealed class TUnit_TestProject_ClassAndMethodArgumentsTests_WithMethodL switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.WithMethodLevel(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.WithMethodLevel(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -320,7 +322,9 @@ internal sealed class TUnit_TestProject_ClassAndMethodArgumentsTests_IgnoreParam switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.IgnoreParameters(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.IgnoreParameters(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests.Test.verified.txt index 3187ccef56..01fcc949d8 100644 --- a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTests.Test.verified.txt @@ -75,8 +75,10 @@ internal sealed class TUnit_TestProject_ClassDataSourceDrivenTests_DataSource_Cl switch (args.Length) { case 1: - instance.DataSource_Class(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Class(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -181,8 +183,10 @@ internal sealed class TUnit_TestProject_ClassDataSourceDrivenTests_DataSource_Cl switch (args.Length) { case 1: - instance.DataSource_Class_Generic(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Class_Generic(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -287,7 +291,9 @@ internal sealed class TUnit_TestProject_ClassDataSourceDrivenTests_IsInitialized switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_1_ClassDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_1_ClassDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -399,7 +405,9 @@ internal sealed class TUnit_TestProject_ClassDataSourceDrivenTests_IsInitialized switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_2_ClassDataSources(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_2_ClassDataSources(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -518,7 +526,9 @@ internal sealed class TUnit_TestProject_ClassDataSourceDrivenTests_IsInitialized switch (args.Length) { case 3: - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_3_ClassDataSources(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_3_ClassDataSources(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } @@ -644,7 +654,9 @@ internal sealed class TUnit_TestProject_ClassDataSourceDrivenTests_IsInitialized switch (args.Length) { case 4: - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_4_ClassDataSources(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_4_ClassDataSources(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + } default: throw new global::System.ArgumentException($"Expected exactly 4 arguments, but got {args.Length}"); } @@ -777,7 +789,9 @@ internal sealed class TUnit_TestProject_ClassDataSourceDrivenTests_IsInitialized switch (args.Length) { case 5: - return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_5_ClassDataSources(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.IsInitialized_With_5_ClassDataSources(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]))); + } default: throw new global::System.ArgumentException($"Expected exactly 5 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTestsSharedKeyed.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTestsSharedKeyed.Test.verified.txt index 23866673ce..0ed69b261f 100644 --- a/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTestsSharedKeyed.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ClassDataSourceDrivenTestsSharedKeyed.Test.verified.txt @@ -76,8 +76,10 @@ internal sealed class TUnit_TestProject_ClassDataSourceDrivenTestsSharedKeyed_Da switch (args.Length) { case 1: - instance.DataSource_Class(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Class(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -183,8 +185,10 @@ internal sealed class TUnit_TestProject_ClassDataSourceDrivenTestsSharedKeyed_Da switch (args.Length) { case 1: - instance.DataSource_Class_Generic(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Class_Generic(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/ClassTupleDataSourceDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ClassTupleDataSourceDrivenTests.Test.verified.txt index dd7c2f5de3..d5dd55b3e5 100644 --- a/TUnit.Core.SourceGenerator.Tests/ClassTupleDataSourceDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ClassTupleDataSourceDrivenTests.Test.verified.txt @@ -364,8 +364,10 @@ internal sealed class TUnit_TestProject_ClassTupleDataSourceDrivenTests_DataSour switch (args.Length) { case 3: - instance.DataSource_TupleMethod(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_TupleMethod(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt index 582a7cb6ec..1f18d3968c 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConstantArgumentsTests.Test.verified.txt @@ -75,7 +75,9 @@ internal sealed class TUnit_TestProject_ConstantArgumentsTests_String1__string_T switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.String1(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.String1(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -180,7 +182,9 @@ internal sealed class TUnit_TestProject_ConstantArgumentsTests_Int__int_TestSour switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Int(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Int(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -285,7 +289,9 @@ internal sealed class TUnit_TestProject_ConstantArgumentsTests_Double__double_Te switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Double(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Double(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -390,7 +396,9 @@ internal sealed class TUnit_TestProject_ConstantArgumentsTests_Float__float_Test switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Float(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Float(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -495,7 +503,9 @@ internal sealed class TUnit_TestProject_ConstantArgumentsTests_Long__long_TestSo switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Long(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Long(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -600,7 +610,9 @@ internal sealed class TUnit_TestProject_ConstantArgumentsTests_UInt__uint_TestSo switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.UInt(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.UInt(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -705,7 +717,9 @@ internal sealed class TUnit_TestProject_ConstantArgumentsTests_ULong__ulong_Test switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.ULong(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.ULong(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/ConstantInBaseClassTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConstantInBaseClassTests.Test.verified.txt index 33ca6c1b5e..823766c246 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConstantInBaseClassTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConstantInBaseClassTests.Test.verified.txt @@ -75,7 +75,9 @@ internal sealed class TUnit_TestProject_Bugs__1432_ConstantInBaseClassTests_Some switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.SomeTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.SomeTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/ConstantsInInterpolatedStringsTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ConstantsInInterpolatedStringsTests.Test.verified.txt index 92048fa458..640e00c2d5 100644 --- a/TUnit.Core.SourceGenerator.Tests/ConstantsInInterpolatedStringsTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ConstantsInInterpolatedStringsTests.Test.verified.txt @@ -75,7 +75,9 @@ internal sealed class TUnit_TestProject_Bugs__1432_ConstantsInInterpolatedString switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.SomeTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.SomeTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt index f28457c175..f5b91c08ec 100644 --- a/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/CustomDisplayNameTests.Test.verified.txt @@ -306,7 +306,9 @@ internal sealed class TUnit_TestProject_CustomDisplayNameTests_Test3__string_int switch (args.Length) { case 3: - return new global::System.Threading.Tasks.ValueTask(instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } @@ -436,7 +438,9 @@ internal sealed class TUnit_TestProject_CustomDisplayNameTests_MethodDataSourceT switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.MethodDataSourceTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.MethodDataSourceTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -564,7 +568,9 @@ internal sealed class TUnit_TestProject_CustomDisplayNameTests_TestParameterName switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.TestParameterNamePrefixBug(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.TestParameterNamePrefixBug(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -682,7 +688,9 @@ internal sealed class TUnit_TestProject_CustomDisplayNameTests_PasswordTest__str switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.PasswordTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.PasswordTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt index c49443a6ac..75c55bca72 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataDrivenTests.Test.verified.txt @@ -77,8 +77,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_DataSource_Method__int_T switch (args.Length) { case 1: - instance.DataSource_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -192,8 +194,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_DataSource_Method__int_s switch (args.Length) { case 2: - instance.DataSource_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -300,8 +304,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_EnumValue__TestEnum_Test switch (args.Length) { case 1: - instance.EnumValue(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.EnumValue(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -406,8 +412,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_NullValue__string__TestS switch (args.Length) { case 1: - instance.NullValue(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.NullValue(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -512,8 +520,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_EmptyString__string__Tes switch (args.Length) { case 1: - instance.EmptyString(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.EmptyString(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -618,8 +628,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_NonEmptyString__string__ switch (args.Length) { case 1: - instance.NonEmptyString(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.NonEmptyString(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -726,8 +738,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_BooleanString__bool__Tes switch (args.Length) { case 1: - instance.BooleanString(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.BooleanString(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -832,8 +846,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_Type__Type_TestSource : switch (args.Length) { case 1: - instance.Type(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Type(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -938,8 +954,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_IntegerArray__int___Test switch (args.Length) { case 1: - instance.IntegerArray(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.IntegerArray(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -1044,8 +1062,10 @@ internal sealed class TUnit_TestProject_DataDrivenTests_IntMaxValue__int_TestSou switch (args.Length) { case 1: - instance.IntMaxValue(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.IntMaxValue(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/DataSourceClassCombinedWithDataSourceMethodTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataSourceClassCombinedWithDataSourceMethodTests.Test.verified.txt index f3385cb194..e7e7b03dce 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataSourceClassCombinedWithDataSourceMethodTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataSourceClassCombinedWithDataSourceMethodTests.Test.verified.txt @@ -163,8 +163,10 @@ internal sealed class TUnit_TestProject_DataSourceClassCombinedWithDataSourceMet switch (args.Length) { case 1: - instance.DataSourceClassCombinedWithDataSourceMethodTest(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSourceClassCombinedWithDataSourceMethodTest(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt index 9bd5b95edd..85fe8ddee5 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Typed.verified.txt @@ -107,8 +107,10 @@ internal sealed class TUnit_TestProject_DataSourceGeneratorTests_GeneratedData_M switch (args.Length) { case 1: - instance.GeneratedData_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.GeneratedData_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -259,8 +261,10 @@ internal sealed class TUnit_TestProject_DataSourceGeneratorTests_GeneratedData_M switch (args.Length) { case 3: - instance.GeneratedData_Method2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.GeneratedData_Method2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } @@ -411,8 +415,10 @@ internal sealed class TUnit_TestProject_DataSourceGeneratorTests_GeneratedData_M switch (args.Length) { case 3: - instance.GeneratedData_Method3(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.GeneratedData_Method3(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Untyped.verified.txt b/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Untyped.verified.txt index bd26b22a7c..5a4bbdb52f 100644 --- a/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Untyped.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DataSourceGeneratorTests.Untyped.verified.txt @@ -95,7 +95,9 @@ internal sealed class TUnit_TestProject_AutoDataTests_Test1__string_int_double_b switch (args.Length) { case 4: - return new global::System.Threading.Tasks.ValueTask(instance.Test1(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Test1(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + } default: throw new global::System.ArgumentException($"Expected exactly 4 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt index 894195d62b..f10cb90061 100644 --- a/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/DecimalArgumentTests.Test.verified.txt @@ -84,7 +84,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_Transfer__decimal_d switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.Transfer(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Transfer(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -189,7 +191,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_SimpleDecimal__deci switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.SimpleDecimal(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.SimpleDecimal(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -294,7 +298,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_SmallDecimal__decim switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.SmallDecimal(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.SmallDecimal(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -399,7 +405,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_MaxDecimal__decimal switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.MaxDecimal(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.MaxDecimal(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -504,7 +512,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_MinDecimal__decimal switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.MinDecimal(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.MinDecimal(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -609,7 +619,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_ExplicitDecimalValu switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.ExplicitDecimalValue(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.ExplicitDecimalValue(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -728,7 +740,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_MultipleDecimals__d switch (args.Length) { case 3: - return new global::System.Threading.Tasks.ValueTask(instance.MultipleDecimals(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.MultipleDecimals(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } @@ -835,8 +849,10 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_Test__decimal_TestS switch (args.Length) { case 1: - instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -978,8 +994,10 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_TransactionDiscount switch (args.Length) { case 6: - instance.TransactionDiscountCalculations(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.TransactionDiscountCalculations(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 6 arguments, but got {args.Length}"); } @@ -1084,7 +1102,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_Equality3__decimal_ switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Equality3(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Equality3(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -1189,7 +1209,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_Equality4__decimal_ switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Equality4(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Equality4(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -1294,7 +1316,9 @@ internal sealed class TUnit_TestProject_DecimalArgumentTests_TestMethod__decimal switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.TestMethod(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.TestMethod(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/EnumMemberNamesTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/EnumMemberNamesTests.Test.verified.txt index 7b793e1562..208b4be63b 100644 --- a/TUnit.Core.SourceGenerator.Tests/EnumMemberNamesTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/EnumMemberNamesTests.Test.verified.txt @@ -77,8 +77,10 @@ internal sealed class TUnit_TestProject_Bugs__1432_EnumMemberNamesTests_SomeTest switch (args.Length) { case 1: - instance.SomeTest(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.SomeTest(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/EnumerableDataSourceDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/EnumerableDataSourceDrivenTests.Test.verified.txt index 6ba8139e62..1dbabb774a 100644 --- a/TUnit.Core.SourceGenerator.Tests/EnumerableDataSourceDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/EnumerableDataSourceDrivenTests.Test.verified.txt @@ -96,7 +96,9 @@ internal sealed class TUnit_TestProject_EnumerableDataSourceDrivenTests_DataSour switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.DataSource_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.DataSource_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -222,7 +224,9 @@ internal sealed class TUnit_TestProject_EnumerableDataSourceDrivenTests_DataSour switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.DataSource_Method2(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.DataSource_Method2(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -348,8 +352,10 @@ internal sealed class TUnit_TestProject_EnumerableDataSourceDrivenTests_DataSour switch (args.Length) { case 1: - instance.DataSource_WithBaseReturn(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_WithBaseReturn(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/EnumerableTupleDataSourceDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/EnumerableTupleDataSourceDrivenTests.Test.verified.txt index 3a9a293f16..798797ad0e 100644 --- a/TUnit.Core.SourceGenerator.Tests/EnumerableTupleDataSourceDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/EnumerableTupleDataSourceDrivenTests.Test.verified.txt @@ -132,8 +132,10 @@ internal sealed class TUnit_TestProject_EnumerableTupleDataSourceDrivenTests_Dat switch (args.Length) { case 3: - instance.DataSource_TupleMethod(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_TupleMethod(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt index bda4e49e2b..d7b8d38a28 100644 --- a/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/ExpectedArgumentTypeTests.Test.verified.txt @@ -94,7 +94,9 @@ internal sealed class TUnit_TestProject_ExpectedArgumentTypeTests_TypedArguments switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.TypedArguments(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.TypedArguments(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -220,7 +222,9 @@ internal sealed class TUnit_TestProject_ExpectedArgumentTypeTests_EnumTypes__obj switch (args.Length) { case 3: - return new global::System.Threading.Tasks.ValueTask(instance.EnumTypes(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.EnumTypes(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.Test.verified.txt index 2994f8ddff..219be38513 100644 --- a/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/InheritedTestsFromDifferentProjectTests.Test.verified.txt @@ -167,8 +167,10 @@ internal sealed class TUnit_TestProject_InheritedTestsFromDifferentProjectTests_ switch (args.Length) { case 1: - instance.GenericMethodDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.GenericMethodDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -286,8 +288,10 @@ internal sealed class TUnit_TestProject_InheritedTestsFromDifferentProjectTests_ switch (args.Length) { case 1: - instance.NonGenericMethodDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.NonGenericMethodDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/InheritsTestsTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/InheritsTestsTests.Test.verified.txt index 94339aecb1..0f192ff3f5 100644 --- a/TUnit.Core.SourceGenerator.Tests/InheritsTestsTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/InheritsTestsTests.Test.verified.txt @@ -125,7 +125,9 @@ internal sealed class TUnit_TestProject_Bugs__1924_None_BaseClass_Test__int_Test switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -268,7 +270,9 @@ internal sealed class TUnit_TestProject_Bugs__1924_None_Tests_Test__int_TestSour switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -411,7 +415,9 @@ internal sealed class TUnit_TestProject_Bugs__1924_None_Tests2_Test__int_TestSou switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -554,7 +560,9 @@ internal sealed class TUnit_TestProject_Bugs__1924_None_Tests3_Test__int_TestSou switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt index f3e953ed52..ea365df2ae 100644 --- a/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/MatrixTests.Test.verified.txt @@ -89,7 +89,9 @@ internal sealed class TUnit_TestProject_MatrixTests_MatrixTest_One__string_int_b switch (args.Length) { case 3: - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_One(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } @@ -215,7 +217,9 @@ internal sealed class TUnit_TestProject_MatrixTests_MatrixTest_Two__int_int_int_ switch (args.Length) { case 4: - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Two(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]))); + } default: throw new global::System.ArgumentException($"Expected exactly 4 arguments, but got {args.Length}"); } @@ -334,7 +338,9 @@ internal sealed class TUnit_TestProject_MatrixTests_MatrixTest_Enum__int_TestEnu switch (args.Length) { case 3: - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest_Enum(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]))); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } @@ -446,7 +452,9 @@ internal sealed class TUnit_TestProject_MatrixTests_AutoGenerateBools__string_bo switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -558,7 +566,9 @@ internal sealed class TUnit_TestProject_MatrixTests_AutoGenerateBools2__string_b switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.AutoGenerateBools2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -670,7 +680,9 @@ internal sealed class TUnit_TestProject_MatrixTests_ImplicitConversion__OneOf_Te switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.ImplicitConversion(TUnit.Core.Helpers.CastHelper.Cast>(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.ImplicitConversion(TUnit.Core.Helpers.CastHelper.Cast>(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -782,7 +794,9 @@ internal sealed class TUnit_TestProject_MatrixTests_ExcludingAutoGeneratedMatrix switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.ExcludingAutoGeneratedMatrixValues(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.ExcludingAutoGeneratedMatrixValues(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } @@ -887,7 +901,9 @@ internal sealed class TUnit_TestProject_MatrixTests_Method1__int_TestSource : gl switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Method1(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Method1(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -992,7 +1008,9 @@ internal sealed class TUnit_TestProject_MatrixTests_Method2__int_TestSource : gl switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Method2(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Method2(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -1097,7 +1115,9 @@ internal sealed class TUnit_TestProject_MatrixTests_Method3__int_TestSource : gl switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Method3(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Method3(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -1202,7 +1222,9 @@ internal sealed class TUnit_TestProject_MatrixTests_Method4__int_TestSource : gl switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Method4(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Method4(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -1317,7 +1339,9 @@ internal sealed class TUnit_TestProject_MatrixTests_Exclusion__int_int_TestSourc switch (args.Length) { case 2: - return new global::System.Threading.Tasks.ValueTask(instance.Exclusion(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Exclusion(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]))); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenTests.Test.verified.txt index 4ac45b08bb..beb11429e7 100644 --- a/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenTests.Test.verified.txt @@ -86,8 +86,10 @@ internal sealed class TUnit_TestProject_MethodDataSourceDrivenTests_DataSource_M switch (args.Length) { case 1: - instance.DataSource_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Method(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -203,8 +205,10 @@ internal sealed class TUnit_TestProject_MethodDataSourceDrivenTests_DataSource_M switch (args.Length) { case 1: - instance.DataSource_Method2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Method2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -320,8 +324,10 @@ internal sealed class TUnit_TestProject_MethodDataSourceDrivenTests_DataSource_M switch (args.Length) { case 1: - instance.DataSource_Method_WithAction(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Method_WithAction(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -452,8 +458,10 @@ internal sealed class TUnit_TestProject_MethodDataSourceDrivenTests_DataSource_M switch (args.Length) { case 1: - instance.DataSource_Method3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Method3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -611,8 +619,10 @@ internal sealed class TUnit_TestProject_MethodDataSourceDrivenTests_DataSource_M switch (args.Length) { case 1: - instance.DataSource_Method4(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_Method4(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -728,8 +738,10 @@ internal sealed class TUnit_TestProject_MethodDataSourceDrivenTests_DataSource_W switch (args.Length) { case 1: - instance.DataSource_WithBaseReturn(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_WithBaseReturn(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -855,7 +867,9 @@ internal sealed class TUnit_TestProject_MethodDataSourceDrivenTests_EnumerableFu switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.EnumerableFuncArrayTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenWithCancellationTokenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenWithCancellationTokenTests.Test.verified.txt index a842404390..0a25276f19 100644 --- a/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenWithCancellationTokenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/MethodDataSourceDrivenWithCancellationTokenTests.Test.verified.txt @@ -195,8 +195,10 @@ internal sealed class TUnit_TestProject_MethodDataSourceDrivenWithCancellationTo switch (args.Length) { case 1: - instance.MyTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.MyTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/NameOfArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/NameOfArgumentTests.Test.verified.txt index a89cf44b80..9bb78eda2d 100644 --- a/TUnit.Core.SourceGenerator.Tests/NameOfArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NameOfArgumentTests.Test.verified.txt @@ -75,7 +75,9 @@ internal sealed class TUnit_TestProject_NameOfArgumentTests_TestName__string_Tes switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.TestName(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.TestName(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt index 7683db8709..60b1e9ef0e 100644 --- a/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NullableByteArgumentTests.Test.verified.txt @@ -76,8 +76,10 @@ internal sealed class TUnit_TestProject_NullableByteArgumentTests_Test__byte__Te switch (args.Length) { case 1: - instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -190,8 +192,10 @@ internal sealed class TUnit_TestProject_NullableByteArgumentTests_Test2__byte_by switch (args.Length) { case 2: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 2 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt index f953755712..ba3b1e1df9 100644 --- a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.Test.verified.txt @@ -75,8 +75,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_Int__int_TestSource switch (args.Length) { case 1: - instance.Int(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Int(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -181,8 +183,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_Double__double_TestS switch (args.Length) { case 1: - instance.Double(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Double(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -287,8 +291,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_Float__float_TestSou switch (args.Length) { case 1: - instance.Float(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Float(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -393,8 +399,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_Long__long_TestSourc switch (args.Length) { case 1: - instance.Long(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Long(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -499,8 +507,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_ULong__ulong_TestSou switch (args.Length) { case 1: - instance.ULong(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.ULong(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -605,8 +615,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_UInt__uint_TestSourc switch (args.Length) { case 1: - instance.UInt(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.UInt(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt index f953755712..ba3b1e1df9 100644 --- a/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/NumberArgumentTests.TestDE.verified.txt @@ -75,8 +75,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_Int__int_TestSource switch (args.Length) { case 1: - instance.Int(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Int(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -181,8 +183,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_Double__double_TestS switch (args.Length) { case 1: - instance.Double(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Double(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -287,8 +291,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_Float__float_TestSou switch (args.Length) { case 1: - instance.Float(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Float(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -393,8 +399,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_Long__long_TestSourc switch (args.Length) { case 1: - instance.Long(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Long(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -499,8 +507,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_ULong__ulong_TestSou switch (args.Length) { case 1: - instance.ULong(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.ULong(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -605,8 +615,10 @@ internal sealed class TUnit_TestProject_NumberArgumentTests_UInt__uint_TestSourc switch (args.Length) { case 1: - instance.UInt(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.UInt(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/StringArgumentTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/StringArgumentTests.Test.verified.txt index ddf79f8751..6e12c5b360 100644 --- a/TUnit.Core.SourceGenerator.Tests/StringArgumentTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/StringArgumentTests.Test.verified.txt @@ -86,8 +86,10 @@ internal sealed class TUnit_TestProject_StringArgumentTests_Normal__string_TestS switch (args.Length) { case 1: - instance.Normal(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Normal(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -199,8 +201,10 @@ internal sealed class TUnit_TestProject_StringArgumentTests_Nullable__string__Te switch (args.Length) { case 1: - instance.Nullable(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Nullable(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests.Test.verified.txt index ae338ab8db..b14aa06aac 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests.Test.verified.txt @@ -87,7 +87,9 @@ internal sealed class TUnit_TestProject_Bugs__1304_Tests_TryParse_InvalidString_ switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.TryParse_InvalidString_ReturnsFailure(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None)); + { + return new global::System.Threading.Tasks.ValueTask(instance.TryParse_InvalidString_ReturnsFailure(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None)); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -196,7 +198,9 @@ internal sealed class TUnit_TestProject_Bugs__1304_Tests_Parse_InvalidString_Thr switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Parse_InvalidString_ThrowsException(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Parse_InvalidString_ThrowsException(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -302,7 +306,9 @@ internal sealed class TUnit_TestProject_Bugs__1304_Tests_TryParse_ValidString_Re switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.TryParse_ValidString_ReturnsAccountId(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.TryParse_ValidString_ReturnsAccountId(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1538.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1538.Test.verified.txt index f58a3c0aac..8851e6d423 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1538.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1538.Test.verified.txt @@ -145,8 +145,10 @@ internal sealed class TUnit_TestProject_Bugs__1538_Tests_Eight_Args__bool_string switch (args.Length) { case 8: - instance.Eight_Args(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]), TUnit.Core.Helpers.CastHelper.Cast(args[6]), TUnit.Core.Helpers.CastHelper.Cast(args[7])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Eight_Args(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]), TUnit.Core.Helpers.CastHelper.Cast(args[6]), TUnit.Core.Helpers.CastHelper.Cast(args[7])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 8 arguments, but got {args.Length}"); } @@ -377,8 +379,10 @@ internal sealed class TUnit_TestProject_Bugs__1538_Tests_SixteenArgs__bool_strin switch (args.Length) { case 16: - instance.SixteenArgs(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]), TUnit.Core.Helpers.CastHelper.Cast(args[6]), TUnit.Core.Helpers.CastHelper.Cast(args[7]), TUnit.Core.Helpers.CastHelper.Cast(args[8]), TUnit.Core.Helpers.CastHelper.Cast(args[9]), TUnit.Core.Helpers.CastHelper.Cast(args[10]), TUnit.Core.Helpers.CastHelper.Cast(args[11]), TUnit.Core.Helpers.CastHelper.Cast(args[12]), TUnit.Core.Helpers.CastHelper.Cast(args[13]), TUnit.Core.Helpers.CastHelper.Cast(args[14]), TUnit.Core.Helpers.CastHelper.Cast(args[15])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.SixteenArgs(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]), TUnit.Core.Helpers.CastHelper.Cast(args[6]), TUnit.Core.Helpers.CastHelper.Cast(args[7]), TUnit.Core.Helpers.CastHelper.Cast(args[8]), TUnit.Core.Helpers.CastHelper.Cast(args[9]), TUnit.Core.Helpers.CastHelper.Cast(args[10]), TUnit.Core.Helpers.CastHelper.Cast(args[11]), TUnit.Core.Helpers.CastHelper.Cast(args[12]), TUnit.Core.Helpers.CastHelper.Cast(args[13]), TUnit.Core.Helpers.CastHelper.Cast(args[14]), TUnit.Core.Helpers.CastHelper.Cast(args[15])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 16 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1603.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1603.Test.verified.txt index 3a4cae1c70..1dbf27639e 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1603.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1603.Test.verified.txt @@ -75,7 +75,9 @@ internal sealed class TUnit_TestProject_Bugs__1603_Tests_Casted_Integer_To_Short switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Casted_Integer_To_Short_Converts(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Casted_Integer_To_Short_Converts(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -180,7 +182,9 @@ internal sealed class TUnit_TestProject_Bugs__1603_Tests_Integer_To_Short_Conver switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Integer_To_Short_Converts(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Integer_To_Short_Converts(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1692.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1692.Test.verified.txt index dc434877b6..e33251f183 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1692.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1692.Test.verified.txt @@ -76,7 +76,9 @@ internal sealed class TUnit_TestProject_Bugs__1692_Tests_NullTest__string__TestS switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.NullTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.NullTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt index 13a93df557..2c961017ad 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1821.Test.verified.txt @@ -91,7 +91,9 @@ internal sealed class TUnit_TestProject_Bugs__1821_Tests_MethodDataSource__strin switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.MethodDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.MethodDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -212,7 +214,9 @@ internal sealed class TUnit_TestProject_Bugs__1821_Tests_MatrixDataSource__strin switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.MatrixDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.MatrixDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt index 65d6193dd3..d867b43a61 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet10_0.verified.txt @@ -165,8 +165,10 @@ internal sealed class TUnit_TestProject_Bugs__1889_DerivedTest_Test2__bool_TestS switch (args.Length) { case 1: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -273,8 +275,10 @@ internal sealed class TUnit_TestProject_Bugs__1889_DerivedTest_Test3__bool_TestS switch (args.Length) { case 1: - instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt index 65d6193dd3..d867b43a61 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet8_0.verified.txt @@ -165,8 +165,10 @@ internal sealed class TUnit_TestProject_Bugs__1889_DerivedTest_Test2__bool_TestS switch (args.Length) { case 1: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -273,8 +275,10 @@ internal sealed class TUnit_TestProject_Bugs__1889_DerivedTest_Test3__bool_TestS switch (args.Length) { case 1: - instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt index 65d6193dd3..d867b43a61 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.DotNet9_0.verified.txt @@ -165,8 +165,10 @@ internal sealed class TUnit_TestProject_Bugs__1889_DerivedTest_Test2__bool_TestS switch (args.Length) { case 1: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -273,8 +275,10 @@ internal sealed class TUnit_TestProject_Bugs__1889_DerivedTest_Test3__bool_TestS switch (args.Length) { case 1: - instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt index aabb7073ca..eab0c4bca0 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests1889.Test.Net4_7.verified.txt @@ -165,8 +165,10 @@ internal sealed class TUnit_TestProject_Bugs__1889_DerivedTest_Test2__bool_TestS switch (args.Length) { case 1: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -273,8 +275,10 @@ internal sealed class TUnit_TestProject_Bugs__1889_DerivedTest_Test3__bool_TestS switch (args.Length) { case 1: - instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test3(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt index 7106ee7e81..38dbc9f180 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2083.Test.verified.txt @@ -80,8 +80,10 @@ internal sealed class TUnit_TestProject_Bugs__2083_Tests_MyTest__long_TestSource switch (args.Length) { case 1: - instance.MyTest(TUnit.Core.Helpers.CastHelper.Cast(args[0])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.MyTest(TUnit.Core.Helpers.CastHelper.Cast(args[0])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2085.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2085.Test.verified.txt index aa612dac45..c5d52626cf 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2085.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2085.Test.verified.txt @@ -75,7 +75,9 @@ internal sealed class TUnit_TestProject_Bugs__2085_Tests_Double_SpecialConsts__d switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Double_SpecialConsts(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Double_SpecialConsts(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -180,7 +182,9 @@ internal sealed class TUnit_TestProject_Bugs__2085_Tests_Float_SpecialConsts__fl switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.Float_SpecialConsts(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + { + return new global::System.Threading.Tasks.ValueTask(instance.Float_SpecialConsts(TUnit.Core.Helpers.CastHelper.Cast(args[0]))); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt index 8d2e037a83..269c08b65f 100644 --- a/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/Tests2112.Test.verified.txt @@ -83,26 +83,40 @@ internal sealed class TUnit_TestProject_Bugs__2112_Tests_Test__int_long___TestSo switch (args.Length) { case 1: - instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[0]); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[0]); + return default(global::System.Threading.Tasks.ValueTask); + } case 2: - instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + return default(global::System.Threading.Tasks.ValueTask); + } case 3: - instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 4: - instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 5: - instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 6: - instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 7: - instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]), TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]), TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected between 1 and 2 arguments, but got {args.Length}"); } @@ -215,26 +229,40 @@ internal sealed class TUnit_TestProject_Bugs__2112_Tests_Test2__int_long___TestS switch (args.Length) { case 1: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[0]); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[0]); + return default(global::System.Threading.Tasks.ValueTask); + } case 2: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), (args[1] is null ? null : args[1] is long[] arr ? arr : new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]) })); + return default(global::System.Threading.Tasks.ValueTask); + } case 3: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 4: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 5: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 6: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]) }); + return default(global::System.Threading.Tasks.ValueTask); + } case 7: - instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]), TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.Test2(TUnit.Core.Helpers.CastHelper.Cast(args[0]), new long[] { TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2]), TUnit.Core.Helpers.CastHelper.Cast(args[3]), TUnit.Core.Helpers.CastHelper.Cast(args[4]), TUnit.Core.Helpers.CastHelper.Cast(args[5]), TUnit.Core.Helpers.CastHelper.Cast(args[6]) }); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected between 1 and 2 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt index 85223a7c65..b4e8b9f589 100644 --- a/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/TimeoutCancellationTokenTests.Test.verified.txt @@ -527,7 +527,9 @@ internal sealed class TUnit_TestProject_TimeoutCancellationTokenTests_DataTest__ switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.DataTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None)); + { + return new global::System.Threading.Tasks.ValueTask(instance.DataTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None)); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -700,7 +702,9 @@ internal sealed class TUnit_TestProject_TimeoutCancellationTokenTests_DataSource switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.DataSourceTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None)); + { + return new global::System.Threading.Tasks.ValueTask(instance.DataSourceTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None)); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } @@ -853,7 +857,9 @@ internal sealed class TUnit_TestProject_TimeoutCancellationTokenTests_MatrixTest switch (args.Length) { case 1: - return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None)); + { + return new global::System.Threading.Tasks.ValueTask(instance.MatrixTest(TUnit.Core.Helpers.CastHelper.Cast(args[0]), context?.Execution.CancellationToken ?? System.Threading.CancellationToken.None)); + } default: throw new global::System.ArgumentException($"Expected exactly 1 argument, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/TupleDataSourceDrivenTests.Test.verified.txt b/TUnit.Core.SourceGenerator.Tests/TupleDataSourceDrivenTests.Test.verified.txt index 11251b1886..581ace613b 100644 --- a/TUnit.Core.SourceGenerator.Tests/TupleDataSourceDrivenTests.Test.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/TupleDataSourceDrivenTests.Test.verified.txt @@ -100,8 +100,10 @@ internal sealed class TUnit_TestProject_TupleDataSourceDrivenTests_DataSource_Tu switch (args.Length) { case 3: - instance.DataSource_TupleMethod(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.DataSource_TupleMethod(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator.Tests/UnifiedReflectionFreeTests.Test_AotSafeDataSourceFactories.verified.txt b/TUnit.Core.SourceGenerator.Tests/UnifiedReflectionFreeTests.Test_AotSafeDataSourceFactories.verified.txt index c8205e4f6e..7190aae3f4 100644 --- a/TUnit.Core.SourceGenerator.Tests/UnifiedReflectionFreeTests.Test_AotSafeDataSourceFactories.verified.txt +++ b/TUnit.Core.SourceGenerator.Tests/UnifiedReflectionFreeTests.Test_AotSafeDataSourceFactories.verified.txt @@ -89,8 +89,10 @@ internal sealed class TUnit_TestProject_AotDataSourceTest_TestWithDataSource__in switch (args.Length) { case 3: - instance.TestWithDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); - return default(global::System.Threading.Tasks.ValueTask); + { + instance.TestWithDataSource(TUnit.Core.Helpers.CastHelper.Cast(args[0]), TUnit.Core.Helpers.CastHelper.Cast(args[1]), TUnit.Core.Helpers.CastHelper.Cast(args[2])); + return default(global::System.Threading.Tasks.ValueTask); + } default: throw new global::System.ArgumentException($"Expected exactly 3 arguments, but got {args.Length}"); } diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/AssemblyLoaderGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/AssemblyLoaderGenerator.cs index 57e91c9efe..0c30bd8995 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/AssemblyLoaderGenerator.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/AssemblyLoaderGenerator.cs @@ -70,6 +70,8 @@ private void GenerateCode(SourceProductionContext context, Compilation compilati } var sourceBuilder = new CodeWriter(); + + sourceBuilder.AppendLine("[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute]"); sourceBuilder.AppendLine($"[global::System.CodeDom.Compiler.GeneratedCode(\"TUnit\", \"{typeof(AssemblyLoaderGenerator).Assembly.GetName().Version}\")]"); using (sourceBuilder.BeginBlock("file static class AssemblyLoader" + Guid.NewGuid().ToString("N"))) { diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/DisableReflectionScannerGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/DisableReflectionScannerGenerator.cs index 9374f6526c..fcd650ee75 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/DisableReflectionScannerGenerator.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/DisableReflectionScannerGenerator.cs @@ -36,6 +36,7 @@ private void GenerateCode(SourceProductionContext context) { var sourceBuilder = new CodeWriter(); + sourceBuilder.AppendLine("[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute]"); sourceBuilder.AppendLine($"[global::System.CodeDom.Compiler.GeneratedCode(\"TUnit\", \"{typeof(DisableReflectionScannerGenerator).Assembly.GetName().Version}\")]"); using (sourceBuilder.BeginBlock("file static class DisableReflectionScanner_" + Guid.NewGuid().ToString("N"))) { diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/FullyQualifiedWithGlobalPrefixRewriter.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/FullyQualifiedWithGlobalPrefixRewriter.cs index 5937229fc4..4dce582257 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/FullyQualifiedWithGlobalPrefixRewriter.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/FullyQualifiedWithGlobalPrefixRewriter.cs @@ -224,6 +224,26 @@ public override SyntaxNode VisitTypeOfExpression(TypeOfExpressionSyntax node) { elementType = arrayTypeSymbol2.ElementType.GloballyQualified(); } + // Check parent cast expression for array type context + // Handles cases like: (MyEnum[])[MyEnum.One, MyEnum.Two] + else if (node.Parent is CastExpressionSyntax castExpr) + { + var castTypeInfo = semanticModel.GetTypeInfo(castExpr.Type); + if (castTypeInfo.Type is IArrayTypeSymbol castArrayType) + { + elementType = castArrayType.ElementType.GloballyQualified(); + } + } + // Infer element type from first element if still unknown + // Handles cases where semantic model doesn't provide array type info + else if (node.Elements.Count > 0 && node.Elements[0] is ExpressionElementSyntax firstElement) + { + var elementTypeInfo = semanticModel.GetTypeInfo(firstElement.Expression); + if (elementTypeInfo.Type is ITypeSymbol inferredType) + { + elementType = inferredType.GloballyQualified(); + } + } // Visit and rewrite each element var rewrittenElements = new List(); diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs index 38fb99fadc..1d6e6cc0bb 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/Helpers/InstanceFactoryGenerator.cs @@ -6,6 +6,37 @@ namespace TUnit.Core.SourceGenerator.CodeGenerators.Helpers; public static class InstanceFactoryGenerator { + /// + /// Generates code to create an instance of a type with proper required property handling. + /// This handles required properties that don't have data sources by initializing them with defaults. + /// + public static void GenerateInstanceCreation(CodeWriter writer, ITypeSymbol typeSymbol, string variableName) + { + var className = typeSymbol.GloballyQualified(); + // Use GetAllRequiredProperties because even properties with data sources need to be + // initialized in the object initializer to satisfy C#'s required modifier constraint. + // The actual values will be populated by the data sources at runtime. + var requiredProperties = RequiredPropertyHelper.GetAllRequiredProperties(typeSymbol).ToArray(); + + if (requiredProperties.Length == 0) + { + writer.AppendLine($"{variableName} = new {className}();"); + } + else + { + writer.AppendLine($"{variableName} = new {className}()"); + writer.AppendLine("{"); + writer.Indent(); + foreach (var property in requiredProperties) + { + var defaultValue = RequiredPropertyHelper.GetDefaultValueForType(property.Type); + writer.AppendLine($"{property.Name} = {defaultValue},"); + } + writer.Unindent(); + writer.AppendLine("};"); + } + } + public static void GenerateInstanceFactory(CodeWriter writer, ITypeSymbol typeSymbol) { GenerateInstanceFactory(writer, typeSymbol, null); diff --git a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs index 6a20095a97..9c3c88b637 100644 --- a/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/PropertyInjectionSourceGenerator.cs @@ -38,6 +38,51 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } GenerateIndividualPropertyInjectionSource(context, classData); }); + + // Also generate property sources for closed generic types used in data source attributes + // This ensures AOT compatibility for types like ErrFixture + var closedGenericTypesFromDataSources = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (node, _) => IsClassWithDataSourceProperties(node), + transform: (ctx, _) => GetClosedGenericTypesFromDataSources(ctx)) + .Where(x => x != null) + .SelectMany((types, _) => types ?? []) + .Collect() + .SelectMany((classes, _) => classes.DistinctBy(c => c.ClassSymbol, SymbolEqualityComparer.Default)) + .Combine(enabledProvider); + + context.RegisterSourceOutput(closedGenericTypesFromDataSources, (context, data) => + { + var (classData, isEnabled) = data; + if (!isEnabled) + { + return; + } + GenerateIndividualPropertyInjectionSource(context, classData); + }); + + // Third pipeline: Generate InitializerPropertyRegistry metadata for IAsyncInitializer types + // that have properties returning other IAsyncInitializer types. + // This enables AOT-compatible nested initializer discovery. + var asyncInitializerTypes = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: (node, _) => node is TypeDeclarationSyntax, + transform: (ctx, _) => GetAsyncInitializerWithInitializerProperties(ctx)) + .Where(x => x != null) + .Select((x, _) => x!) + .Collect() + .SelectMany((types, _) => types.DistinctBy(t => t.TypeSymbol, SymbolEqualityComparer.Default)) + .Combine(enabledProvider); + + context.RegisterSourceOutput(asyncInitializerTypes, (ctx, data) => + { + var (typeInfo, isEnabled) = data; + if (!isEnabled) + { + return; + } + GenerateInitializerPropertySource(ctx, typeInfo); + }); } private static bool IsClassWithDataSourceProperties(SyntaxNode node) @@ -45,6 +90,130 @@ private static bool IsClassWithDataSourceProperties(SyntaxNode node) return node is TypeDeclarationSyntax; } + /// + /// Extracts closed generic types from data source attributes (like ClassDataSource<ErrFixture<MyType>>) + /// that have injectable properties. This enables AOT-compatible property injection for generic types. + /// + private static IEnumerable? GetClosedGenericTypesFromDataSources(GeneratorSyntaxContext context) + { + var typeDecl = (TypeDeclarationSyntax)context.Node; + var semanticModel = context.SemanticModel; + + if (semanticModel.GetDeclaredSymbol(typeDecl) is not INamedTypeSymbol typeSymbol) + { + return null; + } + + var dataSourceInterface = semanticModel.Compilation.GetTypeByMetadataName("TUnit.Core.IDataSourceAttribute"); + if (dataSourceInterface == null) + { + return null; + } + + var closedGenericTypes = new List(); + var processedTypes = new HashSet(); + + // Check all properties for data source attributes that reference closed generic types + var allProperties = typeSymbol.GetMembers() + .OfType() + .Concat(typeSymbol.GetMembersIncludingBase().OfType()); + + foreach (var property in allProperties) + { + foreach (var attr in property.GetAttributes()) + { + if (attr.AttributeClass == null || + !attr.AttributeClass.AllInterfaces.Contains(dataSourceInterface, SymbolEqualityComparer.Default)) + { + continue; + } + + // Check if the attribute is a generic type like ClassDataSource + if (attr.AttributeClass.IsGenericType && attr.AttributeClass.TypeArguments.Length > 0) + { + foreach (var typeArg in attr.AttributeClass.TypeArguments) + { + if (typeArg is INamedTypeSymbol namedTypeArg && namedTypeArg.IsGenericType && + !namedTypeArg.IsUnboundGenericType && namedTypeArg.TypeParameters.Length == 0) + { + // This is a closed generic type (e.g., ErrFixture) + var fullName = namedTypeArg.ToDisplayString(); + if (!processedTypes.Add(fullName)) + { + continue; + } + + // Check if this type has properties with data source attributes + var classData = GetClassWithDataSourcePropertiesForType(namedTypeArg, semanticModel, dataSourceInterface); + if (classData != null) + { + closedGenericTypes.Add(classData); + } + } + } + } + } + } + + return closedGenericTypes.Count > 0 ? closedGenericTypes : null; + } + + /// + /// Creates a ClassWithDataSourceProperties for a specific type (used for closed generic types). + /// + private static ClassWithDataSourceProperties? GetClassWithDataSourcePropertiesForType( + INamedTypeSymbol typeSymbol, + SemanticModel semanticModel, + INamedTypeSymbol dataSourceInterface) + { + // Skip types that are not publicly accessible + if (!IsPubliclyAccessible(typeSymbol)) + { + return null; + } + + var propertiesWithDataSources = new List(); + var processedProperties = new HashSet(); + + var allProperties = typeSymbol.GetMembers() + .OfType() + .Where(CanSetProperty) + .ToList(); + + foreach (var property in allProperties) + { + if (!processedProperties.Add(property.Name)) + { + continue; + } + + foreach (var attr in property.GetAttributes()) + { + if (attr.AttributeClass != null && + attr.AttributeClass.AllInterfaces.Contains(dataSourceInterface, SymbolEqualityComparer.Default)) + { + propertiesWithDataSources.Add(new PropertyWithDataSourceAttribute + { + Property = property, + DataSourceAttribute = attr + }); + break; + } + } + } + + if (propertiesWithDataSources.Count == 0) + { + return null; + } + + return new ClassWithDataSourceProperties + { + ClassSymbol = typeSymbol, + Properties = propertiesWithDataSources.ToImmutableArray() + }; + } + private static ClassWithDataSourceProperties? GetClassWithDataSourceProperties(GeneratorSyntaxContext context) { var typeDecl = (TypeDeclarationSyntax)context.Node; @@ -489,6 +658,130 @@ private static string GetNonNullableTypeString(ITypeSymbol typeSymbol) // Alias for consistency private static string GetNonNullableTypeName(ITypeSymbol typeSymbol) => GetNonNullableTypeString(typeSymbol); + + #region IAsyncInitializer Property Discovery (for nested initializer discovery) + + /// + /// Finds types that implement IAsyncInitializer and have properties that return other IAsyncInitializer types. + /// Used for AOT-compatible nested initializer discovery during object graph traversal. + /// + private static AsyncInitializerTypeInfo? GetAsyncInitializerWithInitializerProperties(GeneratorSyntaxContext context) + { + var typeDecl = (TypeDeclarationSyntax)context.Node; + var semanticModel = context.SemanticModel; + + if (semanticModel.GetDeclaredSymbol(typeDecl) is not INamedTypeSymbol typeSymbol) + { + return null; + } + + // Skip non-public/internal types + if (!IsPubliclyAccessible(typeSymbol)) + { + return null; + } + + // Skip open generic types + if (typeSymbol.IsUnboundGenericType || typeSymbol.TypeParameters.Length > 0) + { + return null; + } + + var asyncInitializerInterface = semanticModel.Compilation.GetTypeByMetadataName("TUnit.Core.Interfaces.IAsyncInitializer"); + if (asyncInitializerInterface == null) + { + return null; + } + + // Check if this type implements IAsyncInitializer + if (!typeSymbol.AllInterfaces.Contains(asyncInitializerInterface, SymbolEqualityComparer.Default)) + { + return null; + } + + // Find properties that return IAsyncInitializer types + var initializerProperties = new List(); + + var allProperties = typeSymbol.GetMembers() + .OfType() + .Where(p => p.GetMethod != null && !p.IsStatic && !p.IsIndexer) + .ToList(); + + foreach (var property in allProperties) + { + // Check if the property type implements IAsyncInitializer + if (property.Type is INamedTypeSymbol propertyType) + { + if (propertyType.AllInterfaces.Contains(asyncInitializerInterface, SymbolEqualityComparer.Default) || + SymbolEqualityComparer.Default.Equals(propertyType, asyncInitializerInterface)) + { + initializerProperties.Add(new InitializerPropertyMetadata + { + Property = property + }); + } + } + } + + if (initializerProperties.Count == 0) + { + return null; + } + + return new AsyncInitializerTypeInfo + { + TypeSymbol = typeSymbol, + Properties = initializerProperties.ToImmutableArray() + }; + } + + /// + /// Generates source code that registers IAsyncInitializer property metadata with InitializerPropertyRegistry. + /// + private static void GenerateInitializerPropertySource(SourceProductionContext context, AsyncInitializerTypeInfo typeInfo) + { + var typeSymbol = typeInfo.TypeSymbol; + var safeName = GetSafeClassName(typeSymbol); + var fileName = $"{safeName}_InitializerProperties.g.cs"; + + var sourceBuilder = new StringBuilder(); + + sourceBuilder.AppendLine("using System;"); + sourceBuilder.AppendLine("using TUnit.Core.Discovery;"); + sourceBuilder.AppendLine(); + sourceBuilder.AppendLine("namespace TUnit.Generated;"); + sourceBuilder.AppendLine(); + + // Generate module initializer + sourceBuilder.AppendLine($"internal static class {safeName}_InitializerPropertiesInitializer"); + sourceBuilder.AppendLine("{"); + sourceBuilder.AppendLine(" [global::System.Runtime.CompilerServices.ModuleInitializer]"); + sourceBuilder.AppendLine(" public static void Initialize()"); + sourceBuilder.AppendLine(" {"); + sourceBuilder.AppendLine($" InitializerPropertyRegistry.Register(typeof({typeSymbol.GloballyQualified()}), new InitializerPropertyInfo[]"); + sourceBuilder.AppendLine(" {"); + + foreach (var propInfo in typeInfo.Properties) + { + var property = propInfo.Property; + var propertyTypeName = property.Type.GloballyQualified(); + + sourceBuilder.AppendLine(" new InitializerPropertyInfo"); + sourceBuilder.AppendLine(" {"); + sourceBuilder.AppendLine($" PropertyName = \"{property.Name}\","); + sourceBuilder.AppendLine($" PropertyType = typeof({propertyTypeName}),"); + sourceBuilder.AppendLine($" GetValue = static obj => (({typeSymbol.GloballyQualified()})obj).{property.Name}"); + sourceBuilder.AppendLine(" },"); + } + + sourceBuilder.AppendLine(" });"); + sourceBuilder.AppendLine(" }"); + sourceBuilder.AppendLine("}"); + + context.AddSource(fileName, sourceBuilder.ToString()); + } + + #endregion } internal sealed class ClassWithDataSourceProperties @@ -519,3 +812,21 @@ public int GetHashCode(ClassWithDataSourceProperties obj) return SymbolEqualityComparer.Default.GetHashCode(obj.ClassSymbol); } } + +/// +/// Model for types that implement IAsyncInitializer and have properties returning IAsyncInitializer. +/// Used for generating AOT-compatible nested initializer discovery metadata. +/// +internal sealed class AsyncInitializerTypeInfo +{ + public required INamedTypeSymbol TypeSymbol { get; init; } + public required ImmutableArray Properties { get; init; } +} + +/// +/// Metadata about a property that returns an IAsyncInitializer type. +/// +internal sealed class InitializerPropertyMetadata +{ + public required IPropertySymbol Property { get; init; } +} diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index 0f232f71d3..8e2e044856 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -937,15 +937,23 @@ private static void GenerateMethodDataSourceAttribute(CodeWriter writer, Attribu // Generate the attribute with factory // We need to manually construct this to properly add the Factory property - var attrClass = attr.AttributeClass!; - var attrTypeName = attrClass.GloballyQualified(); + + // Determine if the data source is static or instance-based + var isStatic = dataSourceMethod?.IsStatic ?? dataSourceProperty?.GetMethod?.IsStatic ?? true; + + // Use InstanceMethodDataSourceAttribute for instance-based data sources + // This implements IAccessesInstanceData which tells the engine to create an instance early + var attrTypeName = isStatic + ? "global::TUnit.Core.MethodDataSourceAttribute" + : "global::TUnit.Core.InstanceMethodDataSourceAttribute"; if (attr.ConstructorArguments is [ { Value: ITypeSymbol typeArg } _, _, .. ]) { - // MethodDataSource(Type, string) constructor + // MethodDataSource(Type, string) constructor - only available on MethodDataSourceAttribute + // For instance data sources, we still use the same constructor signature writer.AppendLine($"new {attrTypeName}(typeof({typeArg.GloballyQualified()}), \"{methodName}\")"); } else @@ -1048,7 +1056,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - writer.AppendLine($"instance = new {fullyQualifiedType}();"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};"); @@ -1080,7 +1088,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - writer.AppendLine($"instance = new {fullyQualifiedType}();"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};"); @@ -1124,7 +1132,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - writer.AppendLine($"instance = new {fullyQualifiedType}();"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};"); @@ -1167,7 +1175,7 @@ private static void GenerateMethodDataSourceFactory(CodeWriter writer, IMethodSy writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - writer.AppendLine($"instance = new {fullyQualifiedType}();"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{methodCall};"); @@ -1217,7 +1225,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - writer.AppendLine($"instance = new {fullyQualifiedType}();"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};"); @@ -1249,7 +1257,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - writer.AppendLine($"instance = new {fullyQualifiedType}();"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};"); @@ -1293,7 +1301,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - writer.AppendLine($"instance = new {fullyQualifiedType}();"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};"); @@ -1336,7 +1344,7 @@ private static void GeneratePropertyDataSourceFactory(CodeWriter writer, IProper writer.AppendLine("else"); writer.AppendLine("{"); writer.Indent(); - writer.AppendLine($"instance = new {fullyQualifiedType}();"); + writer.AppendLine("throw new global::System.InvalidOperationException(\"Instance method data source requires TestClassInstance. This should have been provided by the engine.\");"); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine($"var result = (({fullyQualifiedType})instance).{propertyAccess};"); @@ -1876,16 +1884,15 @@ private static void GenerateTypedInvokers(CodeWriter writer, TestMethodMetadata InstanceFactoryGenerator.GenerateInstanceFactory(writer, testMethod.TypeSymbol, testMethod); // Generate InvokeTypedTest for non-generic tests - var isAsync = IsAsyncMethod(testMethod.MethodSymbol); - var returnsValueTask = ReturnsValueTask(testMethod.MethodSymbol); + var returnPattern = GetReturnPattern(testMethod.MethodSymbol); if (testMethod is { IsGenericType: false, IsGenericMethod: false }) { - GenerateConcreteTestInvoker(writer, testMethod, className, methodName, isAsync, returnsValueTask, hasCancellationToken, parametersFromArgs); + GenerateConcreteTestInvoker(writer, testMethod, className, methodName, returnPattern, hasCancellationToken, parametersFromArgs); } } - private static void GenerateConcreteTestInvoker(CodeWriter writer, TestMethodMetadata testMethod, string className, string methodName, bool isAsync, bool returnsValueTask, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs) + private static void GenerateConcreteTestInvoker(CodeWriter writer, TestMethodMetadata testMethod, string className, string methodName, TestReturnPattern returnPattern, bool hasCancellationToken, IParameterSymbol[] parametersFromArgs) { // Generate InvokeTypedTest which is required by CreateExecutableTestFactory writer.AppendLine("InvokeTypedTest = static (instance, args, cancellationToken) =>"); @@ -1925,22 +1932,7 @@ private static void GenerateConcreteTestInvoker(CodeWriter writer, TestMethodMet var methodCallReconstructed = hasCancellationToken ? $"instance.{methodName}({tupleConstruction}, cancellationToken)" : $"instance.{methodName}({tupleConstruction})"; - if (isAsync) - { - if (returnsValueTask) - { - writer.AppendLine($"return {methodCallReconstructed};"); - } - else - { - writer.AppendLine($"return new global::System.Threading.Tasks.ValueTask({methodCallReconstructed});"); - } - } - else - { - writer.AppendLine($"{methodCallReconstructed};"); - writer.AppendLine("return default(global::System.Threading.Tasks.ValueTask);"); - } + GenerateReturnHandling(writer, methodCallReconstructed, returnPattern); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine("else if (args.Length == 1 && global::TUnit.Core.Helpers.DataSourceHelpers.IsTuple(args[0]))"); @@ -1950,22 +1942,7 @@ private static void GenerateConcreteTestInvoker(CodeWriter writer, TestMethodMet var methodCallDirect = hasCancellationToken ? $"instance.{methodName}(TUnit.Core.Helpers.CastHelper.Cast<{singleTupleParam.GloballyQualified()}>(args[0]), cancellationToken)" : $"instance.{methodName}(TUnit.Core.Helpers.CastHelper.Cast<{singleTupleParam.GloballyQualified()}>(args[0]))"; - if (isAsync) - { - if (returnsValueTask) - { - writer.AppendLine($"return {methodCallDirect};"); - } - else - { - writer.AppendLine($"return new global::System.Threading.Tasks.ValueTask({methodCallDirect});"); - } - } - else - { - writer.AppendLine($"{methodCallDirect};"); - writer.AppendLine("return default(global::System.Threading.Tasks.ValueTask);"); - } + GenerateReturnHandling(writer, methodCallDirect, returnPattern); writer.Unindent(); writer.AppendLine("}"); writer.AppendLine("else"); @@ -1980,22 +1957,7 @@ private static void GenerateConcreteTestInvoker(CodeWriter writer, TestMethodMet var typedMethodCall = hasCancellationToken ? $"instance.{methodName}(cancellationToken)" : $"instance.{methodName}()"; - if (isAsync) - { - if (returnsValueTask) - { - writer.AppendLine($"return {typedMethodCall};"); - } - else - { - writer.AppendLine($"return new global::System.Threading.Tasks.ValueTask({typedMethodCall});"); - } - } - else - { - writer.AppendLine($"{typedMethodCall};"); - writer.AppendLine("return default(global::System.Threading.Tasks.ValueTask);"); - } + GenerateReturnHandling(writer, typedMethodCall, returnPattern); } else { @@ -2020,6 +1982,8 @@ private static void GenerateConcreteTestInvoker(CodeWriter writer, TestMethodMet var argCount = requiredParamCount + i; writer.AppendLine($"case {argCount}:"); writer.Indent(); + writer.AppendLine("{"); + writer.Indent(); // Build the arguments to pass, handling params arrays correctly var argsToPass = TupleArgumentHelper.GenerateArgumentAccessWithParams(parametersFromArgs, "args", argCount); @@ -2031,23 +1995,9 @@ private static void GenerateConcreteTestInvoker(CodeWriter writer, TestMethodMet } var typedMethodCall = $"instance.{methodName}({string.Join(", ", argsToPass)})"; - - if (isAsync) - { - if (returnsValueTask) - { - writer.AppendLine($"return {typedMethodCall};"); - } - else - { - writer.AppendLine($"return new global::System.Threading.Tasks.ValueTask({typedMethodCall});"); - } - } - else - { - writer.AppendLine($"{typedMethodCall};"); - writer.AppendLine("return default(global::System.Threading.Tasks.ValueTask);"); - } + GenerateReturnHandling(writer, typedMethodCall, returnPattern); + writer.Unindent(); + writer.AppendLine("}"); writer.Unindent(); } @@ -2115,6 +2065,66 @@ private static bool ReturnsValueTask(IMethodSymbol method) return returnTypeName.StartsWith("System.Threading.Tasks.ValueTask"); } + private enum TestReturnPattern + { + Void, // void methods + ValueTask, // ValueTask or ValueTask + Task, // Task or Task + Unknown // F# Async, custom awaitables, etc. + } + + private static TestReturnPattern GetReturnPattern(IMethodSymbol method) + { + if (method.ReturnType.SpecialType == SpecialType.System_Void) + { + return TestReturnPattern.Void; + } + + var returnTypeName = method.ReturnType.ToDisplayString(); + + if (returnTypeName.StartsWith("System.Threading.Tasks.ValueTask")) + { + return TestReturnPattern.ValueTask; + } + + if (returnTypeName.StartsWith("System.Threading.Tasks.Task") || + returnTypeName.StartsWith("Task<")) + { + return TestReturnPattern.Task; + } + + return TestReturnPattern.Unknown; + } + + private static void GenerateReturnHandling( + CodeWriter writer, + string methodCall, + TestReturnPattern returnPattern) + { + switch (returnPattern) + { + case TestReturnPattern.Void: + writer.AppendLine($"{methodCall};"); + writer.AppendLine("return default(global::System.Threading.Tasks.ValueTask);"); + break; + + case TestReturnPattern.ValueTask: + writer.AppendLine($"return {methodCall};"); + break; + + case TestReturnPattern.Task: + writer.AppendLine($"return new global::System.Threading.Tasks.ValueTask({methodCall});"); + break; + + case TestReturnPattern.Unknown: + // F# Async, custom awaitables + writer.AppendLine($"var methodResult = {methodCall};"); + writer.AppendLine("if (methodResult == null) return default(global::System.Threading.Tasks.ValueTask);"); + writer.AppendLine("return global::TUnit.Core.AsyncConvert.ConvertObject(methodResult);"); + break; + } + } + private static void GenerateDependencies(CodeWriter writer, Compilation compilation, IMethodSymbol methodSymbol) { var dependsOnAttributes = methodSymbol.GetAttributes() diff --git a/TUnit.Core/AbstractDynamicTest.cs b/TUnit.Core/AbstractDynamicTest.cs index e086176bc6..6097c24001 100644 --- a/TUnit.Core/AbstractDynamicTest.cs +++ b/TUnit.Core/AbstractDynamicTest.cs @@ -47,11 +47,23 @@ public class DynamicDiscoveryResult : DiscoveryResult public Dictionary? Properties { get; set; } public string? DisplayName { get; set; } + + /// + /// Unique index for this dynamic test within its builder context. + /// Used to generate unique test IDs when multiple dynamic tests target the same method. + /// + public int DynamicTestIndex { get; set; } } public abstract class AbstractDynamicTest { public abstract IEnumerable GetTests(); + + /// + /// Unique index for this dynamic test within its builder context. + /// Used to generate unique test IDs when multiple dynamic tests target the same method. + /// + public int DynamicTestIndex { get; set; } } public abstract class AbstractDynamicTest<[DynamicallyAccessedMembers( @@ -89,6 +101,11 @@ public class DynamicTest<[DynamicallyAccessedMembers( /// public int? CreatorLineNumber { get; set; } + /// + /// Custom display name for this dynamic test. If not set, a default name will be generated. + /// + public string? DisplayName { get; set; } + public override IEnumerable GetTests() { var result = new DynamicDiscoveryResult @@ -99,7 +116,9 @@ public override IEnumerable GetTests() Attributes = Attributes, TestClassType = typeof(T), CreatorFilePath = CreatorFilePath, - CreatorLineNumber = CreatorLineNumber + CreatorLineNumber = CreatorLineNumber, + DynamicTestIndex = DynamicTestIndex, + DisplayName = DisplayName }; yield return result; diff --git a/TUnit.Core/Attributes/TestData/ClassDataSources.cs b/TUnit.Core/Attributes/TestData/ClassDataSources.cs index efad3790cd..d6fc652455 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSources.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSources.cs @@ -43,11 +43,11 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys) { return sharedType switch { - SharedType.None => Create(dataGeneratorMetadata), - SharedType.PerTestSession => (T) TestDataContainer.GetGlobalInstance(typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!, - SharedType.PerClass => (T) TestDataContainer.GetInstanceForClass(testClassType, typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!, - SharedType.Keyed => (T) TestDataContainer.GetInstanceForKey(key, typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!, - SharedType.PerAssembly => (T) TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, typeof(T), _ => Create(typeof(T), dataGeneratorMetadata))!, + SharedType.None => Create(), + SharedType.PerTestSession => (T) TestDataContainer.GetGlobalInstance(typeof(T), _ => Create(typeof(T)))!, + SharedType.PerClass => (T) TestDataContainer.GetInstanceForClass(testClassType, typeof(T), _ => Create(typeof(T)))!, + SharedType.Keyed => (T) TestDataContainer.GetInstanceForKey(key, typeof(T), _ => Create(typeof(T)))!, + SharedType.PerAssembly => (T) TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, typeof(T), _ => Create(typeof(T)))!, _ => throw new ArgumentOutOfRangeException() }; } @@ -56,37 +56,26 @@ private string GetKey(int index, SharedType[] sharedTypes, string[] keys) { return sharedType switch { - SharedType.None => Create(type, dataGeneratorMetadata), - SharedType.PerTestSession => TestDataContainer.GetGlobalInstance(type, _ => Create(type, dataGeneratorMetadata)), - SharedType.PerClass => TestDataContainer.GetInstanceForClass(testClassType, type, _ => Create(type, dataGeneratorMetadata)), - SharedType.Keyed => TestDataContainer.GetInstanceForKey(key!, type, _ => Create(type, dataGeneratorMetadata)), - SharedType.PerAssembly => TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, type, _ => Create(type, dataGeneratorMetadata)), + SharedType.None => Create(type), + SharedType.PerTestSession => TestDataContainer.GetGlobalInstance(type, _ => Create(type)), + SharedType.PerClass => TestDataContainer.GetInstanceForClass(testClassType, type, _ => Create(type)), + SharedType.Keyed => TestDataContainer.GetInstanceForKey(key!, type, _ => Create(type)), + SharedType.PerAssembly => TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, type, _ => Create(type)), _ => throw new ArgumentOutOfRangeException() }; } [return: NotNull] - private static T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] T>(DataGeneratorMetadata dataGeneratorMetadata) + private static T Create<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T>() { - return ((T) Create(typeof(T), dataGeneratorMetadata))!; + return ((T) Create(typeof(T)))!; } - private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata) + private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type) { - return Create(type, dataGeneratorMetadata, recursionDepth: 0); - } - - private const int MaxRecursionDepth = 10; - - private static object Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type type, DataGeneratorMetadata dataGeneratorMetadata, int recursionDepth) - { - if (recursionDepth >= MaxRecursionDepth) - { - throw new InvalidOperationException($"Maximum recursion depth ({MaxRecursionDepth}) exceeded when creating nested ClassDataSource dependencies. This may indicate a circular dependency."); - } - try { + // Just create the instance - initialization happens in the Engine return Activator.CreateInstance(type)!; } catch (TargetInvocationException targetInvocationException) diff --git a/TUnit.Core/Attributes/TestData/CombinedDataSourcesAttribute.cs b/TUnit.Core/Attributes/TestData/CombinedDataSourcesAttribute.cs index e39d59f1bb..4d8ab15ef5 100644 --- a/TUnit.Core/Attributes/TestData/CombinedDataSourcesAttribute.cs +++ b/TUnit.Core/Attributes/TestData/CombinedDataSourcesAttribute.cs @@ -81,7 +81,7 @@ namespace TUnit.Core; /// /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public sealed class CombinedDataSourcesAttribute : AsyncUntypedDataSourceGeneratorAttribute, IAccessesInstanceData +public sealed class CombinedDataSourcesAttribute : AsyncUntypedDataSourceGeneratorAttribute { protected override async IAsyncEnumerable>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata) { @@ -156,9 +156,12 @@ public sealed class CombinedDataSourcesAttribute : AsyncUntypedDataSourceGenerat if (dataSourceAttr is IAccessesInstanceData && dataGeneratorMetadata.TestClassInstance == null) { var className = dataGeneratorMetadata.TestInformation?.Class.Type.Name ?? "Unknown"; + var attrName = dataSourceAttr.GetType().Name; throw new InvalidOperationException( - $"Cannot use instance-based data source attribute on parameter '{parameterMetadata.Name}' when no instance is available. " + - $"Consider using static data sources or ensure the test class is properly instantiated."); + $"Cannot use instance-based data source '{attrName}' on parameter '{parameterMetadata.Name}' in class '{className}'. " + + $"When [CombinedDataSources] is applied at the class level (constructor parameters), all data sources must be static " + + $"because no instance exists yet. Use static [MethodDataSource] or [Arguments] instead, " + + $"or move [CombinedDataSources] to the method level if you need instance-based data sources."); } // Create metadata for this single parameter diff --git a/TUnit.Core/Attributes/TestData/InstanceMethodDataSourceSourceAttribute.cs b/TUnit.Core/Attributes/TestData/InstanceMethodDataSourceSourceAttribute.cs index 28664cdc9f..9413df05a1 100644 --- a/TUnit.Core/Attributes/TestData/InstanceMethodDataSourceSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/InstanceMethodDataSourceSourceAttribute.cs @@ -1,4 +1,25 @@ -namespace TUnit.Core; +using System.Diagnostics.CodeAnalysis; +namespace TUnit.Core; + +/// +/// A method data source attribute that requires an instance of the test class to be created first. +/// This implements IAccessesInstanceData which tells the engine to create a properly-initialized +/// instance before evaluating the data source. +/// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -public class InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) : MethodDataSourceAttribute(methodNameProvidingDataSource), IAccessesInstanceData; +public class InstanceMethodDataSourceAttribute : MethodDataSourceAttribute, IAccessesInstanceData +{ + public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) + : base(methodNameProvidingDataSource) + { + } + + public InstanceMethodDataSourceAttribute( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods)] + Type classProvidingDataSource, + string methodNameProvidingDataSource) + : base(classProvidingDataSource, methodNameProvidingDataSource) + { + } +} diff --git a/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs b/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs index efa53d7741..c6bd260fe0 100644 --- a/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/MethodDataSourceAttribute.cs @@ -82,7 +82,10 @@ public MethodDataSourceAttribute( // If we have a test class instance and no explicit class was provided, // use the instance's actual type (which will be the constructed generic type) - if (ClassProvidingDataSource == null && dataGeneratorMetadata.TestClassInstance != null) + // Skip PlaceholderInstance as it's used during discovery when the actual instance isn't created yet + if (ClassProvidingDataSource == null + && dataGeneratorMetadata.TestClassInstance != null + && dataGeneratorMetadata.TestClassInstance is not PlaceholderInstance) { targetType = dataGeneratorMetadata.TestClassInstance.GetType(); } diff --git a/TUnit.Core/Data/ScopedDictionary.cs b/TUnit.Core/Data/ScopedDictionary.cs index 7a751429ff..e5766792b0 100644 --- a/TUnit.Core/Data/ScopedDictionary.cs +++ b/TUnit.Core/Data/ScopedDictionary.cs @@ -1,6 +1,4 @@ -using TUnit.Core.Tracking; - -namespace TUnit.Core.Data; +namespace TUnit.Core.Data; public class ScopedDictionary where TScope : notnull @@ -11,14 +9,6 @@ public class ScopedDictionary { var innerDictionary = _scopedContainers.GetOrAdd(scope, static _ => new ThreadSafeDictionary()); - var obj = innerDictionary.GetOrAdd(type, factory); - - ObjectTracker.OnDisposed(obj, () => - { - innerDictionary.Remove(type); - }); - - return obj; + return innerDictionary.GetOrAdd(type, factory); } - } diff --git a/TUnit.Core/DataGeneratorMetadataCreator.cs b/TUnit.Core/DataGeneratorMetadataCreator.cs index cc0b713f70..8abfb23152 100644 --- a/TUnit.Core/DataGeneratorMetadataCreator.cs +++ b/TUnit.Core/DataGeneratorMetadataCreator.cs @@ -170,6 +170,7 @@ public static DataGeneratorMetadata CreateForPropertyInjection( PropertyMetadata propertyMetadata, MethodMetadata? methodMetadata, IDataSourceAttribute dataSource, + string testSessionId, TestContext? testContext = null, object? testClassInstance = null, TestContextEvents? events = null, @@ -193,7 +194,7 @@ public static DataGeneratorMetadata CreateForPropertyInjection( MembersToGenerate = [propertyMetadata], TestInformation = methodMetadata, Type = DataGeneratorType.Property, - TestSessionId = TestSessionContext.Current?.Id ?? "property-injection", + TestSessionId = testSessionId, TestClassInstance = testClassInstance ?? testContext?.Metadata.TestDetails.ClassInstance, ClassInstanceArguments = testContext?.Metadata.TestDetails.TestClassArguments ?? [] }; @@ -215,6 +216,7 @@ public static DataGeneratorMetadata CreateForPropertyInjection( Type containingType, MethodMetadata? methodMetadata, IDataSourceAttribute dataSource, + string testSessionId, TestContext? testContext = null, object? testClassInstance = null, TestContextEvents? events = null, @@ -235,6 +237,7 @@ public static DataGeneratorMetadata CreateForPropertyInjection( propertyMetadata, methodMetadata, dataSource, + testSessionId, testContext, testClassInstance, events, diff --git a/TUnit.Core/Discovery/InitializerPropertyRegistry.cs b/TUnit.Core/Discovery/InitializerPropertyRegistry.cs new file mode 100644 index 0000000000..fe7269a3db --- /dev/null +++ b/TUnit.Core/Discovery/InitializerPropertyRegistry.cs @@ -0,0 +1,60 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using TUnit.Core.Interfaces; + +namespace TUnit.Core.Discovery; + +/// +/// Registry for IAsyncInitializer property metadata generated at compile time. +/// Used for AOT-compatible nested initializer discovery. +/// +public static class InitializerPropertyRegistry +{ + private static readonly ConcurrentDictionary Registry = new(); + + /// + /// Registers property metadata for a type. Called by generated code. + /// + public static void Register(Type type, InitializerPropertyInfo[] properties) + { + Registry[type] = properties; + } + + /// + /// Gets property metadata for a type, or null if not registered. + /// + public static InitializerPropertyInfo[]? GetProperties(Type type) + { + return Registry.TryGetValue(type, out var properties) ? properties : null; + } + + /// + /// Checks if a type has registered property metadata. + /// + public static bool HasRegistration(Type type) + { + return Registry.ContainsKey(type); + } +} + +/// +/// Metadata about a property that returns an IAsyncInitializer. +/// +public sealed class InitializerPropertyInfo +{ + /// + /// The name of the property. + /// + public required string PropertyName { get; init; } + + /// + /// The property type. + /// + public required Type PropertyType { get; init; } + + /// + /// Delegate to get the property value from an instance. + /// + public required Func GetValue { get; init; } +} diff --git a/TUnit.Core/Discovery/ObjectGraph.cs b/TUnit.Core/Discovery/ObjectGraph.cs new file mode 100644 index 0000000000..85a1b6f960 --- /dev/null +++ b/TUnit.Core/Discovery/ObjectGraph.cs @@ -0,0 +1,124 @@ +using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using TUnit.Core.Interfaces; + +namespace TUnit.Core.Discovery; + +/// +/// Represents a discovered object graph organized by depth level. +/// +/// +/// Internal collections are stored privately and exposed as read-only views +/// to prevent callers from corrupting internal state. +/// Uses Lazy<T> for thread-safe lazy initialization of read-only views. +/// +internal sealed class ObjectGraph : IObjectGraph +{ + private readonly ConcurrentDictionary> _objectsByDepth; + private readonly HashSet _allObjects; + + // Thread-safe lazy initialization of read-only views + private readonly Lazy>> _lazyReadOnlyObjectsByDepth; + private readonly Lazy> _lazyReadOnlyAllObjects; + + // Cached sorted depths (computed once in constructor) + private readonly int[] _sortedDepthsDescending; + + /// + /// Creates a new object graph from the discovered objects. + /// + /// Objects organized by depth level. + /// All unique objects in the graph. + public ObjectGraph(ConcurrentDictionary> objectsByDepth, HashSet allObjects) + { + _objectsByDepth = objectsByDepth; + _allObjects = allObjects; + + // Compute MaxDepth and sorted depths without LINQ to reduce allocations + var keyCount = objectsByDepth.Count; + if (keyCount == 0) + { + MaxDepth = -1; + _sortedDepthsDescending = []; + } + else + { + var keys = new int[keyCount]; + objectsByDepth.Keys.CopyTo(keys, 0); + + // Find max manually + var maxDepth = int.MinValue; + foreach (var key in keys) + { + if (key > maxDepth) + { + maxDepth = key; + } + } + MaxDepth = maxDepth; + + // Sort in descending order using Array.Sort with reverse comparison + Array.Sort(keys, (a, b) => b.CompareTo(a)); + _sortedDepthsDescending = keys; + } + + // Use Lazy with ExecutionAndPublication for thread-safe single initialization + _lazyReadOnlyObjectsByDepth = new Lazy>>( + CreateReadOnlyObjectsByDepth, + LazyThreadSafetyMode.ExecutionAndPublication); + + _lazyReadOnlyAllObjects = new Lazy>( + () => _allObjects.ToArray(), + LazyThreadSafetyMode.ExecutionAndPublication); + } + + /// + public IReadOnlyDictionary> ObjectsByDepth => _lazyReadOnlyObjectsByDepth.Value; + + /// + public IReadOnlyCollection AllObjects => _lazyReadOnlyAllObjects.Value; + + /// + public int MaxDepth { get; } + + /// + public IEnumerable GetObjectsAtDepth(int depth) + { + if (!_objectsByDepth.TryGetValue(depth, out var objects)) + { + return []; + } + + // Lock and copy to prevent concurrent modification issues + lock (objects) + { + return objects.ToArray(); + } + } + + /// + public IEnumerable GetDepthsDescending() + { + // Return cached sorted depths (computed once in constructor) + return _sortedDepthsDescending; + } + + /// + /// Creates a thread-safe read-only snapshot of objects by depth. + /// + private IReadOnlyDictionary> CreateReadOnlyObjectsByDepth() + { + var dict = new Dictionary>(_objectsByDepth.Count); + + foreach (var kvp in _objectsByDepth) + { + // Lock each HashSet while copying to ensure consistency + lock (kvp.Value) + { + dict[kvp.Key] = kvp.Value.ToArray(); + } + } + + return new ReadOnlyDictionary>(dict); + } +} diff --git a/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs b/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs new file mode 100644 index 0000000000..e91cd28eb8 --- /dev/null +++ b/TUnit.Core/Discovery/ObjectGraphDiscoverer.cs @@ -0,0 +1,679 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using TUnit.Core.Helpers; +using TUnit.Core.Interfaces; +using TUnit.Core.Interfaces.SourceGenerator; +using TUnit.Core.PropertyInjection; + +namespace TUnit.Core.Discovery; + +/// +/// Represents an error that occurred during object graph discovery. +/// +/// The name of the type being inspected. +/// The name of the property that failed to access. +/// The error message. +/// The exception that occurred. +internal readonly record struct DiscoveryError(string TypeName, string PropertyName, string ErrorMessage, Exception Exception); + +/// +/// Centralized service for discovering and organizing object graphs. +/// Consolidates duplicate graph traversal logic from ObjectGraphDiscoveryService and TrackableObjectGraphProvider. +/// Follows Single Responsibility Principle - only discovers objects, doesn't modify them. +/// +/// +/// +/// This class is thread-safe and uses cached reflection for performance. +/// Objects are organized by their nesting depth in the hierarchy: +/// +/// +/// Depth 0: Root objects (class args, method args, property values) +/// Depth 1+: Nested objects found in properties of objects at previous depth +/// +/// +/// Discovery errors (e.g., property access failures) are collected in +/// rather than thrown, allowing discovery to continue despite individual property failures. +/// +/// +internal sealed class ObjectGraphDiscoverer : IObjectGraphTracker +{ + /// + /// Maximum recursion depth for object graph discovery. + /// Prevents stack overflow on deep or circular object graphs. + /// + private const int MaxRecursionDepth = 50; + + // Reference equality comparer for object tracking (ignores Equals overrides) + private static readonly Helpers.ReferenceEqualityComparer ReferenceComparer = Helpers.ReferenceEqualityComparer.Instance; + + // Types to skip during discovery (primitives, strings, system types) + private static readonly HashSet SkipTypes = + [ + typeof(string), + typeof(decimal), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(TimeSpan), + typeof(Guid) + ]; + + // Thread-safe collection of discovery errors for diagnostics + private static readonly ConcurrentBag DiscoveryErrors = []; + + /// + /// Gets all discovery errors that occurred during object graph traversal. + /// Useful for debugging and diagnostics when property access fails. + /// + /// A read-only list of discovery errors. + public static IReadOnlyList GetDiscoveryErrors() + { + return DiscoveryErrors.ToArray(); + } + + /// + /// Clears all recorded discovery errors. Call at end of test session. + /// + public static void ClearDiscoveryErrors() + { + DiscoveryErrors.Clear(); + } + + /// + /// Delegate for adding discovered objects to collections. + /// Returns true if the object was newly added (not a duplicate). + /// + private delegate bool TryAddObjectFunc(object obj, int depth); + + /// + /// Delegate for recursive discovery after an object is added. + /// + private delegate void RecurseFunc(object obj, int depth); + + /// + /// Delegate for processing a root object after it's been added. + /// + private delegate void RootObjectCallback(object obj); + + /// + public IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default) + { + var objectsByDepth = new ConcurrentDictionary>(); + var allObjects = new HashSet(ReferenceComparer); + var allObjectsLock = new object(); // Thread-safety for allObjects HashSet + var visitedObjects = new ConcurrentDictionary(ReferenceComparer); + + // Standard mode add callback (thread-safe) + bool TryAddStandard(object obj, int depth) + { + if (!visitedObjects.TryAdd(obj, 0)) + { + return false; + } + + AddToDepth(objectsByDepth, depth, obj); + lock (allObjectsLock) + { + allObjects.Add(obj); + } + + return true; + } + + // Collect root-level objects and discover nested objects + CollectRootObjects( + testContext.Metadata.TestDetails, + TryAddStandard, + obj => DiscoverNestedObjects(obj, objectsByDepth, visitedObjects, allObjects, allObjectsLock, currentDepth: 1, cancellationToken), + cancellationToken); + + return new ObjectGraph(objectsByDepth, allObjects); + } + + /// + public IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default) + { + var objectsByDepth = new ConcurrentDictionary>(); + var allObjects = new HashSet(ReferenceComparer); + var allObjectsLock = new object(); // Thread-safety for allObjects HashSet + var visitedObjects = new ConcurrentDictionary(ReferenceComparer); + + if (visitedObjects.TryAdd(rootObject, 0)) + { + AddToDepth(objectsByDepth, 0, rootObject); + lock (allObjectsLock) + { + allObjects.Add(rootObject); + } + + DiscoverNestedObjects(rootObject, objectsByDepth, visitedObjects, allObjects, allObjectsLock, currentDepth: 1, cancellationToken); + } + + return new ObjectGraph(objectsByDepth, allObjects); + } + + /// + /// Discovers objects and adds them to the existing tracked objects dictionary. + /// Used by TrackableObjectGraphProvider to populate TestContext.TrackedObjects. + /// + /// The test context to discover objects from. + /// Cancellation token for the operation. + /// The tracked objects dictionary (same as testContext.TrackedObjects). + public ConcurrentDictionary> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default) + { + var visitedObjects = testContext.TrackedObjects; + + // Collect root-level objects and discover nested objects for tracking + CollectRootObjects( + testContext.Metadata.TestDetails, + (obj, depth) => TryAddToHashSet(visitedObjects, depth, obj), + obj => DiscoverNestedObjectsForTracking(obj, visitedObjects, 1, cancellationToken), + cancellationToken); + + return visitedObjects; + } + + /// + /// Recursively discovers nested objects that have injectable properties OR implement IAsyncInitializer. + /// Uses consolidated TraverseInjectableProperties and TraverseInitializerProperties methods. + /// + private void DiscoverNestedObjects( + object obj, + ConcurrentDictionary> objectsByDepth, + ConcurrentDictionary visitedObjects, + HashSet allObjects, + object allObjectsLock, + int currentDepth, + CancellationToken cancellationToken) + { + if (!CheckRecursionDepth(obj, currentDepth)) + { + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Standard mode add callback: visitedObjects + objectsByDepth + allObjects (thread-safe) + bool TryAddStandard(object value, int depth) + { + if (!visitedObjects.TryAdd(value, 0)) + { + return false; + } + + AddToDepth(objectsByDepth, depth, value); + lock (allObjectsLock) + { + allObjects.Add(value); + } + + return true; + } + + // Recursive callback + void Recurse(object value, int depth) + { + DiscoverNestedObjects(value, objectsByDepth, visitedObjects, allObjects, allObjectsLock, depth, cancellationToken); + } + + // Traverse injectable properties (useSourceRegistrarCheck = false) + TraverseInjectableProperties(obj, TryAddStandard, Recurse, currentDepth, cancellationToken, useSourceRegistrarCheck: false); + + // Also discover nested IAsyncInitializer objects from ALL properties + TraverseInitializerProperties(obj, TryAddStandard, Recurse, currentDepth, cancellationToken); + } + + /// + /// Discovers nested objects for tracking (uses HashSet pattern for compatibility with TestContext.TrackedObjects). + /// Uses consolidated TraverseInjectableProperties and TraverseInitializerProperties methods. + /// + private void DiscoverNestedObjectsForTracking( + object obj, + ConcurrentDictionary> visitedObjects, + int currentDepth, + CancellationToken cancellationToken) + { + if (!CheckRecursionDepth(obj, currentDepth)) + { + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Tracking mode add callback: TryAddToHashSet only + bool TryAddTracking(object value, int depth) + { + return TryAddToHashSet(visitedObjects, depth, value); + } + + // Recursive callback + void Recurse(object value, int depth) + { + DiscoverNestedObjectsForTracking(value, visitedObjects, depth, cancellationToken); + } + + // Traverse injectable properties (useSourceRegistrarCheck = true for tracking mode) + TraverseInjectableProperties(obj, TryAddTracking, Recurse, currentDepth, cancellationToken, useSourceRegistrarCheck: true); + + // Also discover nested IAsyncInitializer objects from ALL properties + TraverseInitializerProperties(obj, TryAddTracking, Recurse, currentDepth, cancellationToken); + } + + /// + /// Clears all caches. Called at end of test session to release memory. + /// + public static void ClearCache() + { + PropertyCacheManager.ClearCache(); + ClearDiscoveryErrors(); + } + + /// + /// Checks if a type should be skipped during discovery. + /// + private static bool ShouldSkipType(Type type) + { + return type.IsPrimitive || + SkipTypes.Contains(type) || + type.Namespace?.StartsWith("System") == true; + } + + /// + /// Adds an object to the specified depth level. + /// Thread-safe: uses lock to protect HashSet modifications. + /// + private static void AddToDepth(ConcurrentDictionary> objectsByDepth, int depth, object obj) + { + var hashSet = objectsByDepth.GetOrAdd(depth, _ => new HashSet(ReferenceComparer)); + lock (hashSet) + { + hashSet.Add(obj); + } + } + + /// + /// Thread-safe add to HashSet at specified depth. Returns true if added (not duplicate). + /// + private static bool TryAddToHashSet(ConcurrentDictionary> dict, int depth, object obj) + { + var hashSet = dict.GetOrAdd(depth, _ => new HashSet(ReferenceComparer)); + lock (hashSet) + { + return hashSet.Add(obj); + } + } + + #region Consolidated Traversal Methods (DRY) + + /// + /// Checks recursion depth guard. Returns false if depth exceeded (caller should return early). + /// + private static bool CheckRecursionDepth(object obj, int currentDepth) + { + if (currentDepth > MaxRecursionDepth) + { +#if DEBUG + Debug.WriteLine($"[ObjectGraphDiscoverer] Max recursion depth ({MaxRecursionDepth}) reached for type '{obj.GetType().Name}'"); +#endif + return false; + } + + return true; + } + + /// + /// Unified traversal for injectable properties (from PropertyInjectionCache). + /// Eliminates duplicate code between DiscoverNestedObjects and DiscoverNestedObjectsForTracking. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Property discovery handles both AOT and reflection modes")] + private static void TraverseInjectableProperties( + object obj, + TryAddObjectFunc tryAdd, + RecurseFunc recurse, + int currentDepth, + CancellationToken cancellationToken, + bool useSourceRegistrarCheck) + { + var plan = PropertyInjectionCache.GetOrCreatePlan(obj.GetType()); + + if (!plan.HasProperties && !useSourceRegistrarCheck) + { + return; + } + + // The two modes differ in how they choose source-gen vs reflection: + // - Standard mode: Uses plan.SourceGeneratedProperties.Length > 0 + // - Tracking mode: Uses SourceRegistrar.IsEnabled + bool useSourceGen = useSourceRegistrarCheck + ? SourceRegistrar.IsEnabled + : plan.SourceGeneratedProperties.Length > 0; + + if (useSourceGen) + { + TraverseSourceGeneratedProperties(obj, plan.SourceGeneratedProperties, tryAdd, recurse, currentDepth, cancellationToken); + } + else + { + var reflectionProps = useSourceRegistrarCheck + ? plan.ReflectionProperties + : (plan.ReflectionProperties.Length > 0 ? plan.ReflectionProperties : []); + + TraverseReflectionProperties(obj, reflectionProps, tryAdd, recurse, currentDepth, cancellationToken); + } + } + + /// + /// Traverses source-generated properties and discovers nested objects. + /// Extracted for reduced complexity in TraverseInjectableProperties. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Property discovery handles both AOT and reflection modes")] + private static void TraverseSourceGeneratedProperties( + object obj, + PropertyInjectionMetadata[] sourceGeneratedProperties, + TryAddObjectFunc tryAdd, + RecurseFunc recurse, + int currentDepth, + CancellationToken cancellationToken) + { + foreach (var metadata in sourceGeneratedProperties) + { + cancellationToken.ThrowIfCancellationRequested(); + var property = metadata.ContainingType.GetProperty(metadata.PropertyName); + if (property == null || !property.CanRead) + { + continue; + } + + var value = property.GetValue(obj); + if (value != null && tryAdd(value, currentDepth)) + { + recurse(value, currentDepth + 1); + } + } + } + + /// + /// Traverses reflection-based properties and discovers nested objects. + /// Extracted for reduced complexity in TraverseInjectableProperties. + /// + private static void TraverseReflectionProperties( + object obj, + (PropertyInfo Property, IDataSourceAttribute DataSource)[] reflectionProperties, + TryAddObjectFunc tryAdd, + RecurseFunc recurse, + int currentDepth, + CancellationToken cancellationToken) + { + foreach (var prop in reflectionProperties) + { + cancellationToken.ThrowIfCancellationRequested(); + var value = prop.Property.GetValue(obj); + if (value != null && tryAdd(value, currentDepth)) + { + recurse(value, currentDepth + 1); + } + } + } + + /// + /// Unified traversal for IAsyncInitializer objects (from all properties). + /// Eliminates duplicate code between DiscoverNestedInitializerObjects and DiscoverNestedInitializerObjectsForTracking. + /// + /// + /// Uses source-generated metadata when available (AOT-compatible), falling back to reflection otherwise. + /// Exceptions during property access are propagated to the caller with context about + /// which type/property failed. This ensures data source initialization failures are + /// properly reported as test failures rather than silently swallowed. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection fallback for nested initializers. In AOT, source-gen handles primary discovery.")] + [UnconditionalSuppressMessage("Trimming", "IL2070", Justification = "Reflection fallback for nested initializers. In AOT, source-gen handles primary discovery.")] + [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Reflection fallback for nested initializers. In AOT, source-gen handles primary discovery.")] + private static void TraverseInitializerProperties( + object obj, + TryAddObjectFunc tryAdd, + RecurseFunc recurse, + int currentDepth, + CancellationToken cancellationToken) + { + var type = obj.GetType(); + + if (ShouldSkipType(type)) + { + return; + } + + // Try source-generated metadata first (AOT-compatible) + var registeredProperties = InitializerPropertyRegistry.GetProperties(type); + if (registeredProperties != null) + { + TraverseRegisteredInitializerProperties(obj, type, registeredProperties, tryAdd, recurse, currentDepth, cancellationToken); + return; + } + + // Fall back to reflection (non-AOT path) + TraverseInitializerPropertiesViaReflection(obj, type, tryAdd, recurse, currentDepth, cancellationToken); + } + + /// + /// Traverses IAsyncInitializer properties using source-generated metadata (AOT-compatible). + /// + private static void TraverseRegisteredInitializerProperties( + object obj, + Type type, + InitializerPropertyInfo[] properties, + TryAddObjectFunc tryAdd, + RecurseFunc recurse, + int currentDepth, + CancellationToken cancellationToken) + { + foreach (var propInfo in properties) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + var value = propInfo.GetValue(obj); + if (value == null) + { + continue; + } + + // Only discover IAsyncInitializer objects + if (value is IAsyncInitializer && tryAdd(value, currentDepth)) + { + recurse(value, currentDepth + 1); + } + } + catch (OperationCanceledException) + { + throw; // Propagate cancellation + } + catch (Exception ex) + { + // Record error for diagnostics (still available via GetDiscoveryErrors()) + DiscoveryErrors.Add(new DiscoveryError(type.Name, propInfo.PropertyName, ex.Message, ex)); + + // Propagate the exception with context about which property failed + // This ensures data source failures are reported as test failures + throw DataSourceException.FromNestedFailure( + $"Failed to access property '{propInfo.PropertyName}' on type '{type.Name}' during object graph discovery. " + + $"This may indicate that a data source or its nested dependencies failed to initialize. " + + $"See inner exception for details.", + ex); + } + } + } + + /// + /// Traverses IAsyncInitializer properties using reflection (fallback for non-source-generated types). + /// + [UnconditionalSuppressMessage("Trimming", "IL2067", Justification = "Reflection fallback path. Types without source-generated metadata may not work in trimmed apps.")] + private static void TraverseInitializerPropertiesViaReflection( + object obj, + Type type, + TryAddObjectFunc tryAdd, + RecurseFunc recurse, + int currentDepth, + CancellationToken cancellationToken) + { + var properties = PropertyCacheManager.GetCachedProperties(type); + + foreach (var property in properties) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Only access properties whose declared type could be IAsyncInitializer. + // This prevents triggering side effects from unrelated property getters + // (e.g., WebApplicationFactory.Server which starts the test host when accessed). + // Properties typed as object/interfaces not extending IAsyncInitializer won't be + // checked - users should properly type their properties or use data source attributes. + if (!typeof(IAsyncInitializer).IsAssignableFrom(property.PropertyType)) + { + continue; + } + + try + { + var value = property.GetValue(obj); + if (value == null) + { + continue; + } + + // Only discover IAsyncInitializer objects + if (value is IAsyncInitializer && tryAdd(value, currentDepth)) + { + recurse(value, currentDepth + 1); + } + } + catch (OperationCanceledException) + { + throw; // Propagate cancellation + } + catch (Exception ex) + { + // Record error for diagnostics (still available via GetDiscoveryErrors()) + DiscoveryErrors.Add(new DiscoveryError(type.Name, property.Name, ex.Message, ex)); + + // Propagate the exception with context about which property failed + // This ensures data source failures are reported as test failures + throw DataSourceException.FromNestedFailure( + $"Failed to access property '{property.Name}' on type '{type.Name}' during object graph discovery. " + + $"This may indicate that a data source or its nested dependencies failed to initialize. " + + $"See inner exception for details.", + ex); + } + } + } + + /// + /// Collects root-level objects (class args, method args, properties) from test details. + /// Eliminates duplicate loops in DiscoverObjectGraph and DiscoverAndTrackObjects. + /// + /// + /// For injected properties, only DIRECT test class properties (including inherited) are added at depth 0. + /// Nested properties (properties of injected objects) are discovered through normal + /// graph traversal at appropriate depths (1+), ensuring correct initialization order + /// for nested IAsyncInitializer dependencies. See GitHub issue #4032. + /// + private static void CollectRootObjects( + TestDetails testDetails, + TryAddObjectFunc tryAdd, + RootObjectCallback onRootObjectAdded, + CancellationToken cancellationToken) + { + // Process class arguments + ProcessRootCollection(testDetails.TestClassArguments, tryAdd, onRootObjectAdded, cancellationToken); + + // Process method arguments + ProcessRootCollection(testDetails.TestMethodArguments, tryAdd, onRootObjectAdded, cancellationToken); + + // Build set of types in the test class hierarchy (for identifying direct properties) + var hierarchyTypes = GetTypeHierarchy(testDetails.ClassType); + + // Process ONLY direct test class injected properties at depth 0. + // Nested properties will be discovered through normal graph traversal at depth 1+. + // This ensures proper initialization order for nested IAsyncInitializer dependencies. + foreach (var kvp in testDetails.TestClassInjectedPropertyArguments) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (kvp.Value == null) + { + continue; + } + + // Check if this property belongs to the test class hierarchy (not nested object properties) + // Cache key format: "{DeclaringType.FullName}.{PropertyName}" + if (IsDirectProperty(kvp.Key, hierarchyTypes)) + { + if (tryAdd(kvp.Value, 0)) + { + onRootObjectAdded(kvp.Value); + } + } + } + } + + /// + /// Gets all types in the inheritance hierarchy from the given type up to (but not including) object. + /// + private static HashSet GetTypeHierarchy(Type type) + { + var result = new HashSet(); + var currentType = type; + + while (currentType != null && currentType != typeof(object)) + { + if (currentType.FullName != null) + { + result.Add(currentType.FullName); + } + + currentType = currentType.BaseType; + } + + return result; + } + + /// + /// Determines if a cache key represents a direct property (belonging to test class hierarchy) + /// vs a nested property (belonging to an injected object). + /// Cache key format: "{DeclaringType.FullName}.{PropertyName}" + /// + private static bool IsDirectProperty(string cacheKey, HashSet hierarchyTypes) + { + // Find the last dot to separate type from property name + var lastDotIndex = cacheKey.LastIndexOf('.'); + if (lastDotIndex <= 0) + { + return true; // Malformed key, treat as direct + } + + var declaringTypeName = cacheKey.Substring(0, lastDotIndex); + return hierarchyTypes.Contains(declaringTypeName); + } + + /// + /// Processes a collection of root objects, adding them to the graph and invoking callback. + /// Extracted to eliminate duplicate iteration patterns in CollectRootObjects. + /// + private static void ProcessRootCollection( + IEnumerable collection, + TryAddObjectFunc tryAdd, + RootObjectCallback onRootObjectAdded, + CancellationToken cancellationToken) + { + foreach (var item in collection) + { + cancellationToken.ThrowIfCancellationRequested(); + if (item != null && tryAdd(item, 0)) + { + onRootObjectAdded(item); + } + } + } + + #endregion +} diff --git a/TUnit.Core/Discovery/PropertyCacheManager.cs b/TUnit.Core/Discovery/PropertyCacheManager.cs new file mode 100644 index 0000000000..0073c4215a --- /dev/null +++ b/TUnit.Core/Discovery/PropertyCacheManager.cs @@ -0,0 +1,123 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace TUnit.Core.Discovery; + +/// +/// Manages cached property reflection results for object graph discovery. +/// Extracted from ObjectGraphDiscoverer to follow Single Responsibility Principle. +/// +/// +/// +/// This class caches arrays per type to avoid repeated reflection calls. +/// Includes automatic cache cleanup when size exceeds to prevent memory leaks. +/// +/// +/// Thread-safe: Uses and for coordination. +/// +/// +internal static class PropertyCacheManager +{ + /// + /// Maximum size for the property cache before cleanup is triggered. + /// Prevents unbounded memory growth in long-running test sessions. + /// + private const int MaxCacheSize = 10000; + + // Cache for GetProperties() results per type - eliminates repeated reflection calls + private static readonly ConcurrentDictionary PropertyCache = new(); + + // Flag to coordinate cache cleanup (prevents multiple threads cleaning simultaneously) + private static int _cleanupInProgress; + + /// + /// Gets cached properties for a type, filtering to only readable non-indexed properties. + /// Includes periodic cache cleanup to prevent unbounded memory growth. + /// + /// The type to get properties for. + /// An array of readable, non-indexed properties for the type. + [UnconditionalSuppressMessage("Trimming", "IL2111", + Justification = "CreatePropertyArray is called with a type that has the required DynamicallyAccessedMembers annotation from the caller.")] + public static PropertyInfo[] GetCachedProperties( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] + Type type) + { + // Periodic cleanup if cache grows too large to prevent memory leaks + // Use Interlocked to ensure only one thread performs cleanup at a time + if (PropertyCache.Count > MaxCacheSize && + Interlocked.CompareExchange(ref _cleanupInProgress, 1, 0) == 0) + { + try + { + // Double-check after acquiring cleanup flag + if (PropertyCache.Count > MaxCacheSize) + { + // Use ToArray() to get a true snapshot for thread-safe enumeration + // This prevents issues with concurrent modifications during iteration + var allKeys = PropertyCache.Keys.ToArray(); + var removeCount = Math.Min(allKeys.Length / 2, MaxCacheSize / 2); + + for (var i = 0; i < removeCount; i++) + { + PropertyCache.TryRemove(allKeys[i], out _); + } +#if DEBUG + Debug.WriteLine($"[PropertyCacheManager] PropertyCache exceeded {MaxCacheSize} entries, cleared {removeCount} entries"); +#endif + } + } + finally + { + Interlocked.Exchange(ref _cleanupInProgress, 0); + } + } + + return PropertyCache.GetOrAdd(type, CreatePropertyArray); + } + + private static PropertyInfo[] CreatePropertyArray( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] + Type t) + { + // Use explicit loops instead of LINQ to avoid allocations in hot path + var allProps = t.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + // First pass: count eligible properties + var eligibleCount = 0; + foreach (var p in allProps) + { + if (p.CanRead && p.GetIndexParameters().Length == 0) + { + eligibleCount++; + } + } + + // Second pass: fill result array + var result = new PropertyInfo[eligibleCount]; + var i = 0; + foreach (var p in allProps) + { + if (p.CanRead && p.GetIndexParameters().Length == 0) + { + result[i++] = p; + } + } + + return result; + } + + /// + /// Clears the property cache. Called at end of test session to release memory. + /// + public static void ClearCache() + { + PropertyCache.Clear(); + } + + /// + /// Gets the current number of cached types. Useful for diagnostics. + /// + public static int CacheCount => PropertyCache.Count; +} diff --git a/TUnit.Core/DynamicTestBuilderContext.cs b/TUnit.Core/DynamicTestBuilderContext.cs index f73e84f4c9..90867d914c 100644 --- a/TUnit.Core/DynamicTestBuilderContext.cs +++ b/TUnit.Core/DynamicTestBuilderContext.cs @@ -11,6 +11,8 @@ public class DynamicTestBuilderContext [ ]; + private int _nextIndex = 1; // Start at 1 to match loop index convention used by regular data sources + public DynamicTestBuilderContext(string filePath, int lineNumber) { FilePath = filePath; @@ -34,6 +36,9 @@ public void AddTest(AbstractDynamicTest test) testWithLocation.CreatorLineNumber = LineNumber; } + // Assign unique index for test ID generation + test.DynamicTestIndex = _nextIndex++; + _tests.Add(test); } } diff --git a/TUnit.Core/DynamicTestMetadata.cs b/TUnit.Core/DynamicTestMetadata.cs new file mode 100644 index 0000000000..eb3d4e77f3 --- /dev/null +++ b/TUnit.Core/DynamicTestMetadata.cs @@ -0,0 +1,125 @@ +using System.Diagnostics.CodeAnalysis; + +namespace TUnit.Core; + +/// +/// Unified metadata class for dynamic tests. +/// Used by both AOT/source-generated mode and reflection mode for tests created via +/// DynamicTestBuilderAttribute or runtime test variant creation. +/// +public sealed class DynamicTestMetadata : TestMetadata, IDynamicTestMetadata +{ + private readonly DynamicDiscoveryResult _dynamicResult; + + public DynamicTestMetadata(DynamicDiscoveryResult dynamicResult) + { + _dynamicResult = dynamicResult; + } + + public int DynamicTestIndex => _dynamicResult.DynamicTestIndex; + + public string? DisplayName => _dynamicResult.DisplayName; + + /// + /// Parent test ID for test variants created at runtime. + /// + public string? ParentTestId => _dynamicResult.ParentTestId; + + /// + /// Relationship to parent test for test variants. + /// + public Enums.TestRelationship? Relationship => _dynamicResult.Relationship; + + /// + /// Custom properties for test variants. + /// + public Dictionary? Properties => _dynamicResult.Properties; + + [field: AllowNull, MaybeNull] + public override Func CreateExecutableTestFactory + { + get => field ??= CreateExecutableTest; + } + + private AbstractExecutableTest CreateExecutableTest(ExecutableTestCreationContext context, TestMetadata metadata) + { + var modifiedContext = new ExecutableTestCreationContext + { + TestId = context.TestId, + DisplayName = _dynamicResult.DisplayName ?? context.DisplayName, + Arguments = _dynamicResult.TestMethodArguments ?? context.Arguments, + ClassArguments = _dynamicResult.TestClassArguments ?? context.ClassArguments, + Context = context.Context, + TestClassInstanceFactory = context.TestClassInstanceFactory + }; + + // Apply runtime test variant properties + if (_dynamicResult.ParentTestId != null) + { + modifiedContext.Context.ParentTestId = _dynamicResult.ParentTestId; + } + + if (_dynamicResult.Relationship.HasValue) + { + modifiedContext.Context.Relationship = _dynamicResult.Relationship.Value; + } + + if (_dynamicResult.Properties != null) + { + foreach (var kvp in _dynamicResult.Properties) + { + modifiedContext.Context.StateBag.Items[kvp.Key] = kvp.Value; + } + } + + // Create instance factory + var createInstance = async (TestContext testContext) => + { + // If we have a factory from discovery, use it + if (modifiedContext.TestClassInstanceFactory != null) + { + return await modifiedContext.TestClassInstanceFactory(); + } + + // Check if there's a ClassConstructor to use + if (testContext.ClassConstructor != null) + { + var testBuilderContext = TestBuilderContext.FromTestContext(testContext, null); + var classConstructorMetadata = new ClassConstructorMetadata + { + TestSessionId = metadata.TestSessionId, + TestBuilderContext = testBuilderContext + }; + + return await testContext.ClassConstructor.Create(metadata.TestClassType, classConstructorMetadata); + } + + // Fall back to default instance factory + var instance = metadata.InstanceFactory(Type.EmptyTypes, modifiedContext.ClassArguments); + + // Handle property injections + foreach (var propertyInjection in metadata.PropertyInjections) + { + var value = propertyInjection.ValueFactory(); + propertyInjection.Setter(instance, value); + } + + return instance; + }; + + var invokeTest = metadata.TestInvoker ?? throw new InvalidOperationException("Test invoker is null"); + + return new ExecutableTest(createInstance, + async (instance, args, ctx, ct) => + { + await invokeTest(instance, args); + }) + { + TestId = modifiedContext.TestId, + Metadata = metadata, + Arguments = modifiedContext.Arguments, + ClassArguments = modifiedContext.ClassArguments, + Context = modifiedContext.Context + }; + } +} diff --git a/TUnit.Core/EngineCancellationToken.cs b/TUnit.Core/EngineCancellationToken.cs index 3ed56c20b1..395cc7c5dd 100644 --- a/TUnit.Core/EngineCancellationToken.cs +++ b/TUnit.Core/EngineCancellationToken.cs @@ -33,6 +33,7 @@ internal void Initialise(CancellationToken cancellationToken) { #endif Console.CancelKeyPress += OnCancelKeyPress; + AppDomain.CurrentDomain.ProcessExit += OnProcessExit; #if NET5_0_OR_GREATER } #endif @@ -71,6 +72,21 @@ private void OnCancelKeyPress(object? sender, ConsoleCancelEventArgs e) e.Cancel = true; } + private void OnProcessExit(object? sender, EventArgs e) + { + // Process is exiting (SIGTERM, kill, etc.) - trigger cancellation to execute After hooks + // Note: ProcessExit runs on a background thread with limited time (~3 seconds on Windows) + // The After hooks registered via CancellationToken.Register() will execute when we cancel + if (!CancellationTokenSource.IsCancellationRequested) + { + CancellationTokenSource.Cancel(); + + // Give After hooks a brief moment to execute via registered callbacks + // ProcessExit has limited time, so we can only wait briefly + Task.Delay(TimeSpan.FromMilliseconds(500)).GetAwaiter().GetResult(); + } + } + /// /// Disposes the cancellation token source. /// @@ -82,6 +98,7 @@ public void Dispose() { #endif Console.CancelKeyPress -= OnCancelKeyPress; + AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; #if NET5_0_OR_GREATER } #endif diff --git a/TUnit.Core/Helpers/ClassConstructorHelper.cs b/TUnit.Core/Helpers/ClassConstructorHelper.cs index 136a6132b1..8430cfb63b 100644 --- a/TUnit.Core/Helpers/ClassConstructorHelper.cs +++ b/TUnit.Core/Helpers/ClassConstructorHelper.cs @@ -50,9 +50,9 @@ public static class ClassConstructorHelper return null; } - // Use the ClassConstructor to create the instance - var classConstructorType = classConstructorAttribute.ClassConstructorType; - var classConstructor = (IClassConstructor)Activator.CreateInstance(classConstructorType)!; + // Reuse existing ClassConstructor if already set, otherwise create new instance + var classConstructor = testBuilderContext.ClassConstructor + ?? (IClassConstructor)Activator.CreateInstance(classConstructorAttribute.ClassConstructorType)!; testBuilderContext.ClassConstructor = classConstructor; diff --git a/TUnit.Core/Helpers/Counter.cs b/TUnit.Core/Helpers/Counter.cs index d3df14af6f..e4ca4b759a 100644 --- a/TUnit.Core/Helpers/Counter.cs +++ b/TUnit.Core/Helpers/Counter.cs @@ -2,6 +2,11 @@ namespace TUnit.Core.Helpers; +/// +/// Thread-safe counter with event notification. +/// Captures event handler BEFORE state change to prevent race conditions +/// where subscribers miss notifications that occur during subscription. +/// [DebuggerDisplay("Count = {CurrentCount}")] public class Counter { @@ -11,34 +16,91 @@ public class Counter public int Increment() { + // Capture handler BEFORE state change to ensure all subscribers + // at the time of the change are notified (prevents TOCTOU race) + var handler = _onCountChanged; var newCount = Interlocked.Increment(ref _count); - var handler = _onCountChanged; - handler?.Invoke(this, newCount); + RaiseEventSafely(handler, newCount); return newCount; } public int Decrement() { + // Capture handler BEFORE state change to ensure all subscribers + // at the time of the change are notified (prevents TOCTOU race) + var handler = _onCountChanged; var newCount = Interlocked.Decrement(ref _count); - var handler = _onCountChanged; - handler?.Invoke(this, newCount); + RaiseEventSafely(handler, newCount); return newCount; } + /// + /// Adds a value to the counter. Use Increment/Decrement for single-step changes. + /// + /// The value to add (can be positive or negative). + /// The new count after the addition. + /// Thrown if the resulting count is negative, indicating a logic error. public int Add(int value) { + // Capture handler BEFORE state change to ensure all subscribers + // at the time of the change are notified (prevents TOCTOU race) + var handler = _onCountChanged; var newCount = Interlocked.Add(ref _count, value); - var handler = _onCountChanged; - handler?.Invoke(this, newCount); + // Guard against reference count going negative - indicates a bug in calling code + if (newCount < 0) + { + throw new InvalidOperationException( + $"Counter went below zero (result: {newCount}). This indicates a bug in the reference counting logic."); + } + + RaiseEventSafely(handler, newCount); return newCount; } + /// + /// Raises the event safely, ensuring all subscribers are notified even if some throw exceptions. + /// Collects all exceptions and throws AggregateException if any occurred. + /// + private void RaiseEventSafely(EventHandler? handler, int newCount) + { + if (handler == null) + { + return; + } + + var invocationList = handler.GetInvocationList(); + List? exceptions = null; + + foreach (var subscriber in invocationList) + { + try + { + ((EventHandler)subscriber).Invoke(this, newCount); + } + catch (Exception ex) + { + exceptions ??= []; + exceptions.Add(ex); + +#if DEBUG + Debug.WriteLine($"[Counter] Exception in OnCountChanged subscriber: {ex.Message}"); +#endif + } + } + + // If any subscribers threw, aggregate and rethrow after all are notified + if (exceptions?.Count > 0) + { + throw new AggregateException("One or more OnCountChanged subscribers threw an exception.", exceptions); + } + } + public int CurrentCount => Interlocked.CompareExchange(ref _count, 0, 0); public event EventHandler? OnCountChanged diff --git a/TUnit.Core/Helpers/DataSourceHelpers.cs b/TUnit.Core/Helpers/DataSourceHelpers.cs index d77e6aff47..fc7ba158e6 100644 --- a/TUnit.Core/Helpers/DataSourceHelpers.cs +++ b/TUnit.Core/Helpers/DataSourceHelpers.cs @@ -2,6 +2,7 @@ using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using TUnit.Core.Interfaces; namespace TUnit.Core.Helpers; @@ -177,8 +178,9 @@ public static T InvokeIfFunc(object? value) // If it's a Func, invoke it first var actualData = InvokeIfFunc(data); - // Initialize the object if it implements IAsyncInitializer - await ObjectInitializer.InitializeAsync(actualData); + // During discovery, only IAsyncDiscoveryInitializer objects are initialized. + // Regular IAsyncInitializer objects are deferred to Execution phase. + await ObjectInitializer.InitializeForDiscoveryAsync(actualData); return actualData; } @@ -197,7 +199,8 @@ public static T InvokeIfFunc(object? value) if (enumerator.MoveNext()) { var value = enumerator.Current; - await ObjectInitializer.InitializeAsync(value); + // Discovery: only IAsyncDiscoveryInitializer + await ObjectInitializer.InitializeForDiscoveryAsync(value); return value; } @@ -224,14 +227,16 @@ public static T InvokeIfFunc(object? value) if (enumerator.MoveNext()) { var value = enumerator.Current; - await ObjectInitializer.InitializeAsync(value); + // Discovery: only IAsyncDiscoveryInitializer + await ObjectInitializer.InitializeForDiscoveryAsync(value); return value; } return null; } - // For non-enumerable types, just initialize and return - await ObjectInitializer.InitializeAsync(actualData); + // During discovery, only IAsyncDiscoveryInitializer objects are initialized. + // Regular IAsyncInitializer objects are deferred to Execution phase. + await ObjectInitializer.InitializeForDiscoveryAsync(actualData); return actualData; } @@ -561,6 +566,7 @@ public static void RegisterTypeCreator(Func> containingType, testInformation, dataSourceAttribute, + testSessionId, TestContext.Current, TestContext.Current?.Metadata.TestDetails.ClassInstance, TestContext.Current?.InternalEvents, @@ -578,8 +584,8 @@ public static void RegisterTypeCreator(Func> { var value = args[0]; - // Initialize the value if it implements IAsyncInitializer - await ObjectInitializer.InitializeAsync(value); + // Discovery: only IAsyncDiscoveryInitializer + await ObjectInitializer.InitializeForDiscoveryAsync(value); return value; } diff --git a/TUnit.Core/Helpers/Disposer.cs b/TUnit.Core/Helpers/Disposer.cs index 772b49de07..a70033bb79 100644 --- a/TUnit.Core/Helpers/Disposer.cs +++ b/TUnit.Core/Helpers/Disposer.cs @@ -1,9 +1,18 @@ -using TUnit.Core.Logging; +using TUnit.Core.Interfaces; +using TUnit.Core.Logging; namespace TUnit.Core.Helpers; -internal class Disposer(ILogger logger) +/// +/// Disposes objects asynchronously with logging. +/// Implements IDisposer for Dependency Inversion Principle compliance. +/// +internal class Disposer(ILogger logger) : IDisposer { + /// + /// Disposes an object and propagates any exceptions. + /// Exceptions are logged but NOT swallowed - callers must handle them. + /// public async ValueTask DisposeAsync(object? obj) { try @@ -19,10 +28,15 @@ public async ValueTask DisposeAsync(object? obj) } catch (Exception e) { + // Log the error for diagnostics if (logger != null) { await logger.LogErrorAsync(e); } + + // Propagate the exception - don't silently swallow disposal failures + // Callers can catch and aggregate if disposing multiple objects + throw; } } } diff --git a/TUnit.Core/Helpers/ParallelTaskHelper.cs b/TUnit.Core/Helpers/ParallelTaskHelper.cs new file mode 100644 index 0000000000..81f05f06d4 --- /dev/null +++ b/TUnit.Core/Helpers/ParallelTaskHelper.cs @@ -0,0 +1,165 @@ +namespace TUnit.Core.Helpers; + +/// +/// Helper methods for parallel task execution without LINQ allocations. +/// Provides optimized patterns for executing async operations in parallel. +/// Exceptions are aggregated in AggregateException when multiple tasks fail. +/// +public static class ParallelTaskHelper +{ + /// + /// Executes an async action for each item in an array, in parallel. + /// Uses pre-allocated task array to avoid LINQ allocations. + /// + /// The type of items to process. + /// The array of items to process. + /// The async action to execute for each item. + /// A task that completes when all items have been processed. + public static async Task ForEachAsync(T[] items, Func action) + { + if (items.Length == 0) + { + return; + } + + var tasks = new Task[items.Length]; + for (var i = 0; i < items.Length; i++) + { + tasks[i] = action(items[i]); + } + + await Task.WhenAll(tasks); + } + + /// + /// Executes an async action for each item in an array, in parallel, with cancellation support. + /// Uses pre-allocated task array to avoid LINQ allocations. + /// + /// The type of items to process. + /// The array of items to process. + /// The async action to execute for each item. + /// Token to cancel the operation. + /// A task that completes when all items have been processed. + public static async Task ForEachAsync(T[] items, Func action, CancellationToken cancellationToken) + { + if (items.Length == 0) + { + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var tasks = new Task[items.Length]; + for (var i = 0; i < items.Length; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + tasks[i] = action(items[i], cancellationToken); + } + + await Task.WhenAll(tasks); + } + + /// + /// Executes an async action for each item in an array, in parallel, with an index. + /// Uses pre-allocated task array to avoid LINQ allocations. + /// + /// The type of items to process. + /// The array of items to process. + /// The async action to execute for each item with its index. + /// A task that completes when all items have been processed. + public static async Task ForEachWithIndexAsync(T[] items, Func action) + { + if (items.Length == 0) + { + return; + } + + var tasks = new Task[items.Length]; + for (var i = 0; i < items.Length; i++) + { + tasks[i] = action(items[i], i); + } + + await Task.WhenAll(tasks); + } + + /// + /// Executes an async action for each item in an array, in parallel, with an index and cancellation support. + /// Uses pre-allocated task array to avoid LINQ allocations. + /// + /// The type of items to process. + /// The array of items to process. + /// The async action to execute for each item with its index. + /// Token to cancel the operation. + /// A task that completes when all items have been processed. + public static async Task ForEachWithIndexAsync(T[] items, Func action, CancellationToken cancellationToken) + { + if (items.Length == 0) + { + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var tasks = new Task[items.Length]; + for (var i = 0; i < items.Length; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + tasks[i] = action(items[i], i, cancellationToken); + } + + await Task.WhenAll(tasks); + } + + /// + /// Executes an async action for each item in a list, in parallel. + /// Uses pre-allocated task array to avoid LINQ allocations. + /// + /// The type of items to process. + /// The list of items to process. + /// The async action to execute for each item. + /// A task that completes when all items have been processed. + public static async Task ForEachAsync(IReadOnlyList items, Func action) + { + if (items.Count == 0) + { + return; + } + + var tasks = new Task[items.Count]; + for (var i = 0; i < items.Count; i++) + { + tasks[i] = action(items[i]); + } + + await Task.WhenAll(tasks); + } + + /// + /// Executes an async action for each item in a list, in parallel, with cancellation support. + /// Uses pre-allocated task array to avoid LINQ allocations. + /// + /// The type of items to process. + /// The list of items to process. + /// The async action to execute for each item. + /// Token to cancel the operation. + /// A task that completes when all items have been processed. + public static async Task ForEachAsync(IReadOnlyList items, Func action, CancellationToken cancellationToken) + { + if (items.Count == 0) + { + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + var tasks = new Task[items.Count]; + for (var i = 0; i < items.Count; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + tasks[i] = action(items[i], cancellationToken); + } + + await Task.WhenAll(tasks); + } +} diff --git a/TUnit.Core/Helpers/ReferenceEqualityComparer.cs b/TUnit.Core/Helpers/ReferenceEqualityComparer.cs index 2c26bfb5d1..16da75d77b 100644 --- a/TUnit.Core/Helpers/ReferenceEqualityComparer.cs +++ b/TUnit.Core/Helpers/ReferenceEqualityComparer.cs @@ -1,7 +1,34 @@ -namespace TUnit.Core.Helpers; +using System.Runtime.CompilerServices; -public class ReferenceEqualityComparer : IEqualityComparer +namespace TUnit.Core.Helpers; + +/// +/// Compares objects by reference identity, not value equality. +/// Uses RuntimeHelpers.GetHashCode to get identity-based hash codes. +/// +public sealed class ReferenceEqualityComparer : IEqualityComparer { + /// + /// Singleton instance to avoid repeated allocations. + /// + public static readonly ReferenceEqualityComparer Instance = new(); + + /// + /// Private constructor to enforce singleton pattern. + /// + private ReferenceEqualityComparer() + { + } + + /// + /// Compares two objects by reference identity. + /// + /// + /// The 'new' keyword is used because this method explicitly implements + /// IEqualityComparer<object>.Equals with nullable parameters, which + /// hides the inherited static Object.Equals(object?, object?) method. + /// This is intentional and provides the correct behavior for reference equality. + /// public new bool Equals(object? x, object? y) { return ReferenceEquals(x, y); @@ -9,6 +36,8 @@ public class ReferenceEqualityComparer : IEqualityComparer public int GetHashCode(object obj) { - return obj.GetHashCode(); + // Use RuntimeHelpers.GetHashCode for identity-based hash code + // This returns the same value as Object.GetHashCode() would if not overridden + return RuntimeHelpers.GetHashCode(obj); } } diff --git a/TUnit.Core/IDynamicTestMetadata.cs b/TUnit.Core/IDynamicTestMetadata.cs index e28c56dc8d..4b4f2223ad 100644 --- a/TUnit.Core/IDynamicTestMetadata.cs +++ b/TUnit.Core/IDynamicTestMetadata.cs @@ -1,8 +1,18 @@ namespace TUnit.Core; /// -/// Marker interface for dynamic test metadata that should bypass normal data source processing +/// Interface for dynamic test metadata that should bypass normal data source processing /// public interface IDynamicTestMetadata { + /// + /// Unique index for this dynamic test within its builder context. + /// Used to generate unique test IDs when multiple dynamic tests target the same method. + /// + int DynamicTestIndex { get; } + + /// + /// Custom display name for this dynamic test. If null, a default name will be generated. + /// + string? DisplayName { get; } } \ No newline at end of file diff --git a/TUnit.Core/Interfaces/IAsyncDiscoveryInitializer.cs b/TUnit.Core/Interfaces/IAsyncDiscoveryInitializer.cs new file mode 100644 index 0000000000..8c9f4f1dfb --- /dev/null +++ b/TUnit.Core/Interfaces/IAsyncDiscoveryInitializer.cs @@ -0,0 +1,28 @@ +namespace TUnit.Core.Interfaces; + +/// +/// Defines a contract for types that require asynchronous initialization during test discovery. +/// +/// +/// +/// Unlike which runs during test execution, +/// implementations of this interface are initialized during the test discovery phase. +/// This enables data sources (such as InstanceMethodDataSource) to access +/// fully-initialized objects when generating test cases. +/// +/// +/// Common use cases include: +/// +/// Starting Docker containers before test case enumeration +/// Connecting to databases to discover parameterized test data +/// Initializing fixtures that provide data for test case generation +/// +/// +/// +/// This interface extends , meaning the same +/// method is used. The framework +/// guarantees exactly-once initialization semantics - objects will not be +/// re-initialized during test execution. +/// +/// +public interface IAsyncDiscoveryInitializer : IAsyncInitializer; diff --git a/TUnit.Core/Interfaces/IDisposer.cs b/TUnit.Core/Interfaces/IDisposer.cs new file mode 100644 index 0000000000..039665dbae --- /dev/null +++ b/TUnit.Core/Interfaces/IDisposer.cs @@ -0,0 +1,16 @@ +namespace TUnit.Core.Interfaces; + +/// +/// Interface for disposing objects. +/// Follows Dependency Inversion Principle - high-level modules depend on this abstraction. +/// +public interface IDisposer +{ + /// + /// Disposes an object asynchronously. + /// Implementations should propagate exceptions - callers handle aggregation. + /// + /// The object to dispose. + /// A task representing the disposal operation. + ValueTask DisposeAsync(object? obj); +} diff --git a/TUnit.Core/Interfaces/IInitializationCallback.cs b/TUnit.Core/Interfaces/IInitializationCallback.cs new file mode 100644 index 0000000000..4e6449dd6e --- /dev/null +++ b/TUnit.Core/Interfaces/IInitializationCallback.cs @@ -0,0 +1,33 @@ +using System.Collections.Concurrent; + +namespace TUnit.Core.Interfaces; + +/// +/// Defines a callback interface for object initialization during property injection. +/// +/// +/// +/// This interface is used to break circular dependencies between property injection +/// and initialization services. Property injectors can call back to the initialization +/// service without directly depending on it. +/// +/// +internal interface IInitializationCallback +{ + /// + /// Ensures an object is fully initialized (property injection + IAsyncInitializer). + /// + /// The type of object to initialize. + /// The object to initialize. + /// Shared object bag for the test context. + /// Method metadata for the test. Can be null. + /// Test context events for tracking. + /// A token to monitor for cancellation requests. + /// The initialized object. + ValueTask EnsureInitializedAsync( + T obj, + ConcurrentDictionary? objectBag = null, + MethodMetadata? methodMetadata = null, + TestContextEvents? events = null, + CancellationToken cancellationToken = default) where T : notnull; +} diff --git a/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs b/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs new file mode 100644 index 0000000000..8ac4586be3 --- /dev/null +++ b/TUnit.Core/Interfaces/IObjectGraphDiscoverer.cs @@ -0,0 +1,132 @@ +using System.Collections.Concurrent; + +namespace TUnit.Core.Interfaces; + +/// +/// Defines a contract for discovering object graphs from test contexts. +/// Pure query interface - only reads and returns data, does not modify state. +/// +/// +/// +/// Object graph discovery is used to find all objects that need initialization or disposal, +/// organized by their nesting depth in the object hierarchy. +/// +/// +/// The discoverer traverses: +/// +/// Test class constructor arguments +/// Test method arguments +/// Injected property values +/// Nested objects that implement +/// +/// +/// +/// For tracking operations that modify TestContext.TrackedObjects, see . +/// +/// +internal interface IObjectGraphDiscoverer +{ + /// + /// Discovers all objects from a test context, organized by depth level. + /// + /// The test context to discover objects from. + /// Optional cancellation token for long-running discovery. + /// + /// An containing all discovered objects organized by depth. + /// Depth 0 contains root objects (arguments and property values). + /// Higher depths contain nested objects. + /// + IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default); + + /// + /// Discovers nested objects from a single root object, organized by depth. + /// + /// The root object to discover nested objects from. + /// Optional cancellation token for long-running discovery. + /// + /// An containing all discovered objects organized by depth. + /// Depth 0 contains the root object itself. + /// Higher depths contain nested objects. + /// + IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default); + + /// + /// Discovers objects and populates the test context's tracked objects dictionary directly. + /// Used for efficient object tracking without intermediate allocations. + /// + /// The test context to discover objects from and populate. + /// Optional cancellation token for long-running discovery. + /// + /// The tracked objects dictionary (same as testContext.TrackedObjects) populated with discovered objects. + /// + /// + /// This method modifies testContext.TrackedObjects directly. For pure query operations, + /// use instead. + /// + ConcurrentDictionary> DiscoverAndTrackObjects(TestContext testContext, CancellationToken cancellationToken = default); +} + +/// +/// Marker interface for object graph tracking operations. +/// Extends with operations that modify state. +/// +/// +/// +/// This interface exists to support Interface Segregation Principle: +/// clients that only need query operations can depend on , +/// while clients that need tracking can depend on . +/// +/// +/// Currently inherits all methods from . +/// The distinction exists for semantic clarity and future extensibility. +/// +/// +internal interface IObjectGraphTracker : IObjectGraphDiscoverer +{ + // All methods inherited from IObjectGraphDiscoverer + // This interface provides semantic clarity for tracking operations +} + +/// +/// Represents a discovered object graph organized by depth level. +/// +/// +/// Collections are exposed as read-only to prevent callers from corrupting internal state. +/// Use and for safe iteration. +/// +internal interface IObjectGraph +{ + /// + /// Gets objects organized by depth (0 = root arguments, 1+ = nested). + /// + /// + /// Returns a read-only view. Use for iteration. + /// + IReadOnlyDictionary> ObjectsByDepth { get; } + + /// + /// Gets all unique objects in the graph. + /// + /// + /// Returns a read-only view to prevent modification. + /// + IReadOnlyCollection AllObjects { get; } + + /// + /// Gets the maximum nesting depth (-1 if empty). + /// + int MaxDepth { get; } + + /// + /// Gets objects at a specific depth level. + /// + /// The depth level to retrieve objects from. + /// An enumerable of objects at the specified depth, or empty if none exist. + IEnumerable GetObjectsAtDepth(int depth); + + /// + /// Gets depth levels in descending order (deepest first). + /// + /// An enumerable of depth levels ordered from deepest to shallowest. + IEnumerable GetDepthsDescending(); +} diff --git a/TUnit.Core/Interfaces/IObjectInitializationService.cs b/TUnit.Core/Interfaces/IObjectInitializationService.cs new file mode 100644 index 0000000000..6e5eb70338 --- /dev/null +++ b/TUnit.Core/Interfaces/IObjectInitializationService.cs @@ -0,0 +1,67 @@ +namespace TUnit.Core.Interfaces; + +/// +/// Defines a contract for managing object initialization with phase awareness. +/// +/// +/// +/// This service provides thread-safe, deduplicated initialization of objects that implement +/// or . +/// +/// +/// The service supports two initialization phases: +/// +/// Discovery phase: Only objects are initialized +/// Execution phase: All objects are initialized +/// +/// +/// +internal interface IObjectInitializationService +{ + /// + /// Initializes an object during the execution phase. + /// + /// The object to initialize. If null or not an , no action is taken. + /// A token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// + /// + /// This method is thread-safe and ensures that each object is initialized exactly once. + /// Multiple concurrent calls for the same object will share the same initialization task. + /// + /// + ValueTask InitializeAsync(object? obj, CancellationToken cancellationToken = default); + + /// + /// Initializes an object during the discovery phase. + /// + /// The object to initialize. If null or not an , no action is taken. + /// A token to monitor for cancellation requests. + /// A representing the asynchronous operation. + /// + /// + /// Only objects implementing are initialized during discovery. + /// Regular objects are deferred to execution phase. + /// + /// + ValueTask InitializeForDiscoveryAsync(object? obj, CancellationToken cancellationToken = default); + + /// + /// Checks if an object has been successfully initialized. + /// + /// The object to check. + /// True if the object has been initialized successfully; otherwise, false. + /// + /// Returns false if the object is null, not an , + /// has not been initialized yet, or if initialization failed. + /// + bool IsInitialized(object? obj); + + /// + /// Clears the initialization cache. + /// + /// + /// Called at the end of a test session to release resources. + /// + void ClearCache(); +} diff --git a/TUnit.Core/Interfaces/ITestOutput.cs b/TUnit.Core/Interfaces/ITestOutput.cs index 3af148ff1f..0562502e4d 100644 --- a/TUnit.Core/Interfaces/ITestOutput.cs +++ b/TUnit.Core/Interfaces/ITestOutput.cs @@ -47,6 +47,16 @@ public interface ITestOutput /// The artifact to attach void AttachArtifact(Artifact artifact); + /// + /// Attaches a file as an artifact to this test. + /// Artifacts are preserved after test execution. + /// Thread-safe for concurrent calls. + /// + /// The path to the file to attach + /// Optional display name for the artifact. Defaults to the file name. + /// Optional description of the artifact + void AttachArtifact(string filePath, string? displayName = null, string? description = null); + /// /// Gets all standard output written during test execution as a single string. /// diff --git a/TUnit.Core/Interfaces/SourceGenerator/PropertyInjectionMetadata.cs b/TUnit.Core/Interfaces/SourceGenerator/PropertyInjectionMetadata.cs index d4c8309fc3..bea4f11a9f 100644 --- a/TUnit.Core/Interfaces/SourceGenerator/PropertyInjectionMetadata.cs +++ b/TUnit.Core/Interfaces/SourceGenerator/PropertyInjectionMetadata.cs @@ -24,7 +24,7 @@ public sealed class PropertyInjectionMetadata /// /// Gets the type that contains the property (the parent class). /// - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] public required Type ContainingType { get; init; } /// diff --git a/TUnit.Core/ObjectInitializer.cs b/TUnit.Core/ObjectInitializer.cs index 362445816e..46c52923e8 100644 --- a/TUnit.Core/ObjectInitializer.cs +++ b/TUnit.Core/ObjectInitializer.cs @@ -1,52 +1,131 @@ -using System.Runtime.CompilerServices; +using System.Collections.Concurrent; +using TUnit.Core.Helpers; using TUnit.Core.Interfaces; +using TUnit.Core.Services; namespace TUnit.Core; -public static class ObjectInitializer +/// +/// Static facade for initializing objects that implement . +/// Provides thread-safe, deduplicated initialization with explicit phase control. +/// +/// +/// +/// Use during test discovery - only objects are initialized. +/// Use during test execution - all objects are initialized. +/// +/// +/// For dependency injection scenarios, use directly. +/// +/// +internal static class ObjectInitializer { - private static readonly ConditionalWeakTable _initializationTasks = new(); - private static readonly Lock _lock = new(); + // Use Lazy pattern to ensure InitializeAsync is called exactly once per object, + // even under contention. GetOrAdd's factory can be called multiple times, but with + // Lazy + ExecutionAndPublication mode, only one initialization actually runs. + private static readonly ConcurrentDictionary> InitializationTasks = + new(Helpers.ReferenceEqualityComparer.Instance); - internal static bool IsInitialized(object? obj) + /// + /// Initializes an object during the discovery phase. + /// Only objects implementing IAsyncDiscoveryInitializer are initialized. + /// Regular IAsyncInitializer objects are skipped (deferred to execution phase). + /// Thread-safe with deduplication - safe to call multiple times. + /// + /// The object to potentially initialize. + /// Cancellation token. + internal static ValueTask InitializeForDiscoveryAsync(object? obj, CancellationToken cancellationToken = default) { - if (obj is not IAsyncInitializer) + // During discovery, only initialize IAsyncDiscoveryInitializer + if (obj is not IAsyncDiscoveryInitializer asyncDiscoveryInitializer) { - return false; + return default; } - lock (_lock) + return InitializeCoreAsync(obj, asyncDiscoveryInitializer, cancellationToken); + } + + /// + /// Initializes an object during the execution phase. + /// All objects implementing IAsyncInitializer are initialized. + /// Thread-safe with deduplication - safe to call multiple times. + /// + /// The object to potentially initialize. + /// Cancellation token. + internal static ValueTask InitializeAsync(object? obj, CancellationToken cancellationToken = default) + { + if (obj is not IAsyncInitializer asyncInitializer) { - return _initializationTasks.TryGetValue(obj, out var task) && task.IsCompleted; + return default; } + + return InitializeCoreAsync(obj, asyncInitializer, cancellationToken); } - public static async ValueTask InitializeAsync(object? obj, CancellationToken cancellationToken = default) + /// + /// Checks if an object has been successfully initialized by ObjectInitializer. + /// + /// The object to check. + /// True if the object has been initialized successfully; otherwise, false. + /// + /// Returns false if the object is null, not an , + /// has not been initialized yet, or if initialization failed. + /// + internal static bool IsInitialized(object? obj) { - if (obj is IAsyncInitializer asyncInitializer) + if (obj is not IAsyncInitializer) { - await GetInitializationTask(obj, asyncInitializer, cancellationToken); + return false; } + + // Use Status == RanToCompletion to ensure we don't return true for faulted/canceled tasks + // (IsCompletedSuccessfully is not available in netstandard2.0) + // With Lazy, we need to check if the Lazy has a value AND that value completed successfully + return InitializationTasks.TryGetValue(obj, out var lazyTask) && + lazyTask.IsValueCreated && + lazyTask.Value.Status == TaskStatus.RanToCompletion; } - private static async Task GetInitializationTask(object obj, IAsyncInitializer asyncInitializer, CancellationToken cancellationToken) + /// + /// Clears the initialization cache. + /// + /// + /// Called at the end of a test session to release resources. + /// + internal static void ClearCache() { - Task initializationTask; + InitializationTasks.Clear(); + } - lock (_lock) + private static async ValueTask InitializeCoreAsync( + object obj, + IAsyncInitializer asyncInitializer, + CancellationToken cancellationToken) + { + // Use Lazy with ExecutionAndPublication mode to ensure InitializeAsync + // is called exactly once, even under contention. GetOrAdd's factory may be + // called multiple times, but Lazy ensures only one initialization runs. + var lazyTask = InitializationTasks.GetOrAdd(obj, + _ => new Lazy( + () => asyncInitializer.InitializeAsync(), + LazyThreadSafetyMode.ExecutionAndPublication)); + + try { - if (_initializationTasks.TryGetValue(obj, out var existingTask)) - { - initializationTask = existingTask; - } - else - { - initializationTask = asyncInitializer.InitializeAsync(); - _initializationTasks.Add(obj, initializationTask); - } + // Wait for initialization with cancellation support + await lazyTask.Value.WaitAsync(cancellationToken); + } + catch (OperationCanceledException) + { + // Propagate cancellation without modification + throw; + } + catch + { + // Remove failed initialization from cache to allow retry + // This is important for transient failures that may succeed on retry + InitializationTasks.TryRemove(obj, out _); + throw; } - - // Wait for initialization with cancellation support - await initializationTask.WaitAsync(cancellationToken); } } diff --git a/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationContext.cs b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationContext.cs index 12aef89941..85d4f6ae80 100644 --- a/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationContext.cs +++ b/TUnit.Core/PropertyInjection/Initialization/PropertyInitializationContext.cs @@ -7,6 +7,7 @@ namespace TUnit.Core.PropertyInjection.Initialization; /// /// Encapsulates all context needed for property initialization. /// Follows Single Responsibility Principle by being a pure data container. +/// Provides factory methods to reduce duplication when creating contexts (DRY). /// internal sealed class PropertyInitializationContext { @@ -84,4 +85,124 @@ internal sealed class PropertyInitializationContext /// Parent object for nested properties. /// public object? ParentInstance { get; init; } + + #region Factory Methods (DRY) + + /// + /// Creates a context for source-generated property injection. + /// + public static PropertyInitializationContext ForSourceGenerated( + object instance, + PropertyInjectionMetadata metadata, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects, + TestContext? testContext, + bool isNestedProperty = false) + { + return new PropertyInitializationContext + { + Instance = instance, + SourceGeneratedMetadata = metadata, + PropertyName = metadata.PropertyName, + PropertyType = metadata.PropertyType, + PropertySetter = metadata.SetProperty, + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = visitedObjects, + TestContext = testContext, + IsNestedProperty = isNestedProperty + }; + } + + /// + /// Creates a context for reflection-based property injection. + /// + public static PropertyInitializationContext ForReflection( + object instance, + PropertyInfo property, + IDataSourceAttribute dataSource, + Action propertySetter, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects, + TestContext? testContext, + bool isNestedProperty = false) + { + return new PropertyInitializationContext + { + Instance = instance, + PropertyInfo = property, + DataSource = dataSource, + PropertyName = property.Name, + PropertyType = property.PropertyType, + PropertySetter = propertySetter, + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = visitedObjects, + TestContext = testContext, + IsNestedProperty = isNestedProperty + }; + } + + /// + /// Creates a context for caching during registration (uses placeholder instance). + /// + public static PropertyInitializationContext ForCaching( + PropertyInjectionMetadata metadata, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + TestContext testContext) + { + return new PropertyInitializationContext + { + Instance = PlaceholderInstance.Instance, + SourceGeneratedMetadata = metadata, + PropertyName = metadata.PropertyName, + PropertyType = metadata.PropertyType, + PropertySetter = metadata.SetProperty, + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = new ConcurrentDictionary(), + TestContext = testContext, + IsNestedProperty = false + }; + } + + /// + /// Creates a context for reflection caching during registration (uses placeholder instance). + /// + public static PropertyInitializationContext ForReflectionCaching( + PropertyInfo property, + IDataSourceAttribute dataSource, + Action propertySetter, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + TestContext testContext) + { + return new PropertyInitializationContext + { + Instance = PlaceholderInstance.Instance, + PropertyInfo = property, + DataSource = dataSource, + PropertyName = property.Name, + PropertyType = property.PropertyType, + PropertySetter = propertySetter, + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = new ConcurrentDictionary(), + TestContext = testContext, + IsNestedProperty = false + }; + } + + #endregion } \ No newline at end of file diff --git a/TUnit.Core/PropertyInjection/PropertyCacheKeyGenerator.cs b/TUnit.Core/PropertyInjection/PropertyCacheKeyGenerator.cs new file mode 100644 index 0000000000..c98abadadb --- /dev/null +++ b/TUnit.Core/PropertyInjection/PropertyCacheKeyGenerator.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using TUnit.Core.Interfaces.SourceGenerator; + +namespace TUnit.Core.PropertyInjection; + +/// +/// Generates consistent cache keys for property injection values. +/// Centralizes cache key generation to ensure consistency across the codebase (DRY principle). +/// +/// +/// Cache keys are formatted as "{DeclaringTypeName}.{PropertyName}" to uniquely identify +/// properties across different types. This format is used for storing and retrieving +/// injected property values in test contexts. +/// +public static class PropertyCacheKeyGenerator +{ + /// + /// Generates a cache key from source-generated property metadata. + /// + /// The property injection metadata from source generation. + /// A unique cache key string for the property. + public static string GetCacheKey(PropertyInjectionMetadata metadata) + { + return $"{metadata.ContainingType.FullName}.{metadata.PropertyName}"; + } + + /// + /// Generates a cache key from a PropertyInfo (reflection-based properties). + /// + /// The PropertyInfo from reflection. + /// A unique cache key string for the property. + public static string GetCacheKey(PropertyInfo property) + { + return $"{property.DeclaringType!.FullName}.{property.Name}"; + } +} diff --git a/TUnit.Core/PropertyInjection/PropertyHelper.cs b/TUnit.Core/PropertyInjection/PropertyHelper.cs index e5cd934d66..a32d24a7a7 100644 --- a/TUnit.Core/PropertyInjection/PropertyHelper.cs +++ b/TUnit.Core/PropertyInjection/PropertyHelper.cs @@ -11,13 +11,18 @@ internal static class PropertyHelper { /// /// Gets PropertyInfo in an AOT-safe manner. + /// Searches for both public and non-public properties to support internal properties. /// public static PropertyInfo GetPropertyInfo( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type containingType, string propertyName) { - var property = containingType.GetProperty(propertyName); + // Use binding flags to find both public and non-public properties + // This is necessary to support internal properties on internal classes + var property = containingType.GetProperty( + propertyName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); if (property == null) { diff --git a/TUnit.Core/PropertyInjection/PropertyInjectionCache.cs b/TUnit.Core/PropertyInjection/PropertyInjectionCache.cs index c883dfc858..b94917c014 100644 --- a/TUnit.Core/PropertyInjection/PropertyInjectionCache.cs +++ b/TUnit.Core/PropertyInjection/PropertyInjectionCache.cs @@ -1,24 +1,22 @@ -using System.Diagnostics.CodeAnalysis; -using TUnit.Core.Data; +using TUnit.Core.Data; namespace TUnit.Core.PropertyInjection; /// -/// Provides caching functionality for property injection operations. -/// Follows Single Responsibility Principle by focusing only on caching. +/// Provides pure caching functionality for property injection metadata. +/// Follows Single Responsibility Principle - only caches type metadata, no execution logic. /// /// This cache supports both execution modes: /// - Source Generation Mode: Uses pre-compiled property setters and metadata /// - Reflection Mode: Uses runtime discovery and dynamic property access /// -/// The IL2067 suppressions are necessary because types come from runtime objects -/// (via GetType() calls) which cannot have compile-time annotations. +/// Instance-level injection tracking has been moved to ObjectLifecycleService +/// to maintain SRP (caching vs execution are separate concerns). /// internal static class PropertyInjectionCache { private static readonly ThreadSafeDictionary _injectionPlans = new(); private static readonly ThreadSafeDictionary _shouldInjectCache = new(); - private static readonly ThreadSafeDictionary> _injectionTasks = new(); /// /// Gets or creates an injection plan for the specified type. @@ -41,42 +39,4 @@ public static bool HasInjectableProperties(Type type) return plan.HasProperties; }); } - - /// - /// Ensures properties are injected into the specified instance. - /// Fast-path optimized for already-injected instances (zero allocation). - /// - public static async ValueTask EnsureInjectedAsync(object instance, Func injectionFactory) - { - if (_injectionTasks.TryGetValue(instance, out var existingTcs) && existingTcs.Task.IsCompleted) - { - if (existingTcs.Task.IsFaulted) - { - await existingTcs.Task.ConfigureAwait(false); - } - - return; - } - - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - existingTcs = _injectionTasks.GetOrAdd(instance, _ => tcs); - - if (existingTcs == tcs) - { - try - { - await injectionFactory(instance).ConfigureAwait(false); - tcs.SetResult(true); - } - catch (Exception ex) - { - tcs.SetException(ex); - throw; - } - } - else - { - await existingTcs.Task.ConfigureAwait(false); - } - } } diff --git a/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs b/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs index a0cf5ee323..9c950da9c5 100644 --- a/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs +++ b/TUnit.Core/PropertyInjection/PropertyInjectionPlanBuilder.cs @@ -10,6 +10,22 @@ namespace TUnit.Core.PropertyInjection; /// internal static class PropertyInjectionPlanBuilder { + /// + /// Walks up the inheritance chain from the given type to typeof(object), + /// invoking the action for each type in the hierarchy. + /// + /// The starting type. + /// The action to invoke for each type in the inheritance chain. + private static void WalkInheritanceChain(Type type, Action action) + { + var currentType = type; + while (currentType != null && currentType != typeof(object)) + { + action(currentType); + currentType = currentType.BaseType; + } + } + /// /// Creates an injection plan for source-generated mode. /// Walks the inheritance chain to include all injectable properties from base classes. @@ -20,8 +36,7 @@ public static PropertyInjectionPlan BuildSourceGeneratedPlan(Type type) var processedProperties = new HashSet(); // Walk up the inheritance chain to find all properties with data sources - var currentType = type; - while (currentType != null && currentType != typeof(object)) + WalkInheritanceChain(type, currentType => { var propertySource = PropertySourceRegistry.GetSource(currentType); if (propertySource?.ShouldInitialize == true) @@ -35,9 +50,7 @@ public static PropertyInjectionPlan BuildSourceGeneratedPlan(Type type) } } } - - currentType = currentType.BaseType; - } + }); var sourceGenProps = allProperties.ToArray(); @@ -62,8 +75,7 @@ public static PropertyInjectionPlan BuildReflectionPlan(Type type) var processedProperties = new HashSet(); // Walk up the inheritance chain to find all properties with data source attributes - var currentType = type; - while (currentType != null && currentType != typeof(object)) + WalkInheritanceChain(type, currentType => { var properties = currentType.GetProperties( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) @@ -87,9 +99,7 @@ public static PropertyInjectionPlan BuildReflectionPlan(Type type) } } } - - currentType = currentType.BaseType; - } + }); return new PropertyInjectionPlan { @@ -102,18 +112,39 @@ public static PropertyInjectionPlan BuildReflectionPlan(Type type) /// /// Builds an injection plan based on the current execution mode. + /// Falls back to reflection when source-gen mode has no registered source for a type. + /// This handles generic types like ErrFixture<MyType> where the source generator + /// couldn't register a property source for the closed generic type. /// - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Source gen mode has its own path>")] + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Source gen mode has its own path")] public static PropertyInjectionPlan Build(Type type) { - return SourceRegistrar.IsEnabled - ? BuildSourceGeneratedPlan(type) - : BuildReflectionPlan(type); + if (!SourceRegistrar.IsEnabled) + { + return BuildReflectionPlan(type); + } + + // Try source-generated plan first + var plan = BuildSourceGeneratedPlan(type); + + // If no properties found in source-gen mode, fall back to reflection + // This handles generic types that couldn't be registered at compile time + if (!plan.HasProperties) + { + var reflectionPlan = BuildReflectionPlan(type); + if (reflectionPlan.HasProperties) + { + return reflectionPlan; + } + } + + return plan; } } /// /// Represents a plan for injecting properties into an object. +/// Provides iterator methods to abstract source-gen vs reflection branching (DRY). /// internal sealed class PropertyInjectionPlan { @@ -121,4 +152,100 @@ internal sealed class PropertyInjectionPlan public required PropertyInjectionMetadata[] SourceGeneratedProperties { get; init; } public required (PropertyInfo Property, IDataSourceAttribute DataSource)[] ReflectionProperties { get; init; } public required bool HasProperties { get; init; } + + /// + /// Iterates over all properties in the plan, abstracting source-gen vs reflection. + /// Call the appropriate callback based on which mode has properties. + /// + /// Action to invoke for each source-generated property. + /// Action to invoke for each reflection property. + public void ForEachProperty( + Action onSourceGenerated, + Action<(PropertyInfo Property, IDataSourceAttribute DataSource)> onReflection) + { + if (SourceGeneratedProperties.Length > 0) + { + foreach (var metadata in SourceGeneratedProperties) + { + onSourceGenerated(metadata); + } + } + else if (ReflectionProperties.Length > 0) + { + foreach (var prop in ReflectionProperties) + { + onReflection(prop); + } + } + } + + /// + /// Iterates over all properties in the plan asynchronously. + /// + public async Task ForEachPropertyAsync( + Func onSourceGenerated, + Func<(PropertyInfo Property, IDataSourceAttribute DataSource), Task> onReflection) + { + if (SourceGeneratedProperties.Length > 0) + { + foreach (var metadata in SourceGeneratedProperties) + { + await onSourceGenerated(metadata); + } + } + else if (ReflectionProperties.Length > 0) + { + foreach (var prop in ReflectionProperties) + { + await onReflection(prop); + } + } + } + + /// + /// Executes actions for all properties in parallel. + /// + public Task ForEachPropertyParallelAsync( + Func onSourceGenerated, + Func<(PropertyInfo Property, IDataSourceAttribute DataSource), Task> onReflection) + { + if (SourceGeneratedProperties.Length > 0) + { + return Helpers.ParallelTaskHelper.ForEachAsync(SourceGeneratedProperties, onSourceGenerated); + } + else if (ReflectionProperties.Length > 0) + { + return Helpers.ParallelTaskHelper.ForEachAsync(ReflectionProperties, onReflection); + } + + return Task.CompletedTask; + } + + /// + /// Gets property values from an instance, abstracting source-gen vs reflection. + /// + public IEnumerable GetPropertyValues(object instance) + { + if (SourceGeneratedProperties.Length > 0) + { + foreach (var metadata in SourceGeneratedProperties) + { + var property = metadata.ContainingType.GetProperty(metadata.PropertyName); + if (property?.CanRead == true) + { + yield return property.GetValue(instance); + } + } + } + else if (ReflectionProperties.Length > 0) + { + foreach (var (property, _) in ReflectionProperties) + { + if (property.CanRead) + { + yield return property.GetValue(instance); + } + } + } + } } diff --git a/TUnit.Core/PropertyInjection/PropertySetterFactory.cs b/TUnit.Core/PropertyInjection/PropertySetterFactory.cs index 3d9dce651d..8bc1662cdf 100644 --- a/TUnit.Core/PropertyInjection/PropertySetterFactory.cs +++ b/TUnit.Core/PropertyInjection/PropertySetterFactory.cs @@ -1,21 +1,56 @@ -using System.Diagnostics.CodeAnalysis; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace TUnit.Core.PropertyInjection; /// -/// Factory for creating property setters. +/// Factory for creating property setters with caching for performance. /// Consolidates all property setter creation logic in one place following DRY principle. /// +/// +/// Setters are cached using the PropertyInfo as the key to avoid repeated reflection calls. +/// This significantly improves performance when the same property is accessed multiple times +/// (e.g., in test retries or shared test data scenarios). +/// internal static class PropertySetterFactory { + // Cache setters per PropertyInfo to avoid repeated reflection + private static readonly ConcurrentDictionary> SetterCache = new(); + + /// + /// Gets or creates a setter delegate for the given property. + /// Uses caching to avoid repeated reflection calls. + /// + #if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Backing field access for init-only properties requires reflection")] + #endif + public static Action GetOrCreateSetter(PropertyInfo property) + { + return SetterCache.GetOrAdd(property, CreateSetterCore); + } + /// /// Creates a setter delegate for the given property. + /// Consider using for better performance through caching. /// #if NET6_0_OR_GREATER [RequiresUnreferencedCode("Backing field access for init-only properties requires reflection")] #endif public static Action CreateSetter(PropertyInfo property) + { + // Delegate to cached version for consistency + return GetOrCreateSetter(property); + } + + /// + /// Core implementation for creating a setter delegate. + /// Called by GetOrCreateSetter for caching. + /// + #if NET6_0_OR_GREATER + [RequiresUnreferencedCode("Backing field access for init-only properties requires reflection")] + #endif + private static Action CreateSetterCore(PropertyInfo property) { if (property.CanWrite && property.SetMethod != null) { diff --git a/TUnit.Core/Services/ObjectInitializationService.cs b/TUnit.Core/Services/ObjectInitializationService.cs new file mode 100644 index 0000000000..a18845001c --- /dev/null +++ b/TUnit.Core/Services/ObjectInitializationService.cs @@ -0,0 +1,40 @@ +using TUnit.Core.Interfaces; + +namespace TUnit.Core.Services; + +/// +/// Thread-safe service for initializing objects that implement . +/// Provides deduplicated initialization with explicit phase control. +/// +/// +/// +/// This service delegates to the static to ensure consistent +/// behavior and avoid duplicate caches. This consolidates initialization tracking in one place. +/// +/// +internal sealed class ObjectInitializationService : IObjectInitializationService +{ + /// + /// Creates a new instance of the initialization service. + /// + public ObjectInitializationService() + { + // No local cache needed - delegates to static ObjectInitializer + } + + /// + public ValueTask InitializeForDiscoveryAsync(object? obj, CancellationToken cancellationToken = default) + => ObjectInitializer.InitializeForDiscoveryAsync(obj, cancellationToken); + + /// + public ValueTask InitializeAsync(object? obj, CancellationToken cancellationToken = default) + => ObjectInitializer.InitializeAsync(obj, cancellationToken); + + /// + public bool IsInitialized(object? obj) + => ObjectInitializer.IsInitialized(obj); + + /// + public void ClearCache() + => ObjectInitializer.ClearCache(); +} diff --git a/TUnit.Core/TUnit.Core.targets b/TUnit.Core/TUnit.Core.targets index 3e87a68017..9992679a03 100644 --- a/TUnit.Core/TUnit.Core.targets +++ b/TUnit.Core/TUnit.Core.targets @@ -8,7 +8,7 @@ - <_TUnitPolyfillVersion>9.1.0 + <_TUnitPolyfillVersion>9.4.1 <_TUnitNeedsPolyfill Condition="'$(TargetFramework)' == 'netstandard2.0' or '$(TargetFramework)' == 'netstandard2.1' or '$(TargetFrameworkIdentifier)' == '.NETFramework'">true diff --git a/TUnit.Core/TestBuilder/TestBuilderException.cs b/TUnit.Core/TestBuilder/TestBuilderException.cs index a74c6c3f60..348dbfa42e 100644 --- a/TUnit.Core/TestBuilder/TestBuilderException.cs +++ b/TUnit.Core/TestBuilder/TestBuilderException.cs @@ -66,6 +66,24 @@ public DataSourceException(string dataSourceName, string message) /// Gets the name of the data source that failed. /// public string DataSourceName { get; } + + /// + /// Creates a DataSourceException with a custom message and inner exception. + /// Used when a data source or its nested dependencies fail during initialization. + /// + /// The full error message. + /// The exception that caused this error. + /// A new DataSourceException instance. + public static DataSourceException FromNestedFailure(string message, Exception innerException) + { + return new DataSourceException(message, innerException, isCustomMessage: true); + } + + private DataSourceException(string message, Exception innerException, bool isCustomMessage) + : base(message, innerException) + { + DataSourceName = string.Empty; + } } /// diff --git a/TUnit.Core/TestBuilderContext.cs b/TUnit.Core/TestBuilderContext.cs index cac0481205..f6ec4b0eef 100644 --- a/TUnit.Core/TestBuilderContext.cs +++ b/TUnit.Core/TestBuilderContext.cs @@ -49,7 +49,8 @@ public void RegisterForInitialization(object? obj) { Events.OnInitialize += async (sender, args) => { - await ObjectInitializer.InitializeAsync(obj); + // Discovery: only IAsyncDiscoveryInitializer + await ObjectInitializer.InitializeForDiscoveryAsync(obj); }; } @@ -57,7 +58,11 @@ internal static TestBuilderContext FromTestContext(TestContext testContext, IDat { return new TestBuilderContext { - Events = testContext.InternalEvents, TestMetadata = testContext.Metadata.TestDetails.MethodMetadata, DataSourceAttribute = dataSourceAttribute, StateBag = testContext.StateBag.Items, + Events = testContext.InternalEvents, + TestMetadata = testContext.Metadata.TestDetails.MethodMetadata, + DataSourceAttribute = dataSourceAttribute, + StateBag = testContext.StateBag.Items, + ClassConstructor = testContext.ClassConstructor, }; } } diff --git a/TUnit.Core/TestContext.Output.cs b/TUnit.Core/TestContext.Output.cs index 8f2223eacc..b951d3d4f3 100644 --- a/TUnit.Core/TestContext.Output.cs +++ b/TUnit.Core/TestContext.Output.cs @@ -31,6 +31,17 @@ void ITestOutput.AttachArtifact(Artifact artifact) _artifactsBag.Add(artifact); } + void ITestOutput.AttachArtifact(string filePath, string? displayName, string? description) + { + var fileInfo = new FileInfo(filePath); + _artifactsBag.Add(new Artifact + { + File = fileInfo, + DisplayName = displayName ?? fileInfo.Name, + Description = description + }); + } + string ITestOutput.GetStandardOutput() => GetOutput(); string ITestOutput.GetErrorOutput() => GetOutputError(); diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs index 61ce4b2571..6ef95724de 100644 --- a/TUnit.Core/TestContext.cs +++ b/TUnit.Core/TestContext.cs @@ -49,7 +49,8 @@ public TestContext(string testName, IServiceProvider serviceProvider, ClassHookC private static readonly AsyncLocal TestContexts = new(); - internal static readonly Dictionary> InternalParametersDictionary = new(); + // Use ConcurrentDictionary for thread-safe access during parallel test discovery + internal static readonly ConcurrentDictionary> InternalParametersDictionary = new(); private StringWriter? _outputWriter; @@ -72,7 +73,21 @@ internal set public static IReadOnlyDictionary> Parameters => InternalParametersDictionary; - public static IConfiguration Configuration { get; internal set; } = null!; + private static IConfiguration? _configuration; + + /// + /// Gets the test configuration. Throws a descriptive exception if accessed before initialization. + /// + /// Thrown if Configuration is accessed before the test engine initializes it. + public static IConfiguration Configuration + { + get => _configuration ?? throw new InvalidOperationException( + "TestContext.Configuration has not been initialized. " + + "This property is only available after the TUnit test engine has started. " + + "If you are accessing this from a static constructor or field initializer, " + + "consider moving the code to a test setup method or test body instead."); + internal set => _configuration = value; + } public static string? OutputDirectory { @@ -138,6 +153,12 @@ internal override void SetAsyncLocalContext() internal bool RunOnTestDiscovery { get; set; } + /// + /// Indicates whether this test is reusing the discovery-time instance instead of creating a new instance. + /// When true, property resolution and initialization should be skipped since the instance is already prepared. + /// + internal bool IsDiscoveryInstanceReused { get; set; } + public object Lock { get; } = new(); @@ -152,8 +173,13 @@ internal override void SetAsyncLocalContext() internal AbstractExecutableTest InternalExecutableTest { get; set; } = null!; private ConcurrentDictionary>? _trackedObjects; + + /// + /// Thread-safe lazy initialization of TrackedObjects using LazyInitializer + /// to prevent race conditions when multiple threads access this property simultaneously. + /// internal ConcurrentDictionary> TrackedObjects => - _trackedObjects ??= new(); + LazyInitializer.EnsureInitialized(ref _trackedObjects)!; /// /// Sets the output captured during test building phase. diff --git a/TUnit.Core/Tracking/ObjectTracker.cs b/TUnit.Core/Tracking/ObjectTracker.cs index 8e702a6262..bb273255c5 100644 --- a/TUnit.Core/Tracking/ObjectTracker.cs +++ b/TUnit.Core/Tracking/ObjectTracker.cs @@ -7,19 +7,105 @@ namespace TUnit.Core.Tracking; /// /// Pure reference counting object tracker for disposable objects. /// Objects are disposed when their reference count reaches zero, regardless of sharing type. +/// Uses ReferenceEqualityComparer to track objects by identity, not value equality. /// +/// +/// The static s_trackedObjects dictionary is shared across all tests. +/// Call at the end of a test session to release memory. +/// internal class ObjectTracker(TrackableObjectGraphProvider trackableObjectGraphProvider, Disposer disposer) { - private static readonly ConcurrentDictionary _trackedObjects = new(); + // Use ReferenceEqualityComparer to prevent objects with custom Equals from sharing state + private static readonly ConcurrentDictionary s_trackedObjects = + new(Helpers.ReferenceEqualityComparer.Instance); + + // Lock for atomic decrement-check-dispose operations to prevent race conditions + private static readonly object s_disposalLock = new(); + + // Collects errors from async disposal callbacks for post-session review + private static readonly ConcurrentBag s_asyncCallbackErrors = new(); + + /// + /// Gets any errors that occurred during async disposal callbacks. + /// Check this at the end of a test session to surface hidden failures. + /// + public static IReadOnlyCollection GetAsyncCallbackErrors() => s_asyncCallbackErrors.ToArray(); + + /// + /// Clears all static tracking state. Call at the end of a test session to release memory. + /// + public static void ClearStaticTracking() + { + s_trackedObjects.Clear(); + s_asyncCallbackErrors.Clear(); + } + + /// + /// Gets an existing counter for the object or creates a new one. + /// Centralizes the GetOrAdd pattern to ensure consistent counter creation. + /// + private static Counter GetOrCreateCounter(object obj) => + s_trackedObjects.GetOrAdd(obj, static _ => new Counter()); + + /// + /// Flattens a ConcurrentDictionary of depth-keyed HashSets into a single HashSet. + /// Thread-safe: locks each HashSet while copying. + /// Pre-calculates capacity to avoid HashSet resizing during population. + /// + private static HashSet FlattenTrackedObjects(ConcurrentDictionary> trackedObjects) + { +#if NETSTANDARD2_0 + // .NET Standard 2.0 doesn't support HashSet capacity constructor + var result = new HashSet(Helpers.ReferenceEqualityComparer.Instance); +#else + // First pass: calculate total capacity to avoid resizing + var totalCapacity = 0; + foreach (var kvp in trackedObjects) + { + lock (kvp.Value) + { + totalCapacity += kvp.Value.Count; + } + } + + // Second pass: populate with pre-sized HashSet + var result = new HashSet(totalCapacity, Helpers.ReferenceEqualityComparer.Instance); +#endif + foreach (var kvp in trackedObjects) + { + lock (kvp.Value) + { + foreach (var obj in kvp.Value) + { + result.Add(obj); + } + } + } + + return result; + } public void TrackObjects(TestContext testContext) { - var alreadyTracked = testContext.TrackedObjects.SelectMany(x => x.Value).ToHashSet(); + // Get already tracked objects (DRY: use helper method) + var alreadyTracked = FlattenTrackedObjects(testContext.TrackedObjects); - var newTrackableObjects = trackableObjectGraphProvider.GetTrackableObjects(testContext) - .SelectMany(x => x.Value) - .Except(alreadyTracked) - .ToHashSet(); + // Get new trackable objects + var newTrackableObjects = new HashSet(Helpers.ReferenceEqualityComparer.Instance); + var trackableDict = trackableObjectGraphProvider.GetTrackableObjects(testContext); + foreach (var kvp in trackableDict) + { + lock (kvp.Value) + { + foreach (var obj in kvp.Value) + { + if (!alreadyTracked.Contains(obj)) + { + newTrackableObjects.Add(obj); + } + } + } + } foreach (var obj in newTrackableObjects) { @@ -29,9 +115,10 @@ public void TrackObjects(TestContext testContext) public async ValueTask UntrackObjects(TestContext testContext, List cleanupExceptions) { - foreach (var obj in testContext.TrackedObjects - .SelectMany(x => x.Value) - .ToHashSet()) + // Get all objects to untrack (DRY: use helper method) + var objectsToUntrack = FlattenTrackedObjects(testContext.TrackedObjects); + + foreach (var obj in objectsToUntrack) { try { @@ -70,7 +157,7 @@ private void TrackObject(object? obj) return; } - var counter = _trackedObjects.GetOrAdd(obj, static _ => new Counter()); + var counter = GetOrCreateCounter(obj); counter.Increment(); } @@ -81,20 +168,36 @@ private async ValueTask UntrackObject(object? obj) return; } - if (_trackedObjects.TryGetValue(obj, out var counter)) - { - var count = counter.Decrement(); + var shouldDispose = false; - if (count < 0) + // Use lock to make decrement-check-remove atomic and prevent race conditions + // where multiple tests could try to dispose the same object simultaneously + lock (s_disposalLock) + { + if (s_trackedObjects.TryGetValue(obj, out var counter)) { - throw new InvalidOperationException("Reference count for object went below zero. This indicates a bug in the reference counting logic."); - } + var count = counter.Decrement(); - if (count == 0) - { - await disposer.DisposeAsync(obj).ConfigureAwait(false); + if (count < 0) + { + throw new InvalidOperationException("Reference count for object went below zero. This indicates a bug in the reference counting logic."); + } + + if (count == 0) + { + // Remove from tracking dictionary to prevent memory leak + // Use TryRemove to ensure atomicity - only remove if still in dictionary + s_trackedObjects.TryRemove(obj, out _); + shouldDispose = true; + } } } + + // Dispose outside the lock to avoid blocking other untrack operations + if (shouldDispose) + { + await disposer.DisposeAsync(obj).ConfigureAwait(false); + } } /// @@ -105,37 +208,128 @@ private static bool ShouldSkipTracking(object? obj) return obj is not IDisposable and not IAsyncDisposable; } + /// + /// Registers a callback to be invoked when the object is disposed (ref count reaches 0). + /// If the object is already disposed (or was never tracked), the callback is invoked immediately. + /// The callback is guaranteed to be invoked exactly once (idempotent). + /// + /// The object to monitor for disposal. If null or not disposable, the method returns without action. + /// The callback to invoke on disposal. Must not be null. + /// Thrown when is null. public static void OnDisposed(object? o, Action action) { - if(o is not IDisposable and not IAsyncDisposable) +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(action); +#else + if (action == null) { - return; + throw new ArgumentNullException(nameof(action)); } +#endif - _trackedObjects.GetOrAdd(o, static _ => new Counter()) - .OnCountChanged += (_, count) => - { - if (count == 0) - { - action(); - } - }; + RegisterDisposalCallback(o, action, static a => a()); } + /// + /// Registers an async callback to be invoked when the object is disposed (ref count reaches 0). + /// If the object is already disposed (or was never tracked), the callback is invoked immediately. + /// The callback is guaranteed to be invoked exactly once (idempotent). + /// + /// The object to monitor for disposal. If null or not disposable, the method returns without action. + /// The async callback to invoke on disposal. Must not be null. + /// Thrown when is null. public static void OnDisposedAsync(object? o, Func asyncAction) { - if(o is not IDisposable and not IAsyncDisposable) +#if NET6_0_OR_GREATER + ArgumentNullException.ThrowIfNull(asyncAction); +#else + if (asyncAction == null) + { + throw new ArgumentNullException(nameof(asyncAction)); + } +#endif + + // Wrap async action in fire-and-forget with exception collection + RegisterDisposalCallback(o, asyncAction, static a => _ = SafeExecuteAsync(a)); + } + + /// + /// Core implementation for registering disposal callbacks. + /// Extracts common logic from OnDisposed and OnDisposedAsync (DRY principle). + /// + /// The type of action (Action or Func<Task>). + /// The object to monitor for disposal. + /// The callback action. + /// How to invoke the action (sync vs async wrapper). + private static void RegisterDisposalCallback( + object? o, + TAction action, + Action invoker) + where TAction : Delegate + { + if (o is not IDisposable and not IAsyncDisposable) { return; } - _trackedObjects.GetOrAdd(o, static _ => new Counter()) - .OnCountChanged += async (_, count) => + // Only register callback if the object is actually being tracked. + // If not tracked, invoke callback immediately (object is effectively "disposed"). + // This prevents creating spurious counters for untracked objects. + if (!s_trackedObjects.TryGetValue(o, out var counter)) { - if (count == 0) + // Object not tracked - invoke callback immediately + invoker(action); + return; + } + + // Use flag to ensure callback only fires once (idempotent) + var invoked = 0; + EventHandler? handler = null; + + handler = (sender, count) => + { + if (count == 0 && Interlocked.Exchange(ref invoked, 1) == 0) { - await asyncAction().ConfigureAwait(false); + // Remove handler to prevent memory leaks + if (sender is Counter c && handler != null) + { + c.OnCountChanged -= handler; + } + + invoker(action); } }; + + counter.OnCountChanged += handler; + + // Check if already disposed (count is 0) - invoke immediately if so + // This prevents lost callbacks when registering after disposal + // Idempotent check ensures this doesn't double-fire if event already triggered + if (counter.CurrentCount == 0 && Interlocked.Exchange(ref invoked, 1) == 0) + { + counter.OnCountChanged -= handler; + invoker(action); + } + } + + /// + /// Executes an async action safely, catching and collecting exceptions + /// for post-session review instead of silently swallowing them. + /// + private static async Task SafeExecuteAsync(Func asyncAction) + { + try + { + await asyncAction().ConfigureAwait(false); + } + catch (Exception ex) + { + // Collect error for post-session review instead of silently swallowing + s_asyncCallbackErrors.Add(ex); + +#if DEBUG + System.Diagnostics.Debug.WriteLine($"[ObjectTracker] Exception in OnDisposedAsync callback: {ex.Message}"); +#endif + } } } diff --git a/TUnit.Core/Tracking/TrackableObjectGraphProvider.cs b/TUnit.Core/Tracking/TrackableObjectGraphProvider.cs index 97301e53f8..2460406c6f 100644 --- a/TUnit.Core/Tracking/TrackableObjectGraphProvider.cs +++ b/TUnit.Core/Tracking/TrackableObjectGraphProvider.cs @@ -1,57 +1,47 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using TUnit.Core.PropertyInjection; +using System.Collections.Concurrent; +using TUnit.Core.Discovery; +using TUnit.Core.Interfaces; using TUnit.Core.StaticProperties; namespace TUnit.Core.Tracking; +/// +/// Provides trackable objects from test contexts for lifecycle management. +/// Delegates to for the actual discovery logic. +/// internal class TrackableObjectGraphProvider { - public ConcurrentDictionary> GetTrackableObjects(TestContext testContext) - { - var visitedObjects = testContext.TrackedObjects; - - var testDetails = testContext.Metadata.TestDetails; - - foreach (var classArgument in testDetails.TestClassArguments) - { - if (classArgument != null && visitedObjects.GetOrAdd(0, []).Add(classArgument)) - { - AddNestedTrackableObjects(classArgument, visitedObjects, 1); - } - } + private readonly IObjectGraphDiscoverer _discoverer; - foreach (var methodArgument in testDetails.TestMethodArguments) - { - if (methodArgument != null && visitedObjects.GetOrAdd(0, []).Add(methodArgument)) - { - AddNestedTrackableObjects(methodArgument, visitedObjects, 1); - } - } - - foreach (var property in testDetails.TestClassInjectedPropertyArguments.Values) - { - if (property != null && visitedObjects.GetOrAdd(0, []).Add(property)) - { - AddNestedTrackableObjects(property, visitedObjects, 1); - } - } + /// + /// Creates a new instance with the default discoverer. + /// + public TrackableObjectGraphProvider() : this(new ObjectGraphDiscoverer()) + { + } - return visitedObjects; + /// + /// Creates a new instance with a custom discoverer (for testing). + /// + public TrackableObjectGraphProvider(IObjectGraphDiscoverer discoverer) + { + _discoverer = discoverer; } - private static void AddToLevel(Dictionary> objectsByLevel, int level, object obj) + /// + /// Gets trackable objects from a test context, organized by depth level. + /// Delegates to the shared IObjectGraphDiscoverer to eliminate code duplication. + /// + /// The test context to get trackable objects from. + /// Optional cancellation token for long-running discovery. + public ConcurrentDictionary> GetTrackableObjects(TestContext testContext, CancellationToken cancellationToken = default) { - if (!objectsByLevel.TryGetValue(level, out var list)) - { - list = []; - objectsByLevel[level] = list; - } - list.Add(obj); + // OCP-compliant: Use the interface method directly instead of type-checking + return _discoverer.DiscoverAndTrackObjects(testContext, cancellationToken); } /// - /// Get trackable objects for static properties (session-level) + /// Gets trackable objects for static properties (session-level). /// public IEnumerable GetStaticPropertyTrackableObjects() { @@ -63,67 +53,4 @@ public IEnumerable GetStaticPropertyTrackableObjects() } } } - - private void AddNestedTrackableObjects(object obj, ConcurrentDictionary> visitedObjects, int currentDepth) - { - var plan = PropertyInjectionCache.GetOrCreatePlan(obj.GetType()); - - if(!SourceRegistrar.IsEnabled) - { - foreach (var prop in plan.ReflectionProperties) - { - var value = prop.Property.GetValue(obj); - - if (value == null) - { - continue; - } - - // Check if already visited before yielding to prevent duplicates - if (!visitedObjects.GetOrAdd(currentDepth, []).Add(value)) - { - continue; - } - - if (!PropertyInjectionCache.HasInjectableProperties(value.GetType())) - { - continue; - } - - AddNestedTrackableObjects(value, visitedObjects, currentDepth + 1); - } - } - else - { - foreach (var metadata in plan.SourceGeneratedProperties) - { - var property = metadata.ContainingType.GetProperty(metadata.PropertyName); - - if (property == null || !property.CanRead) - { - continue; - } - - var value = property.GetValue(obj); - - if (value == null) - { - continue; - } - - // Check if already visited before yielding to prevent duplicates - if (!visitedObjects.GetOrAdd(currentDepth, []).Add(value)) - { - continue; - } - - if (!PropertyInjectionCache.HasInjectableProperties(value.GetType())) - { - continue; - } - - AddNestedTrackableObjects(value, visitedObjects, currentDepth + 1); - } - } - } } diff --git a/TUnit.Engine.Tests/CancellationAfterHooksTests.cs b/TUnit.Engine.Tests/CancellationAfterHooksTests.cs new file mode 100644 index 0000000000..750bb9ef38 --- /dev/null +++ b/TUnit.Engine.Tests/CancellationAfterHooksTests.cs @@ -0,0 +1,122 @@ +using Shouldly; +using TUnit.Engine.Tests.Enums; +using TUnit.Engine.Tests.Extensions; + +namespace TUnit.Engine.Tests; + +/// +/// Validates that After hooks execute even when tests are cancelled (Issue #3882). +/// These tests run the cancellation test scenarios and verify that After hooks created marker files. +/// +public class CancellationAfterHooksTests(TestMode testMode) : InvokableTestBase(testMode) +{ + private static readonly string TempPath = Path.GetTempPath(); + + [Test] + public async Task TestLevel_AfterHook_Runs_OnCancellation() + { + var afterMarkerFile = Path.Combine(TempPath, "TUnit_3882_Tests", "after_Test_ThatGets_Cancelled.txt"); + + // Clean up any existing marker files + if (File.Exists(afterMarkerFile)) + { + File.Delete(afterMarkerFile); + } + + await RunTestsWithFilter( + "/*/*/CancellationAfterHooksTests/*", + [ + // Test run completes even though the test itself fails (timeout is expected) + result => result.ResultSummary.Counters.Total.ShouldBe(1), + // Test should fail due to timeout + result => result.ResultSummary.Counters.Failed.ShouldBe(1), + // After hook should have created the marker file - this proves After hooks ran on cancellation + _ => File.Exists(afterMarkerFile).ShouldBeTrue($"After hook marker file should exist at {afterMarkerFile}") + ]); + + // Verify marker file content + if (File.Exists(afterMarkerFile)) + { + var content = await File.ReadAllTextAsync(afterMarkerFile); + content.ShouldContain("After hook executed"); + } + } + + [Test] + public async Task SessionLevel_AfterHook_Runs_OnCancellation() + { + var afterMarkerFile = Path.Combine(TempPath, "TUnit_3882_Session_After.txt"); + + // Clean up any existing marker files + if (File.Exists(afterMarkerFile)) + { + File.Delete(afterMarkerFile); + } + + await RunTestsWithFilter( + "/*/*/SessionLevelCancellationTests/*", + [ + // After Session hook should have created the marker file - this proves Session After hooks ran on cancellation + _ => File.Exists(afterMarkerFile).ShouldBeTrue($"Session After hook marker file should exist at {afterMarkerFile}") + ]); + + // Verify marker file content + if (File.Exists(afterMarkerFile)) + { + var content = await File.ReadAllTextAsync(afterMarkerFile); + content.ShouldContain("Session After hook executed"); + } + } + + [Test] + public async Task AssemblyLevel_AfterHook_Runs_OnCancellation() + { + var afterMarkerFile = Path.Combine(TempPath, "TUnit_3882_Assembly_After.txt"); + + // Clean up any existing marker files + if (File.Exists(afterMarkerFile)) + { + File.Delete(afterMarkerFile); + } + + await RunTestsWithFilter( + "/*/*/AssemblyLevelCancellationTests/*", + [ + // After Assembly hook should have created the marker file - this proves Assembly After hooks ran on cancellation + _ => File.Exists(afterMarkerFile).ShouldBeTrue($"Assembly After hook marker file should exist at {afterMarkerFile}") + ]); + + // Verify marker file content + if (File.Exists(afterMarkerFile)) + { + var content = await File.ReadAllTextAsync(afterMarkerFile); + content.ShouldContain("Assembly After hook executed"); + } + } + + [Test] + public async Task ClassLevel_AfterHook_Runs_OnCancellation() + { + var afterMarkerFile = Path.Combine(TempPath, "TUnit_3882_Class_After.txt"); + + // Clean up any existing marker files + if (File.Exists(afterMarkerFile)) + { + File.Delete(afterMarkerFile); + } + + await RunTestsWithFilter( + "/*/*/ClassLevelCancellationTests/*", + [ + // After Class hook should have created the marker file - this proves Class After hooks ran on cancellation + _ => File.Exists(afterMarkerFile).ShouldBeTrue($"Class After hook marker file should exist at {afterMarkerFile}") + ]); + + // Verify marker file content + if (File.Exists(afterMarkerFile)) + { + var content = await File.ReadAllTextAsync(afterMarkerFile); + content.ShouldContain("Class After hook executed"); + } + } +} diff --git a/TUnit.Engine.Tests/DataSourceExceptionPropagationTests.cs b/TUnit.Engine.Tests/DataSourceExceptionPropagationTests.cs new file mode 100644 index 0000000000..ab8005332a --- /dev/null +++ b/TUnit.Engine.Tests/DataSourceExceptionPropagationTests.cs @@ -0,0 +1,56 @@ +using Shouldly; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +/// +/// Tests that exceptions thrown during data source initialization are properly propagated +/// and cause tests to fail with appropriate error messages. +/// See: https://github.com/thomhurst/TUnit/issues/4049 +/// +public class DataSourceExceptionPropagationTests(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task NestedInitializer_PropertyAccessFailure_FailsTestWithDataSourceException() + { + await RunTestsWithFilter( + "/*/*/NestedInitializerExceptionPropagationTests/*", + [ + result => result.ResultSummary.Outcome.ShouldBe("Failed"), + result => result.ResultSummary.Counters.Total.ShouldBe(1), + result => result.ResultSummary.Counters.Passed.ShouldBe(0), + result => result.ResultSummary.Counters.Failed.ShouldBe(1), + result => + { + var errorMessage = result.Results.First().Output?.ErrorInfo?.Message; + errorMessage.ShouldNotBeNull("Expected an error message"); + // Should identify the failing property + errorMessage.ShouldContain("Failed to access property 'NestedInitializer'"); + // Should identify the type containing the failing property + errorMessage.ShouldContain("FailingNestedInitializerFactory"); + // Should indicate when the failure occurred + errorMessage.ShouldContain("during object graph discovery"); + } + ]); + } + + [Test] + public async Task Initializer_InitializeAsyncFailure_FailsTestWithException() + { + await RunTestsWithFilter( + "/*/*/InitializerExceptionPropagationTests/*", + [ + result => result.ResultSummary.Outcome.ShouldBe("Failed"), + result => result.ResultSummary.Counters.Total.ShouldBe(1), + result => result.ResultSummary.Counters.Passed.ShouldBe(0), + result => result.ResultSummary.Counters.Failed.ShouldBe(1), + result => + { + var errorMessage = result.Results.First().Output?.ErrorInfo?.Message; + errorMessage.ShouldNotBeNull("Expected an error message"); + // Should contain the original exception message + errorMessage.ShouldContain("Simulated initialization failure"); + } + ]); + } +} diff --git a/TUnit.Engine.Tests/DynamicTestIndexTests.cs b/TUnit.Engine.Tests/DynamicTestIndexTests.cs new file mode 100644 index 0000000000..d0513ff80f --- /dev/null +++ b/TUnit.Engine.Tests/DynamicTestIndexTests.cs @@ -0,0 +1,24 @@ +using Shouldly; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +/// +/// Tests that validate DynamicTestIndex generates unique test IDs +/// when multiple dynamic tests target the same method. +/// +public class DynamicTestIndexTests(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task Test() + { + await RunTestsWithFilter( + "/*/*DynamicTests/DynamicTestIndexTests/*", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(5), + result => result.ResultSummary.Counters.Passed.ShouldBe(5), + result => result.ResultSummary.Counters.Failed.ShouldBe(0) + ]); + } +} diff --git a/TUnit.Engine.Tests/ExternalCancellationTests.cs b/TUnit.Engine.Tests/ExternalCancellationTests.cs new file mode 100644 index 0000000000..ec08ba2f92 --- /dev/null +++ b/TUnit.Engine.Tests/ExternalCancellationTests.cs @@ -0,0 +1,154 @@ +using System.Diagnostics; +using CliWrap; +using Shouldly; +using TUnit.Core.Enums; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +/// +/// Validates that After hooks execute even when tests are cancelled EXTERNALLY (Issue #3882). +/// These tests start the test process asynchronously, cancel it mid-execution (simulating Ctrl+C or Stop button), +/// and verify that After hooks still execute by checking for marker files. +/// +/// +/// These tests are skipped on Windows because CliWrap's graceful cancellation uses GenerateConsoleCtrlEvent, +/// which doesn't work reliably for child processes with their own console. +/// See: https://github.com/Tyrrrz/CliWrap/issues/47 +/// +[ExcludeOn(OS.Windows)] +public class ExternalCancellationTests(TestMode testMode) : InvokableTestBase(testMode) +{ + private static readonly string TempPath = Path.GetTempPath(); + private static readonly string GetEnvironmentVariable = Environment.GetEnvironmentVariable("NET_VERSION") ?? "net10.0"; + + /// + /// Runs a test with external cancellation (simulates Ctrl+C, VS Test Explorer Stop button). + /// + /// Test filter pattern + /// Path to the marker file that proves After hook executed + /// Expected content in the marker file + private async Task RunTestWithExternalCancellation(string filter, string markerFile, string expectedMarkerContent) + { + // Clean up any existing marker files + if (File.Exists(markerFile)) + { + File.Delete(markerFile); + } + + var testProject = Sourcy.DotNet.Projects.TUnit_TestProject; + var guid = Guid.NewGuid().ToString("N"); + var trxFilename = guid + ".trx"; + + using var gracefulCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var forcefulCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(25)); + + // Use cross-platform executable detection (Linux: no extension, Windows: .exe) + var binDir = new DirectoryInfo(Path.Combine(testProject.DirectoryName!, "bin", "Release", GetEnvironmentVariable)); + var file = binDir.GetFiles("TUnit.TestProject").FirstOrDefault()?.FullName + ?? binDir.GetFiles("TUnit.TestProject.exe").First().FullName; + + var command = testMode switch + { + TestMode.SourceGenerated => Cli.Wrap(file) + .WithArguments( + [ + "--treenode-filter", filter, + "--report-trx", "--report-trx-filename", trxFilename, + "--diagnostic-verbosity", "Debug", + "--diagnostic", "--diagnostic-file-prefix", $"log_ExternalCancellation_{GetType().Name}_", + ]) + .WithWorkingDirectory(testProject.DirectoryName!) + .WithValidation(CommandResultValidation.None), + + TestMode.Reflection => Cli.Wrap(file) + .WithArguments( + [ + "--treenode-filter", filter, + "--report-trx", "--report-trx-filename", trxFilename, + "--diagnostic-verbosity", "Debug", + "--diagnostic", "--diagnostic-file-prefix", $"log_ExternalCancellation_{GetType().Name}_", + "--reflection" + ]) + .WithWorkingDirectory(testProject.DirectoryName!) + .WithValidation(CommandResultValidation.None), + + // Skip AOT and SingleFile modes for external cancellation (only test in CI) + TestMode.AOT => null, + TestMode.SingleFileApplication => null, + _ => throw new ArgumentOutOfRangeException(nameof(testMode), testMode, null) + }; + + // Skip AOT and SingleFile modes + if (command == null) + { + return; + } + + try + { + await command.ExecuteAsync(forcefulCancellationTokenSource.Token, gracefulCancellationTokenSource.Token); + } + catch (OperationCanceledException) + { + // Expected: Process was cancelled via CancellationToken + Console.WriteLine("[ExternalCancellation] Process cancelled successfully (expected)"); + } + catch (Exception ex) + { + // Log unexpected exceptions but don't fail - After hooks might still execute + Console.WriteLine($"[ExternalCancellation] Unexpected exception: {ex.Message}"); + } + + // Verify marker file exists - this proves After hook executed even on external cancellation + File.Exists(markerFile).ShouldBeTrue($"After hook marker file should exist at {markerFile}"); + + // Verify marker file content + var content = await File.ReadAllTextAsync(markerFile); + content.ShouldContain(expectedMarkerContent); + } + + [Test] + public async Task TestLevel_AfterHook_Runs_OnExternalCancellation() + { + var afterMarkerFile = Path.Combine(TempPath, "TUnit_3882_External", "after_Test_ThatGets_Cancelled_Externally.txt"); + + await RunTestWithExternalCancellation( + "/*/*/ExternalCancellationTests/*", + afterMarkerFile, + "After hook executed"); + } + + [Test] + public async Task SessionLevel_AfterHook_Runs_OnExternalCancellation() + { + var afterMarkerFile = Path.Combine(TempPath, "TUnit_3882_External_Session_After.txt"); + + await RunTestWithExternalCancellation( + "/*/*/ExternalSessionLevelCancellationTests/*", + afterMarkerFile, + "Session After hook executed"); + } + + [Test] + public async Task AssemblyLevel_AfterHook_Runs_OnExternalCancellation() + { + var afterMarkerFile = Path.Combine(TempPath, "TUnit_3882_External_Assembly_After.txt"); + + await RunTestWithExternalCancellation( + "/*/*/ExternalAssemblyLevelCancellationTests/*", + afterMarkerFile, + "Assembly After hook executed"); + } + + [Test] + public async Task ClassLevel_AfterHook_Runs_OnExternalCancellation() + { + var afterMarkerFile = Path.Combine(TempPath, "TUnit_3882_External_Class_After.txt"); + + await RunTestWithExternalCancellation( + "/*/*/ExternalClassLevelCancellationTests/*", + afterMarkerFile, + "Class After hook executed"); + } +} diff --git a/TUnit.Engine.Tests/JUnitReporterTests.cs b/TUnit.Engine.Tests/JUnitReporterTests.cs new file mode 100644 index 0000000000..9320db65d6 --- /dev/null +++ b/TUnit.Engine.Tests/JUnitReporterTests.cs @@ -0,0 +1,120 @@ +using Microsoft.Testing.Platform.Extensions; +using TUnit.Core; +using TUnit.Engine.Reporters; + +namespace TUnit.Engine.Tests; + +[NotInParallel] +public class JUnitReporterTests +{ + private sealed class MockExtension : IExtension + { + public string Uid => "MockExtension"; + public string DisplayName => "Mock"; + public string Version => "1.0.0"; + public string Description => "Mock Extension"; + public Task IsEnabledAsync() => Task.FromResult(true); + } + + [After(Test)] + public void Cleanup() + { + // Clean up environment variables after each test + Environment.SetEnvironmentVariable("TUNIT_DISABLE_JUNIT_REPORTER", null); + Environment.SetEnvironmentVariable("TUNIT_ENABLE_JUNIT_REPORTER", null); + Environment.SetEnvironmentVariable("GITLAB_CI", null); + Environment.SetEnvironmentVariable("CI_SERVER", null); + Environment.SetEnvironmentVariable("JUNIT_XML_OUTPUT_PATH", null); + } + + [Test] + public async Task IsEnabledAsync_Should_Return_False_When_TUNIT_DISABLE_JUNIT_REPORTER_Is_Set() + { + // Arrange + Environment.SetEnvironmentVariable("TUNIT_DISABLE_JUNIT_REPORTER", "true"); + Environment.SetEnvironmentVariable("GITLAB_CI", "true"); // Even with GitLab CI, should be disabled + var extension = new MockExtension(); + var reporter = new JUnitReporter(extension); + + // Act + var isEnabled = await reporter.IsEnabledAsync(); + + // Assert + await Assert.That(isEnabled).IsFalse(); + } + + [Test] + public async Task IsEnabledAsync_Should_Return_True_When_GITLAB_CI_Is_Set() + { + // Arrange + Environment.SetEnvironmentVariable("GITLAB_CI", "true"); + var extension = new MockExtension(); + var reporter = new JUnitReporter(extension); + + // Act + var isEnabled = await reporter.IsEnabledAsync(); + + // Assert + await Assert.That(isEnabled).IsTrue(); + } + + [Test] + public async Task IsEnabledAsync_Should_Return_True_When_CI_SERVER_Is_Set() + { + // Arrange + Environment.SetEnvironmentVariable("CI_SERVER", "yes"); + var extension = new MockExtension(); + var reporter = new JUnitReporter(extension); + + // Act + var isEnabled = await reporter.IsEnabledAsync(); + + // Assert + await Assert.That(isEnabled).IsTrue(); + } + + [Test] + public async Task IsEnabledAsync_Should_Return_True_When_TUNIT_ENABLE_JUNIT_REPORTER_Is_Set() + { + // Arrange + Environment.SetEnvironmentVariable("TUNIT_ENABLE_JUNIT_REPORTER", "true"); + var extension = new MockExtension(); + var reporter = new JUnitReporter(extension); + + // Act + var isEnabled = await reporter.IsEnabledAsync(); + + // Assert + await Assert.That(isEnabled).IsTrue(); + } + + [Test] + public async Task IsEnabledAsync_Should_Return_False_When_No_Environment_Variables_Are_Set() + { + // Arrange + var extension = new MockExtension(); + var reporter = new JUnitReporter(extension); + + // Act + var isEnabled = await reporter.IsEnabledAsync(); + + // Assert + await Assert.That(isEnabled).IsFalse(); + } + + [Test] + public async Task IsEnabledAsync_Should_Prefer_Disable_Over_Enable() + { + // Arrange + Environment.SetEnvironmentVariable("TUNIT_DISABLE_JUNIT_REPORTER", "true"); + Environment.SetEnvironmentVariable("TUNIT_ENABLE_JUNIT_REPORTER", "true"); + var extension = new MockExtension(); + var reporter = new JUnitReporter(extension); + + // Act + var isEnabled = await reporter.IsEnabledAsync(); + + // Assert + await Assert.That(isEnabled).IsFalse(); + } +} diff --git a/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs b/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs index a538088821..b673db2071 100644 --- a/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs +++ b/TUnit.Engine/Building/Collectors/AotTestDataCollector.cs @@ -131,7 +131,7 @@ private Task CreateMetadataFromDynamicDiscoveryResult(DynamicDisco var testName = methodInfo.Name; - return Task.FromResult(new AotDynamicTestMetadata(result) + return Task.FromResult(new DynamicTestMetadata(result) { TestName = testName, TestClassType = result.TestClassType, @@ -296,72 +296,6 @@ private static MethodMetadata CreateDummyMethodMetadata(Type type, string method }; } - private sealed class AotDynamicTestMetadata(DynamicDiscoveryResult dynamicResult) : TestMetadata, IDynamicTestMetadata - { - public override Func CreateExecutableTestFactory - { - get => (context, metadata) => - { - // For dynamic tests, we need to use the specific arguments from the dynamic result - var modifiedContext = new ExecutableTestCreationContext - { - TestId = context.TestId, - DisplayName = context.DisplayName, - Arguments = dynamicResult.TestMethodArguments ?? context.Arguments, - ClassArguments = dynamicResult.TestClassArguments ?? context.ClassArguments, - Context = context.Context - }; - - // Create instance and test invoker for the dynamic test - var createInstance = async (TestContext testContext) => - { - object instance; - - // Check if there's a ClassConstructor to use - if (testContext.ClassConstructor != null) - { - var testBuilderContext = TestBuilderContext.FromTestContext(testContext, null); - var classConstructorMetadata = new ClassConstructorMetadata - { - TestSessionId = "", // Dynamic tests don't have session IDs - TestBuilderContext = testBuilderContext - }; - - instance = await testContext.ClassConstructor.Create(metadata.TestClassType, classConstructorMetadata); - } - else - { - instance = metadata.InstanceFactory(Type.EmptyTypes, modifiedContext.ClassArguments); - } - - // Handle property injections - foreach (var propertyInjection in metadata.PropertyInjections) - { - var value = propertyInjection.ValueFactory(); - propertyInjection.Setter(instance, value); - } - - return instance; - }; - - var invokeTest = metadata.TestInvoker ?? throw new InvalidOperationException("Test invoker is null"); - - return new ExecutableTest(createInstance, - async (instance, args, context, ct) => - { - await invokeTest(instance, args); - }) - { - TestId = modifiedContext.TestId, - Metadata = metadata, - Arguments = modifiedContext.Arguments, - ClassArguments = modifiedContext.ClassArguments, - Context = modifiedContext.Context - }; - }; - } - } - private sealed class FailedDynamicTestMetadata(Exception exception) : TestMetadata { public override Func CreateExecutableTestFactory diff --git a/TUnit.Engine/Building/Interfaces/ITestBuilder.cs b/TUnit.Engine/Building/Interfaces/ITestBuilder.cs index c50bda428e..8f75be2a0e 100644 --- a/TUnit.Engine/Building/Interfaces/ITestBuilder.cs +++ b/TUnit.Engine/Building/Interfaces/ITestBuilder.cs @@ -14,8 +14,9 @@ internal interface ITestBuilder /// The test metadata /// The test data /// + /// Whether this test is reusing the discovery instance /// An executable test ready for execution - Task BuildTestAsync(TestMetadata metadata, TestBuilder.TestData testData, TestBuilderContext testBuilderContext); + Task BuildTestAsync(TestMetadata metadata, TestBuilder.TestData testData, TestBuilderContext testBuilderContext, bool isReusingDiscoveryInstance = false); /// /// Builds all executable tests from a single TestMetadata using its DataCombinationGenerator delegate. diff --git a/TUnit.Engine/Building/TestBuilder.cs b/TUnit.Engine/Building/TestBuilder.cs index a854f4e2df..937d913d6f 100644 --- a/TUnit.Engine/Building/TestBuilder.cs +++ b/TUnit.Engine/Building/TestBuilder.cs @@ -21,8 +21,7 @@ internal sealed class TestBuilder : ITestBuilder private readonly string _sessionId; private readonly EventReceiverOrchestrator _eventReceiverOrchestrator; private readonly IContextProvider _contextProvider; - private readonly PropertyInjectionService _propertyInjectionService; - private readonly DataSourceInitializer _dataSourceInitializer; + private readonly ObjectLifecycleService _objectLifecycleService; private readonly Discovery.IHookDiscoveryService _hookDiscoveryService; private readonly TestArgumentRegistrationService _testArgumentRegistrationService; private readonly IMetadataFilterMatcher _filterMatcher; @@ -31,8 +30,7 @@ public TestBuilder( string sessionId, EventReceiverOrchestrator eventReceiverOrchestrator, IContextProvider contextProvider, - PropertyInjectionService propertyInjectionService, - DataSourceInitializer dataSourceInitializer, + ObjectLifecycleService objectLifecycleService, Discovery.IHookDiscoveryService hookDiscoveryService, TestArgumentRegistrationService testArgumentRegistrationService, IMetadataFilterMatcher filterMatcher) @@ -41,16 +39,17 @@ public TestBuilder( _hookDiscoveryService = hookDiscoveryService; _eventReceiverOrchestrator = eventReceiverOrchestrator; _contextProvider = contextProvider; - _propertyInjectionService = propertyInjectionService; - _dataSourceInitializer = dataSourceInitializer; + _objectLifecycleService = objectLifecycleService; _testArgumentRegistrationService = testArgumentRegistrationService; _filterMatcher = filterMatcher ?? throw new ArgumentNullException(nameof(filterMatcher)); } /// - /// Initializes any IAsyncInitializer objects in class data that were deferred during registration. + /// Initializes class data objects during test building. + /// Only IAsyncDiscoveryInitializer objects are initialized during discovery. + /// Regular IAsyncInitializer objects are deferred to execution phase. /// - private async Task InitializeDeferredClassDataAsync(object?[] classData) + private static async Task InitializeClassDataAsync(object?[] classData) { if (classData == null || classData.Length == 0) { @@ -59,20 +58,16 @@ private async Task InitializeDeferredClassDataAsync(object?[] classData) foreach (var data in classData) { - if (data is IAsyncInitializer asyncInitializer && data is not IDataSourceAttribute) - { - if (!ObjectInitializer.IsInitialized(data)) - { - await ObjectInitializer.InitializeAsync(data); - } - } + // Discovery: only IAsyncDiscoveryInitializer objects are initialized. + // Regular IAsyncInitializer objects are deferred to execution phase. + await ObjectInitializer.InitializeForDiscoveryAsync(data); } } private async Task CreateInstance(TestMetadata metadata, Type[] resolvedClassGenericArgs, object?[] classData, TestBuilderContext builderContext) { // Initialize any deferred IAsyncInitializer objects in class data - await InitializeDeferredClassDataAsync(classData); + await InitializeClassDataAsync(classData); // First try to create instance with ClassConstructor attribute // Use attributes from context if available @@ -209,9 +204,14 @@ public async Task> BuildTestsFromMetadataAsy var classDataResult = await classDataFactory() ?? []; var classData = DataUnwrapper.Unwrap(classDataResult); + // Initialize objects before method data sources are evaluated. + // ObjectInitializer is phase-aware and will only initialize IAsyncDiscoveryInitializer during Discovery. + await InitializeClassDataAsync(classData); + var needsInstanceForMethodDataSources = metadata.DataSources.Any(ds => ds is IAccessesInstanceData); object? instanceForMethodDataSources = null; + var discoveryInstanceUsed = false; if (needsInstanceForMethodDataSources) { @@ -250,6 +250,27 @@ public async Task> BuildTestsFromMetadataAsy // Non-generic class instanceForMethodDataSources = metadata.InstanceFactory([], classData); } + + // Initialize property data sources on the early instance so that + // method data sources can access fully-initialized properties. + // This is critical for scenarios like: + // [ClassDataSource>] public required ErrFixture Fixture { get; init; } + // public IEnumerable> TestExecutions => [() => Fixture.Value]; + // [MethodDataSource("TestExecutions")] [Test] public void MyTest(T value) { } + if (instanceForMethodDataSources != null) + { + var tempObjectBag = new ConcurrentDictionary(); + var tempEvents = new TestContextEvents(); + + await _objectLifecycleService.RegisterObjectAsync( + instanceForMethodDataSources, + tempObjectBag, + metadata.MethodMetadata, + tempEvents); + + // Discovery: only IAsyncDiscoveryInitializer is initialized + await ObjectInitializer.InitializeForDiscoveryAsync(instanceForMethodDataSources); + } } catch (Exception ex) { @@ -290,12 +311,16 @@ public async Task> BuildTestsFromMetadataAsy Events = new TestContextEvents(), StateBag = new ConcurrentDictionary(), DataSourceAttribute = methodDataSource, - InitializedAttributes = testBuilderContext.InitializedAttributes // Preserve attributes from parent context + InitializedAttributes = testBuilderContext.InitializedAttributes, // Preserve attributes from parent context + ClassConstructor = testBuilderContext.ClassConstructor // Preserve ClassConstructor for instance creation }; classData = DataUnwrapper.Unwrap(await classDataFactory() ?? []); var methodData = DataUnwrapper.UnwrapWithTypes(await methodDataFactory() ?? [], metadata.MethodMetadata.Parameters); + // Initialize method data objects (ObjectInitializer is phase-aware) + await InitializeClassDataAsync(methodData); + // For concrete generic instantiations, check if the data is compatible with the expected types if (metadata.GenericMethodTypeArguments is { Length: > 0 }) { @@ -366,10 +391,21 @@ public async Task> BuildTestsFromMetadataAsy var basicSkipReason = GetBasicSkipReason(metadata, attributes); Func> instanceFactory; + bool isReusingDiscoveryInstance = false; + if (basicSkipReason is { Length: > 0 }) { instanceFactory = () => Task.FromResult(SkippedTestInstance.Instance); } + else if (methodDataLoopIndex == 1 && i == 0 && instanceForMethodDataSources != null && !discoveryInstanceUsed) + { + // Reuse the discovery instance for the first test to avoid duplicate initialization + var capturedInstance = instanceForMethodDataSources; + discoveryInstanceUsed = true; + isReusingDiscoveryInstance = true; + + instanceFactory = () => Task.FromResult(capturedInstance); + } else { var capturedMetadata = metadata; @@ -404,7 +440,7 @@ public async Task> BuildTestsFromMetadataAsy InitializedAttributes = attributes }; - var test = await BuildTestAsync(metadata, testData, testSpecificContext); + var test = await BuildTestAsync(metadata, testData, testSpecificContext, isReusingDiscoveryInstance); // If we have a basic skip reason, set it immediately if (!string.IsNullOrEmpty(basicSkipReason)) @@ -423,45 +459,56 @@ public async Task> BuildTestsFromMetadataAsy const string skipReason = "Data source returned no data"; Type[] resolvedClassGenericArgs; + Exception? genericResolutionException = null; try { resolvedClassGenericArgs = metadata.TestClassType.IsGenericTypeDefinition ? TryInferClassGenericsFromDataSources(metadata) : Type.EmptyTypes; } - catch + catch (Exception ex) { resolvedClassGenericArgs = Type.EmptyTypes; + genericResolutionException = ex; } - var testData = new TestData + // If generic type inference failed, create a failed test instead of skipped + if (genericResolutionException != null) { - TestClassInstanceFactory = () => Task.FromResult(SkippedTestInstance.Instance), - ClassDataSourceAttributeIndex = classDataAttributeIndex, - ClassDataLoopIndex = classDataLoopIndex, - ClassData = classData, - MethodDataSourceAttributeIndex = methodDataAttributeIndex, - MethodDataLoopIndex = 1, // Use 1 since we're creating a single skipped test - MethodData = [], - RepeatIndex = 0, - InheritanceDepth = metadata.InheritanceDepth, - ResolvedClassGenericArguments = resolvedClassGenericArgs, - ResolvedMethodGenericArguments = Type.EmptyTypes - }; - - var testSpecificContext = new TestBuilderContext + var failedTest = await CreateFailedTestForDataGenerationError(metadata, genericResolutionException); + tests.Add(failedTest); + } + else { - TestMetadata = metadata.MethodMetadata, - Events = new TestContextEvents(), - StateBag = new ConcurrentDictionary(), - ClassConstructor = testBuilderContext.ClassConstructor, - DataSourceAttribute = methodDataSource, - InitializedAttributes = attributes - }; - - var test = await BuildTestAsync(metadata, testData, testSpecificContext); - test.Context.SkipReason = skipReason; - tests.Add(test); + var testData = new TestData + { + TestClassInstanceFactory = () => Task.FromResult(SkippedTestInstance.Instance), + ClassDataSourceAttributeIndex = classDataAttributeIndex, + ClassDataLoopIndex = classDataLoopIndex, + ClassData = classData, + MethodDataSourceAttributeIndex = methodDataAttributeIndex, + MethodDataLoopIndex = 1, // Use 1 since we're creating a single skipped test + MethodData = [], + RepeatIndex = 0, + InheritanceDepth = metadata.InheritanceDepth, + ResolvedClassGenericArguments = resolvedClassGenericArgs, + ResolvedMethodGenericArguments = Type.EmptyTypes + }; + + var testSpecificContext = new TestBuilderContext + { + TestMetadata = metadata.MethodMetadata, + Events = new TestContextEvents(), + StateBag = new ConcurrentDictionary(), + ClassConstructor = testBuilderContext.ClassConstructor, + DataSourceAttribute = methodDataSource, + InitializedAttributes = attributes + }; + + var test = await BuildTestAsync(metadata, testData, testSpecificContext); + test.Context.SkipReason = skipReason; + tests.Add(test); + } } } } @@ -472,45 +519,56 @@ public async Task> BuildTestsFromMetadataAsy const string skipReason = "Data source returned no data"; Type[] resolvedClassGenericArgs; + Exception? genericResolutionException = null; try { resolvedClassGenericArgs = metadata.TestClassType.IsGenericTypeDefinition ? TryInferClassGenericsFromDataSources(metadata) : Type.EmptyTypes; } - catch + catch (Exception ex) { resolvedClassGenericArgs = Type.EmptyTypes; + genericResolutionException = ex; } - var testData = new TestData + // If generic type inference failed, create a failed test instead of skipped + if (genericResolutionException != null) { - TestClassInstanceFactory = () => Task.FromResult(SkippedTestInstance.Instance), - ClassDataSourceAttributeIndex = classDataAttributeIndex, - ClassDataLoopIndex = 1, // Use 1 since we're creating a single skipped test - ClassData = [], - MethodDataSourceAttributeIndex = 0, - MethodDataLoopIndex = 0, - MethodData = [], - RepeatIndex = 0, - InheritanceDepth = metadata.InheritanceDepth, - ResolvedClassGenericArguments = resolvedClassGenericArgs, - ResolvedMethodGenericArguments = Type.EmptyTypes - }; - - var testSpecificContext = new TestBuilderContext + var failedTest = await CreateFailedTestForDataGenerationError(metadata, genericResolutionException); + tests.Add(failedTest); + } + else { - TestMetadata = metadata.MethodMetadata, - Events = new TestContextEvents(), - StateBag = new ConcurrentDictionary(), - ClassConstructor = testBuilderContext.ClassConstructor, - DataSourceAttribute = classDataSource, - InitializedAttributes = attributes - }; - - var test = await BuildTestAsync(metadata, testData, testSpecificContext); - test.Context.SkipReason = skipReason; - tests.Add(test); + var testData = new TestData + { + TestClassInstanceFactory = () => Task.FromResult(SkippedTestInstance.Instance), + ClassDataSourceAttributeIndex = classDataAttributeIndex, + ClassDataLoopIndex = 1, // Use 1 since we're creating a single skipped test + ClassData = [], + MethodDataSourceAttributeIndex = 0, + MethodDataLoopIndex = 0, + MethodData = [], + RepeatIndex = 0, + InheritanceDepth = metadata.InheritanceDepth, + ResolvedClassGenericArguments = resolvedClassGenericArgs, + ResolvedMethodGenericArguments = Type.EmptyTypes + }; + + var testSpecificContext = new TestBuilderContext + { + TestMetadata = metadata.MethodMetadata, + Events = new TestContextEvents(), + StateBag = new ConcurrentDictionary(), + ClassConstructor = testBuilderContext.ClassConstructor, + DataSourceAttribute = classDataSource, + InitializedAttributes = attributes + }; + + var test = await BuildTestAsync(metadata, testData, testSpecificContext); + test.Context.SkipReason = skipReason; + tests.Add(test); + } } } @@ -750,10 +808,10 @@ private async Task GetDataSourcesAsync(IDataSourceAttrib return [NoDataSource.Instance]; } - // Initialize all data sources to ensure properties are injected + // Inject properties into data sources during discovery (IAsyncInitializer deferred to execution) foreach (var dataSource in dataSources) { - await _dataSourceInitializer.EnsureInitializedAsync(dataSource); + await _objectLifecycleService.InjectPropertiesAsync(dataSource); } return dataSources; @@ -767,16 +825,15 @@ private async Task GetDataSourcesAsync(IDataSourceAttrib IDataSourceAttribute dataSource, DataGeneratorMetadata dataGeneratorMetadata) { - // Ensure the data source is fully initialized before getting data rows - // This includes property injection and IAsyncInitializer.InitializeAsync - var initializedDataSource = await _dataSourceInitializer.EnsureInitializedAsync( + // Inject properties into data source during discovery (IAsyncInitializer deferred to execution) + var propertyInjectedDataSource = await _objectLifecycleService.InjectPropertiesAsync( dataSource, dataGeneratorMetadata.TestBuilderContext.Current.StateBag, dataGeneratorMetadata.TestInformation, dataGeneratorMetadata.TestBuilderContext.Current.Events); - // Now get data rows from the initialized data source - await foreach (var dataRow in initializedDataSource.GetDataRowsAsync(dataGeneratorMetadata)) + // Now get data rows from the property-injected data source + await foreach (var dataRow in propertyInjectedDataSource.GetDataRowsAsync(dataGeneratorMetadata)) { yield return dataRow; } @@ -784,7 +841,7 @@ private async Task GetDataSourcesAsync(IDataSourceAttrib [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Hook discovery service handles mode-specific logic; reflection calls suppressed in AOT mode")] [UnconditionalSuppressMessage("AOT", "IL3050", Justification = "Hook discovery service handles mode-specific logic; dynamic code suppressed in AOT mode")] - public async Task BuildTestAsync(TestMetadata metadata, TestData testData, TestBuilderContext testBuilderContext) + public async Task BuildTestAsync(TestMetadata metadata, TestData testData, TestBuilderContext testBuilderContext, bool isReusingDiscoveryInstance = false) { // Discover instance hooks for closed generic types (no-op in source gen mode) if (metadata.TestClassType is { IsGenericType: true, IsGenericTypeDefinition: false }) @@ -796,6 +853,9 @@ public async Task BuildTestAsync(TestMetadata metadata, var context = await CreateTestContextAsync(testId, metadata, testData, testBuilderContext); + // Mark if this test is reusing the discovery instance (already initialized) + context.IsDiscoveryInstanceReused = isReusingDiscoveryInstance; + context.Metadata.TestDetails.ClassInstance = PlaceholderInstance.Instance; // Arguments will be tracked by TestArgumentTrackingService during TestRegistered event @@ -933,7 +993,7 @@ private async Task InvokeTestRegisteredEventReceiversAsync(TestContext context) context.InternalDiscoveredTest = discoveredTest; // First, invoke the global test argument registration service to register shared instances - await _testArgumentRegistrationService.OnTestRegistered(registeredContext); + await _testArgumentRegistrationService.RegisterTestArgumentsAsync(context); var eventObjects = context.GetEligibleEventObjects(); @@ -1001,14 +1061,13 @@ private async Task CreateFailedTestDetails(TestMetadata metadata, s private async Task InitializeAttributesAsync(Attribute[] attributes) { - // Initialize any attributes that need property injection or implement IAsyncInitializer - // This ensures they're fully initialized before being used + // Inject properties into data source attributes during discovery + // IAsyncInitializer.InitializeAsync is deferred to execution time foreach (var attribute in attributes) { if (attribute is IDataSourceAttribute dataSource) { - // Data source attributes need to be initialized with property injection - await _dataSourceInitializer.EnsureInitializedAsync(dataSource); + await _objectLifecycleService.InjectPropertiesAsync(dataSource); } } @@ -1277,6 +1336,26 @@ internal class TestData public Type[] ResolvedMethodGenericArguments { get; set; } = Type.EmptyTypes; } + /// + /// Result of attempting to create an instance for method data sources. + /// Captures either success with an instance or failure with the exception. + /// + private readonly struct InstanceCreationResult + { + public object? Instance { get; } + public Exception? Exception { get; } + public bool Success => Exception == null; + + private InstanceCreationResult(object? instance, Exception? exception) + { + Instance = instance; + Exception = exception; + } + + public static InstanceCreationResult CreateSuccess(object? instance) => new(instance, null); + public static InstanceCreationResult CreateFailure(Exception exception) => new(null, exception); + } + #if NET6_0_OR_GREATER [RequiresUnreferencedCode("Test building in reflection mode uses generic type resolution which requires unreferenced code")] #endif @@ -1357,19 +1436,40 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( var classData = DataUnwrapper.Unwrap(await classDataFactory() ?? []); + // Initialize objects before method data sources are evaluated (ObjectInitializer is phase-aware) + await InitializeClassDataAsync(classData); + // Handle instance creation for method data sources var needsInstanceForMethodDataSources = metadata.DataSources.Any(ds => ds is IAccessesInstanceData); object? instanceForMethodDataSources = null; if (needsInstanceForMethodDataSources) { - instanceForMethodDataSources = await CreateInstanceForMethodDataSources( + var instanceResult = await CreateInstanceForMethodDataSources( metadata, classDataAttributeIndex, classDataLoopIndex, classData); - if (instanceForMethodDataSources == null) + if (!instanceResult.Success) { - continue; // Skip if instance creation failed + // Yield a failed test instead of silently skipping + yield return await CreateFailedTestForInstanceDataSourceError(metadata, instanceResult.Exception!); + continue; } + + instanceForMethodDataSources = instanceResult.Instance!; + + // Initialize property data sources on the early instance so that + // method data sources can access fully-initialized properties. + var tempObjectBag = new ConcurrentDictionary(); + var tempEvents = new TestContextEvents(); + + await _objectLifecycleService.RegisterObjectAsync( + instanceForMethodDataSources, + tempObjectBag, + metadata.MethodMetadata, + tempEvents); + + // Discovery: only IAsyncDiscoveryInitializer is initialized + await ObjectInitializer.InitializeForDiscoveryAsync(instanceForMethodDataSources); } // Stream through method data sources @@ -1418,7 +1518,7 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( #if NET6_0_OR_GREATER [RequiresUnreferencedCode("Generic type resolution for instance creation uses reflection")] #endif - private Task CreateInstanceForMethodDataSources( + private Task CreateInstanceForMethodDataSources( TestMetadata metadata, int classDataAttributeIndex, int classDataLoopIndex, object?[] classData) { try @@ -1441,22 +1541,22 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( try { var resolution = TestGenericTypeResolver.Resolve(metadata, tempTestData); - return Task.FromResult(metadata.InstanceFactory(resolution.ResolvedClassGenericArguments, classData)); + return Task.FromResult(InstanceCreationResult.CreateSuccess(metadata.InstanceFactory(resolution.ResolvedClassGenericArguments, classData))); } catch (GenericTypeResolutionException) when (classData.Length == 0) { var resolvedTypes = TryInferClassGenericsFromDataSources(metadata); - return Task.FromResult(metadata.InstanceFactory(resolvedTypes, classData)); + return Task.FromResult(InstanceCreationResult.CreateSuccess(metadata.InstanceFactory(resolvedTypes, classData))); } } else { - return Task.FromResult(metadata.InstanceFactory([], classData)); + return Task.FromResult(InstanceCreationResult.CreateSuccess(metadata.InstanceFactory([], classData))); } } - catch + catch (Exception ex) { - return Task.FromResult(null); + return Task.FromResult(InstanceCreationResult.CreateFailure(ex)); } } @@ -1480,6 +1580,9 @@ public async IAsyncEnumerable BuildTestsStreamingAsync( var methodData = DataUnwrapper.UnwrapWithTypes(await methodDataFactory() ?? [], metadata.MethodMetadata.Parameters); + // Initialize method data objects (ObjectInitializer is phase-aware) + await InitializeClassDataAsync(methodData); + // Check data compatibility for generic methods if (metadata.GenericMethodTypeArguments is { Length: > 0 }) { diff --git a/TUnit.Engine/Building/TestBuilderPipeline.cs b/TUnit.Engine/Building/TestBuilderPipeline.cs index cc3fc7c815..96da81767f 100644 --- a/TUnit.Engine/Building/TestBuilderPipeline.cs +++ b/TUnit.Engine/Building/TestBuilderPipeline.cs @@ -152,6 +152,8 @@ private async Task GenerateDynamicTests(TestMetadata m .SelectAsync(async repeatIndex => { // Create a simple TestData for ID generation + // Use DynamicTestIndex from the metadata to ensure unique test IDs for multiple dynamic tests + var dynamicTestIndex = metadata is IDynamicTestMetadata dynMeta ? dynMeta.DynamicTestIndex : 0; var testData = new TestBuilder.TestData { TestClassInstanceFactory = () => Task.FromResult(metadata.InstanceFactory(Type.EmptyTypes, [])), @@ -159,7 +161,7 @@ private async Task GenerateDynamicTests(TestMetadata m ClassDataLoopIndex = 0, ClassData = [], MethodDataSourceAttributeIndex = 0, - MethodDataLoopIndex = 0, + MethodDataLoopIndex = dynamicTestIndex, MethodData = [], RepeatIndex = repeatIndex, InheritanceDepth = metadata.InheritanceDepth, @@ -271,6 +273,8 @@ private async IAsyncEnumerable BuildTestsFromSingleMetad // Dynamic tests need to honor attributes like RepeatCount, RetryCount, etc. // We'll create multiple test instances based on RepeatCount + // Use DynamicTestIndex from the metadata to ensure unique test IDs for multiple dynamic tests + var dynamicTestIndex = ((IDynamicTestMetadata)resolvedMetadata).DynamicTestIndex; for (var repeatIndex = 0; repeatIndex < repeatCount + 1; repeatIndex++) { // Create a simple TestData for ID generation @@ -281,7 +285,7 @@ private async IAsyncEnumerable BuildTestsFromSingleMetad ClassDataLoopIndex = 0, ClassData = [], MethodDataSourceAttributeIndex = 0, - MethodDataLoopIndex = 0, + MethodDataLoopIndex = dynamicTestIndex, MethodData = [], RepeatIndex = repeatIndex, InheritanceDepth = resolvedMetadata.InheritanceDepth, @@ -290,9 +294,11 @@ private async IAsyncEnumerable BuildTestsFromSingleMetad }; var testId = TestIdentifierService.GenerateTestId(resolvedMetadata, testData); + var dynamicMetadata = (IDynamicTestMetadata)resolvedMetadata; + var baseDisplayName = dynamicMetadata.DisplayName ?? resolvedMetadata.TestName; var displayName = repeatCount > 0 - ? $"{resolvedMetadata.TestName} (Repeat {repeatIndex + 1}/{repeatCount + 1})" - : resolvedMetadata.TestName; + ? $"{baseDisplayName} (Repeat {repeatIndex + 1}/{repeatCount + 1})" + : baseDisplayName; // Create TestDetails for dynamic tests var testDetails = new TestDetails @@ -322,6 +328,12 @@ private async IAsyncEnumerable BuildTestsFromSingleMetad // Set the TestDetails on the context context.Metadata.TestDetails = testDetails; + // Set custom display name for dynamic tests if specified + if (dynamicMetadata.DisplayName != null) + { + context.Metadata.DisplayName = dynamicMetadata.DisplayName; + } + // Invoke discovery event receivers to properly handle all attribute behaviors await InvokeDiscoveryEventReceiversAsync(context).ConfigureAwait(false); diff --git a/TUnit.Engine/CommandLineProviders/JUnitReporterCommandProvider.cs b/TUnit.Engine/CommandLineProviders/JUnitReporterCommandProvider.cs new file mode 100644 index 0000000000..39977ee5d6 --- /dev/null +++ b/TUnit.Engine/CommandLineProviders/JUnitReporterCommandProvider.cs @@ -0,0 +1,45 @@ +using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.CommandLine; + +namespace TUnit.Engine.CommandLineProviders; + +internal class JUnitReporterCommandProvider(IExtension extension) : ICommandLineOptionsProvider +{ + public const string JUnitOutputPathOption = "junit-output-path"; + + public Task IsEnabledAsync() => extension.IsEnabledAsync(); + + public string Uid => extension.Uid; + + public string Version => extension.Version; + + public string DisplayName => extension.DisplayName; + + public string Description => extension.Description; + + public IReadOnlyCollection GetCommandLineOptions() + { + return + [ + new CommandLineOption( + JUnitOutputPathOption, + "Path to output JUnit XML file (default: TestResults/{AssemblyName}-junit.xml)", + ArgumentArity.ExactlyOne, + false) + ]; + } + + public Task ValidateOptionArgumentsAsync( + CommandLineOption commandOption, + string[] arguments) + { + return ValidationResult.ValidTask; + } + + public Task ValidateCommandLineOptionsAsync( + ICommandLineOptions commandLineOptions) + { + return ValidationResult.ValidTask; + } +} diff --git a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs index e0e541f163..caac547fb9 100644 --- a/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs +++ b/TUnit.Engine/Discovery/ReflectionTestDataCollector.cs @@ -1603,7 +1603,8 @@ private static bool IsCovariantCompatible(Type paramType, [DynamicallyAccessedMe { return valueTask.AsTask(); } - return Task.CompletedTask; + // F# Async support (reuses existing AsyncConvert logic) + return AsyncConvert.ConvertObject(result).AsTask(); } catch (TargetInvocationException tie) { @@ -1921,7 +1922,7 @@ private Task CreateMetadataFromDynamicDiscoveryResult(DynamicDisco var testName = GenerateTestName(result.TestClassType, methodInfo); - var metadata = new DynamicReflectionTestMetadata(result.TestClassType, methodInfo, result) + var metadata = new DynamicTestMetadata(result) { TestName = testName, TestClassType = result.TestClassType, @@ -2090,83 +2091,4 @@ private static TestMetadata CreateFailedTestMetadataForDynamicTest(DynamicDiscov }; } - private sealed class DynamicReflectionTestMetadata : TestMetadata, IDynamicTestMetadata - { - private readonly DynamicDiscoveryResult _dynamicResult; - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - private readonly Type _testClass; - private readonly MethodInfo _testMethod; - - public DynamicReflectionTestMetadata( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type testClass, - MethodInfo testMethod, - DynamicDiscoveryResult dynamicResult) - { - _testClass = testClass; - _testMethod = testMethod; - _dynamicResult = dynamicResult; - } - - public override Func CreateExecutableTestFactory - { - get => (context, metadata) => - { - // For dynamic tests, we need to use the specific arguments from the dynamic result - var modifiedContext = new ExecutableTestCreationContext - { - TestId = context.TestId, - DisplayName = context.DisplayName, - Arguments = _dynamicResult.TestMethodArguments ?? context.Arguments, - ClassArguments = _dynamicResult.TestClassArguments ?? context.ClassArguments, - Context = context.Context - }; - - // Create a regular ExecutableTest with the modified context - // Create instance and test invoker for the dynamic test - Func> createInstance = async (TestContext testContext) => - { - // Try to create instance with ClassConstructor attribute - var attributes = metadata.AttributeFactory(); - var classConstructorInstance = await ClassConstructorHelper.TryCreateInstanceWithClassConstructor( - attributes, - _testClass, - metadata.TestSessionId, - testContext).ConfigureAwait(false); - - if (classConstructorInstance != null) - { - return classConstructorInstance; - } - - // Fall back to default instance factory - var instance = metadata.InstanceFactory(Type.EmptyTypes, modifiedContext.ClassArguments); - - // Handle property injections - foreach (var propertyInjection in metadata.PropertyInjections) - { - var value = propertyInjection.ValueFactory(); - propertyInjection.Setter(instance, value); - } - - return instance; - }; - - var invokeTest = metadata.TestInvoker ?? throw new InvalidOperationException("Test invoker is null"); - - return new ExecutableTest(createInstance, - async (instance, args, context, ct) => - { - await invokeTest(instance, args).ConfigureAwait(false); - }) - { - TestId = modifiedContext.TestId, - Metadata = metadata, - Arguments = modifiedContext.Arguments, - ClassArguments = modifiedContext.ClassArguments, - Context = modifiedContext.Context - }; - }; - } - } - } diff --git a/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs b/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs index 83b7f0c98e..435894225f 100644 --- a/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs +++ b/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs @@ -21,6 +21,9 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) var githubReporter = new GitHubReporter(extension); var githubReporterCommandProvider = new GitHubReporterCommandProvider(extension); + var junitReporter = new JUnitReporter(extension); + var junitReporterCommandProvider = new JUnitReporterCommandProvider(extension); + testApplicationBuilder.RegisterTestFramework( serviceProvider => new TestFrameworkCapabilities(CreateCapabilities(serviceProvider)), (capabilities, serviceProvider) => new TUnitTestFramework(extension, serviceProvider, capabilities)); @@ -46,6 +49,9 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) // GitHub reporter configuration testApplicationBuilder.CommandLine.AddProvider(() => githubReporterCommandProvider); + // JUnit reporter configuration + testApplicationBuilder.CommandLine.AddProvider(() => junitReporterCommandProvider); + testApplicationBuilder.TestHost.AddDataConsumer(serviceProvider => { // Apply command-line configuration if provided @@ -58,6 +64,18 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) return githubReporter; }); testApplicationBuilder.TestHost.AddTestHostApplicationLifetime(_ => githubReporter); + + testApplicationBuilder.TestHost.AddDataConsumer(serviceProvider => + { + // Apply command-line configuration if provided + var commandLineOptions = serviceProvider.GetRequiredService(); + if (commandLineOptions.TryGetOptionArgumentList(JUnitReporterCommandProvider.JUnitOutputPathOption, out var pathArgs)) + { + junitReporter.SetOutputPath(pathArgs[0]); + } + return junitReporter; + }); + testApplicationBuilder.TestHost.AddTestHostApplicationLifetime(_ => junitReporter); } private static IReadOnlyCollection CreateCapabilities(IServiceProvider serviceProvider) diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index 823d643784..9a31e5eb99 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -51,9 +51,7 @@ public ITestExecutionFilter? Filter public TUnitInitializer Initializer { get; } public CancellationTokenSource FailFastCancellationSource { get; } public ParallelLimitLockProvider ParallelLimitLockProvider { get; } - public PropertyInjectionService PropertyInjectionService { get; } - public DataSourceInitializer DataSourceInitializer { get; } - public ObjectRegistrationService ObjectRegistrationService { get; } + public ObjectLifecycleService ObjectLifecycleService { get; } public bool AfterSessionHooksFailed { get; set; } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT/trimmed scenarios")] @@ -104,25 +102,29 @@ public TUnitServiceProvider(IExtension extension, loggerFactory.CreateLogger(), logLevelProvider)); - // Create initialization services - // Note: Circular dependency managed through two-phase initialization - // Phase 1: Create services with partial dependencies - DataSourceInitializer = Register(new DataSourceInitializer()); - PropertyInjectionService = Register(new PropertyInjectionService(DataSourceInitializer)); - ObjectRegistrationService = Register(new ObjectRegistrationService(PropertyInjectionService)); - - // Phase 2: Complete dependencies (Initialize methods accept IObjectRegistry to break circular dependency) - PropertyInjectionService.Initialize(ObjectRegistrationService); - DataSourceInitializer.Initialize(PropertyInjectionService); + // Create initialization services using Lazy to break circular dependencies + // No more two-phase initialization with Initialize() calls + var objectGraphDiscoveryService = Register(new ObjectGraphDiscoveryService()); + // Keep TrackableObjectGraphProvider for ObjectTracker (TUnit.Core dependency) var trackableObjectGraphProvider = new TrackableObjectGraphProvider(); var disposer = new Disposer(Logger); var objectTracker = new ObjectTracker(trackableObjectGraphProvider, disposer); + // Use Lazy to break circular dependency between PropertyInjector and ObjectLifecycleService + // PropertyInjector now depends on IInitializationCallback interface (implemented by ObjectLifecycleService) + // This follows Dependency Inversion Principle and improves testability + ObjectLifecycleService? objectLifecycleServiceInstance = null; + var lazyInitializationCallback = new Lazy(() => objectLifecycleServiceInstance!); + var lazyPropertyInjector = new Lazy(() => new PropertyInjector(lazyInitializationCallback, TestSessionId)); + + objectLifecycleServiceInstance = new ObjectLifecycleService(lazyPropertyInjector, objectGraphDiscoveryService, objectTracker); + ObjectLifecycleService = Register(objectLifecycleServiceInstance); + // Register the test argument registration service to handle object registration for shared instances - var testArgumentRegistrationService = Register(new TestArgumentRegistrationService(ObjectRegistrationService, objectTracker)); + var testArgumentRegistrationService = Register(new TestArgumentRegistrationService(ObjectLifecycleService)); TestFilterService = Register(new TestFilterService(Logger, testArgumentRegistrationService)); @@ -135,7 +137,7 @@ public TUnitServiceProvider(IExtension extension, CancellationToken = Register(new EngineCancellationToken()); - EventReceiverOrchestrator = Register(new EventReceiverOrchestrator(Logger, trackableObjectGraphProvider)); + EventReceiverOrchestrator = Register(new EventReceiverOrchestrator(Logger)); HookCollectionService = Register(new HookCollectionService(EventReceiverOrchestrator)); ParallelLimitLockProvider = Register(new ParallelLimitLockProvider()); @@ -145,8 +147,9 @@ public TUnitServiceProvider(IExtension extension, var hookExecutor = Register(new HookExecutor(HookCollectionService, ContextProvider, EventReceiverOrchestrator)); var lifecycleCoordinator = Register(new TestLifecycleCoordinator()); var beforeHookTaskCache = Register(new BeforeHookTaskCache()); + var afterHookPairTracker = Register(new AfterHookPairTracker()); - TestExecutor = Register(new TestExecutor(hookExecutor, lifecycleCoordinator, beforeHookTaskCache, ContextProvider, EventReceiverOrchestrator)); + TestExecutor = Register(new TestExecutor(hookExecutor, lifecycleCoordinator, beforeHookTaskCache, afterHookPairTracker, ContextProvider, EventReceiverOrchestrator)); var testExecutionGuard = Register(new TestExecutionGuard()); var testStateManager = Register(new TestStateManager()); @@ -173,7 +176,7 @@ public TUnitServiceProvider(IExtension extension, var dependencyExpander = Register(new MetadataDependencyExpander(filterMatcher)); var testBuilder = Register( - new TestBuilder(TestSessionId, EventReceiverOrchestrator, ContextProvider, PropertyInjectionService, DataSourceInitializer, hookDiscoveryService, testArgumentRegistrationService, filterMatcher)); + new TestBuilder(TestSessionId, EventReceiverOrchestrator, ContextProvider, ObjectLifecycleService, hookDiscoveryService, testArgumentRegistrationService, filterMatcher)); TestBuilderPipeline = Register( new TestBuilderPipeline( @@ -187,7 +190,7 @@ public TUnitServiceProvider(IExtension extension, // Create test finder service after discovery service so it can use its cache TestFinder = Register(new TestFinder(DiscoveryService)); - var testInitializer = new TestInitializer(EventReceiverOrchestrator, PropertyInjectionService, objectTracker); + var testInitializer = new TestInitializer(EventReceiverOrchestrator, ObjectLifecycleService); // Create the new TestCoordinator that orchestrates the granular services var testCoordinator = Register( diff --git a/TUnit.Engine/Helpers/EnvironmentVariableCache.cs b/TUnit.Engine/Helpers/EnvironmentVariableCache.cs deleted file mode 100644 index ce0a3ddf70..0000000000 --- a/TUnit.Engine/Helpers/EnvironmentVariableCache.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System.Collections.Concurrent; - -namespace TUnit.Engine.Helpers; - -/// -/// Centralized cache for environment variables to avoid repeated system calls -/// Initializes all environment variables on first access and caches them for the lifetime of the application -/// -internal static class EnvironmentVariableCache -{ - private static readonly ConcurrentDictionary _cache = new(); - private static readonly object _initLock = new(); - private static bool _initialized = false; - - /// - /// All environment variable keys that TUnit cares about - /// This helps us cache only the variables we need rather than all environment variables - /// - private static readonly string[] _tunitEnvironmentVariables = - [ - // TUnit specific variables - "TUNIT_DISCOVERY_DIAGNOSTICS", - "TUNIT_DISCOVERY_TIMEOUT_SECONDS", - "TUNIT_DATA_SOURCE_TIMEOUT_SECONDS", - "TUNIT_EXECUTION_MODE", - "TUNIT_ADAPTIVE_MIN_PARALLELISM", - "TUNIT_ADAPTIVE_MAX_PARALLELISM", - "TUNIT_ADAPTIVE_METRICS", - "TUNIT_DISABLE_GITHUB_REPORTER", - "TUNIT_GITHUB_REPORTER_STYLE", - - // CI environment detection variables - "CI", - "CONTINUOUS_INTEGRATION", - "BUILD_ID", - "BUILD_NUMBER", - "GITHUB_ACTIONS", - "GITLAB_CI", - "AZURE_PIPELINES", - "JENKINS_URL", - "TEAMCITY_VERSION", - "APPVEYOR", - "CIRCLECI", - "TRAVIS", - - // Container detection variables - "DOTNET_RUNNING_IN_CONTAINER", - "CONTAINER", - "KUBERNETES_SERVICE_HOST", - - // GitHub specific variables - "DISABLE_GITHUB_REPORTER", - "GITHUB_STEP_SUMMARY" - ]; - - /// - /// Gets the cached value of an environment variable - /// Initializes the cache on first call - /// - /// The name of the environment variable - /// The environment variable value or null if not set - public static string? Get(string variableName) - { - EnsureInitialized(); - _cache.TryGetValue(variableName, out var value); - return value; - } - - /// - /// Gets multiple cached environment variable values - /// Useful for checking multiple CI or container detection variables - /// - /// The names of the environment variables to retrieve - /// An array of environment variable values (nulls for unset variables) - public static string?[] GetMultiple(params string[] variableNames) - { - EnsureInitialized(); - var result = new string?[variableNames.Length]; - for (var i = 0; i < variableNames.Length; i++) - { - _cache.TryGetValue(variableNames[i], out var value); - result[i] = value; - } - return result; - } - - /// - /// Checks if any of the specified environment variables have non-empty values - /// Useful for CI and container detection - /// - /// The environment variable names to check - /// True if any of the variables have non-empty values - public static bool HasAnyNonEmpty(params string[] variableNames) - { - EnsureInitialized(); - for (var i = 0; i < variableNames.Length; i++) - { - _cache.TryGetValue(variableNames[i], out var value); - if (!string.IsNullOrEmpty(value)) - { - return true; - } - } - return false; - } - - /// - /// CI environment variables for quick access - /// - public static readonly string[] CiVariables = - [ - "CI", - "CONTINUOUS_INTEGRATION", - "BUILD_ID", - "BUILD_NUMBER", - "GITHUB_ACTIONS", - "GITLAB_CI", - "AZURE_PIPELINES", - "JENKINS_URL", - "TEAMCITY_VERSION", - "APPVEYOR", - "CIRCLECI", - "TRAVIS" - ]; - - /// - /// Container environment variables for quick access - /// - public static readonly string[] ContainerVariables = - [ - "DOTNET_RUNNING_IN_CONTAINER", - "CONTAINER", - "KUBERNETES_SERVICE_HOST" - ]; - - /// - /// Convenience method to check if running in a CI environment - /// - public static bool IsRunningInCI() - { - return HasAnyNonEmpty(CiVariables); - } - - /// - /// Convenience method to check if running in a container - /// - public static bool IsRunningInContainer() - { - return HasAnyNonEmpty(ContainerVariables); - } - - /// - /// Initializes the cache with all TUnit environment variables - /// Thread-safe and only runs once - /// - private static void EnsureInitialized() - { - if (_initialized) - { - return; - } - - lock (_initLock) - { - if (_initialized) - { - return; - } - - // Cache all TUnit-related environment variables - foreach (var variableName in _tunitEnvironmentVariables) - { - var value = Environment.GetEnvironmentVariable(variableName); - _cache.TryAdd(variableName, value); - } - - _initialized = true; - } - } - - /// - /// For testing purposes - allows clearing and reinitializing the cache - /// - internal static void ClearCache() - { - lock (_initLock) - { - _cache.Clear(); - _initialized = false; - } - } -} \ No newline at end of file diff --git a/TUnit.Engine/Helpers/ExecutionModeHelper.cs b/TUnit.Engine/Helpers/ExecutionModeHelper.cs index bc654ca3cf..6364fceffd 100644 --- a/TUnit.Engine/Helpers/ExecutionModeHelper.cs +++ b/TUnit.Engine/Helpers/ExecutionModeHelper.cs @@ -67,7 +67,7 @@ public static bool IsSourceGenerationMode(ICommandLineOptions commandLineOptions } // Check environment variable - var envMode = EnvironmentVariableCache.Get("TUNIT_EXECUTION_MODE"); + var envMode = Environment.GetEnvironmentVariable("TUNIT_EXECUTION_MODE"); if (!string.IsNullOrEmpty(envMode)) { var mode = envMode!.ToLowerInvariant(); diff --git a/TUnit.Engine/Reporters/GitHubReporter.cs b/TUnit.Engine/Reporters/GitHubReporter.cs index ab8a2ace43..f7c37ef5c0 100644 --- a/TUnit.Engine/Reporters/GitHubReporter.cs +++ b/TUnit.Engine/Reporters/GitHubReporter.cs @@ -7,7 +7,6 @@ using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Extensions.TestHost; using TUnit.Engine.Framework; -using TUnit.Engine.Helpers; namespace TUnit.Engine.Reporters; @@ -25,18 +24,18 @@ public class GitHubReporter(IExtension extension) : IDataConsumer, ITestHostAppl public async Task IsEnabledAsync() { - if (EnvironmentVariableCache.Get("TUNIT_DISABLE_GITHUB_REPORTER") is not null || - EnvironmentVariableCache.Get("DISABLE_GITHUB_REPORTER") is not null) + if (Environment.GetEnvironmentVariable("TUNIT_DISABLE_GITHUB_REPORTER") is not null || + Environment.GetEnvironmentVariable("DISABLE_GITHUB_REPORTER") is not null) { return false; } - if (EnvironmentVariableCache.Get("GITHUB_ACTIONS") is null) + if (Environment.GetEnvironmentVariable("GITHUB_ACTIONS") is null) { return false; } - if (EnvironmentVariableCache.Get("GITHUB_STEP_SUMMARY") is not { } fileName + if (Environment.GetEnvironmentVariable("GITHUB_STEP_SUMMARY") is not { } fileName || !File.Exists(fileName)) { return false; @@ -45,7 +44,7 @@ public async Task IsEnabledAsync() _outputSummaryFilePath = fileName; // Determine reporter style from environment variable or default to collapsible - var styleEnv = EnvironmentVariableCache.Get("TUNIT_GITHUB_REPORTER_STYLE"); + var styleEnv = Environment.GetEnvironmentVariable("TUNIT_GITHUB_REPORTER_STYLE"); if (!string.IsNullOrEmpty(styleEnv)) { _reporterStyle = styleEnv!.ToLowerInvariant() switch diff --git a/TUnit.Engine/Reporters/JUnitReporter.cs b/TUnit.Engine/Reporters/JUnitReporter.cs new file mode 100644 index 0000000000..f80c52d5c9 --- /dev/null +++ b/TUnit.Engine/Reporters/JUnitReporter.cs @@ -0,0 +1,163 @@ +using System.Collections.Concurrent; +using System.Reflection; +using System.Text; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.TestHost; +using TUnit.Engine.Framework; +using TUnit.Engine.Xml; + +namespace TUnit.Engine.Reporters; + +public class JUnitReporter(IExtension extension) : IDataConsumer, ITestHostApplicationLifetime, IFilterReceiver +{ + private string _outputPath = null!; + private bool _isEnabled; + + public async Task IsEnabledAsync() + { + // Check if explicitly disabled + if (Environment.GetEnvironmentVariable("TUNIT_DISABLE_JUNIT_REPORTER") is not null) + { + return false; + } + + // Check if explicitly enabled OR running in GitLab CI + var explicitlyEnabled = Environment.GetEnvironmentVariable("TUNIT_ENABLE_JUNIT_REPORTER") is not null; + var runningInGitLab = Environment.GetEnvironmentVariable("GITLAB_CI") is not null || + Environment.GetEnvironmentVariable("CI_SERVER") is not null; + + if (!explicitlyEnabled && !runningInGitLab) + { + return false; + } + + // Determine output path (only if not already set via command-line argument) + if (string.IsNullOrEmpty(_outputPath)) + { + _outputPath = Environment.GetEnvironmentVariable("JUNIT_XML_OUTPUT_PATH") + ?? GetDefaultOutputPath(); + } + + _isEnabled = true; + return await extension.IsEnabledAsync(); + } + + public string Uid { get; } = $"{extension.Uid}JUnitReporter"; + + public string Version => extension.Version; + + public string DisplayName => extension.DisplayName; + + public string Description => extension.Description; + + private readonly ConcurrentDictionary> _updates = []; + + public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken) + { + var testNodeUpdateMessage = (TestNodeUpdateMessage)value; + + _updates.GetOrAdd(testNodeUpdateMessage.TestNode.Uid.Value, []).Add(testNodeUpdateMessage); + + return Task.CompletedTask; + } + + public Type[] DataTypesConsumed { get; } = [typeof(TestNodeUpdateMessage)]; + + public Task BeforeRunAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public async Task AfterRunAsync(int exitCode, CancellationToken cancellation) + { + if (!_isEnabled || _updates.Count == 0) + { + return; + } + + // Get the last update for each test + var lastUpdates = new List(_updates.Count); + foreach (var kvp in _updates.Where(kvp => kvp.Value.Count > 0)) + { + lastUpdates.Add(kvp.Value[kvp.Value.Count - 1]); + } + + // Generate JUnit XML + var xmlContent = JUnitXmlWriter.GenerateXml(lastUpdates, Filter); + + if (string.IsNullOrEmpty(xmlContent)) + { + return; + } + + // Write to file with retry logic + await WriteXmlFileAsync(_outputPath, xmlContent, cancellation); + } + + public string? Filter { get; set; } + + internal void SetOutputPath(string path) + { + if (string.IsNullOrWhiteSpace(path)) + { + throw new ArgumentException("Output path cannot be null or empty", nameof(path)); + } + + _outputPath = path; + } + + private static string GetDefaultOutputPath() + { + var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults"; + return Path.Combine("TestResults", $"{assemblyName}-junit.xml"); + } + + private static async Task WriteXmlFileAsync(string path, string content, CancellationToken cancellationToken) + { + // Ensure directory exists + var directory = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + const int maxAttempts = 5; + + for (int attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { +#if NET + await File.WriteAllTextAsync(path, content, Encoding.UTF8, cancellationToken); +#else + File.WriteAllText(path, content, Encoding.UTF8); +#endif + Console.WriteLine($"JUnit XML report written to: {path}"); + return; + } + catch (IOException ex) when (attempt < maxAttempts && IsFileLocked(ex)) + { + var baseDelay = 50 * Math.Pow(2, attempt - 1); + var jitter = Random.Shared.Next(0, 50); + var delay = (int)(baseDelay + jitter); + + Console.WriteLine($"JUnit XML file is locked, retrying in {delay}ms (attempt {attempt}/{maxAttempts})"); + await Task.Delay(delay, cancellationToken); + } + } + + Console.WriteLine($"Failed to write JUnit XML report to: {path} after {maxAttempts} attempts"); + } + + private static bool IsFileLocked(IOException exception) + { + // Check if the exception is due to the file being locked/in use + // HResult 0x80070020 is ERROR_SHARING_VIOLATION on Windows + // HResult 0x80070021 is ERROR_LOCK_VIOLATION on Windows + var errorCode = exception.HResult & 0xFFFF; + return errorCode == 0x20 || errorCode == 0x21 || + exception.Message.Contains("being used by another process") || + exception.Message.Contains("access denied", StringComparison.OrdinalIgnoreCase); + } +} diff --git a/TUnit.Engine/Services/AfterHookPairTracker.cs b/TUnit.Engine/Services/AfterHookPairTracker.cs new file mode 100644 index 0000000000..5e6e12deb5 --- /dev/null +++ b/TUnit.Engine/Services/AfterHookPairTracker.cs @@ -0,0 +1,155 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using TUnit.Core.Data; + +namespace TUnit.Engine.Services; + +/// +/// Responsible for ensuring After hooks run even when tests are cancelled. +/// When a Before hook completes, this tracker registers the corresponding After hook +/// to run on cancellation, guaranteeing cleanup even if the test is aborted. +/// Follows Single Responsibility Principle - only handles After hook pairing and cancellation registration. +/// +internal sealed class AfterHookPairTracker +{ + // Cached After hook tasks to ensure they run only once (prevent double execution) + private readonly ThreadSafeDictionary>> _afterClassTasks = new(); + private readonly ThreadSafeDictionary>> _afterAssemblyTasks = new(); + private Task>? _afterTestSessionTask; + private readonly object _testSessionLock = new(); + private readonly object _classLock = new(); + + // Track cancellation registrations for cleanup + private readonly ConcurrentBag _registrations = []; + + /// + /// Registers Session After hooks to run on cancellation or normal completion. + /// Ensures After hooks run exactly once even if called both ways. + /// + public void RegisterAfterTestSessionHook( + CancellationToken cancellationToken, + Func>> afterHookExecutor) + { + // Register callback to run After hook on cancellation + var registration = cancellationToken.Register(() => + { + // Use sync-over-async here because CancellationToken.Register requires Action (not Func) + // Fire-and-forget is acceptable here - exceptions will be collected when hooks run normally + _ = GetOrCreateAfterTestSessionTask(afterHookExecutor); + }); + + _registrations.Add(registration); + } + + /// + /// Registers Assembly After hooks to run on cancellation or normal completion. + /// Ensures After hooks run exactly once even if called both ways. + /// + public void RegisterAfterAssemblyHook( + Assembly assembly, + CancellationToken cancellationToken, + Func>> afterHookExecutor) + { + var registration = cancellationToken.Register(() => + { + _ = GetOrCreateAfterAssemblyTask(assembly, afterHookExecutor); + }); + + _registrations.Add(registration); + } + + /// + /// Registers Class After hooks to run on cancellation or normal completion. + /// Ensures After hooks run exactly once even if called both ways. + /// + public void RegisterAfterClassHook( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] + Type testClass, + HookExecutor hookExecutor, + CancellationToken cancellationToken) + { + var registration = cancellationToken.Register(() => + { + _ = GetOrCreateAfterClassTask(testClass, hookExecutor, CancellationToken.None); + }); + + _registrations.Add(registration); + } + + /// + /// Gets or creates the After Test Session task, ensuring it runs only once. + /// Thread-safe using double-checked locking. + /// Returns the exceptions from hook execution. + /// + public ValueTask> GetOrCreateAfterTestSessionTask(Func>> taskFactory) + { + if (_afterTestSessionTask != null) + { + return new ValueTask>(_afterTestSessionTask); + } + + lock (_testSessionLock) + { + if (_afterTestSessionTask == null) + { + _afterTestSessionTask = taskFactory().AsTask(); + } + return new ValueTask>(_afterTestSessionTask); + } + } + + /// + /// Gets or creates the After Assembly task for the specified assembly. + /// Thread-safe using ThreadSafeDictionary. + /// Returns the exceptions from hook execution. + /// + public ValueTask> GetOrCreateAfterAssemblyTask(Assembly assembly, Func>> taskFactory) + { + var task = _afterAssemblyTasks.GetOrAdd(assembly, a => taskFactory(a).AsTask()); + return new ValueTask>(task); + } + + /// + /// Gets or creates the After Class task for the specified test class. + /// Thread-safe using double-checked locking. + /// Returns the exceptions from hook execution. + /// + public ValueTask> GetOrCreateAfterClassTask( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] + Type testClass, + HookExecutor hookExecutor, + CancellationToken cancellationToken) + { + if (_afterClassTasks.TryGetValue(testClass, out var existingTask)) + { + return new ValueTask>(existingTask); + } + + lock (_classLock) + { + if (_afterClassTasks.TryGetValue(testClass, out existingTask)) + { + return new ValueTask>(existingTask); + } + + // Call ExecuteAfterClassHooksAsync directly with the annotated testClass + // The factory ignores the key since we've already created the task with the annotated type + var newTask = hookExecutor.ExecuteAfterClassHooksAsync(testClass, cancellationToken).AsTask(); + _afterClassTasks.GetOrAdd(testClass, _ => newTask); + return new ValueTask>(newTask); + } + } + + /// + /// Disposes all cancellation token registrations. + /// Should be called at the end of test execution to clean up resources. + /// + public void Dispose() + { + foreach (var registration in _registrations) + { + registration.Dispose(); + } + } +} diff --git a/TUnit.Engine/Services/BeforeHookTaskCache.cs b/TUnit.Engine/Services/BeforeHookTaskCache.cs index 3b76326dac..1a23fe93af 100644 --- a/TUnit.Engine/Services/BeforeHookTaskCache.cs +++ b/TUnit.Engine/Services/BeforeHookTaskCache.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Reflection; using TUnit.Core.Data; @@ -16,8 +15,9 @@ internal sealed class BeforeHookTaskCache private readonly ThreadSafeDictionary _beforeAssemblyTasks = new(); private Task? _beforeTestSessionTask; private readonly object _testSessionLock = new(); + private readonly object _classLock = new(); - public ValueTask GetOrCreateBeforeTestSessionTask(Func taskFactory) + public ValueTask GetOrCreateBeforeTestSessionTask(Func taskFactory, CancellationToken cancellationToken) { if (_beforeTestSessionTask != null) { @@ -28,23 +28,41 @@ public ValueTask GetOrCreateBeforeTestSessionTask(Func taskFactory) { if (_beforeTestSessionTask == null) { - _beforeTestSessionTask = taskFactory().AsTask(); + _beforeTestSessionTask = taskFactory(cancellationToken).AsTask(); } return new ValueTask(_beforeTestSessionTask); } } - public ValueTask GetOrCreateBeforeAssemblyTask(Assembly assembly, Func taskFactory) + public ValueTask GetOrCreateBeforeAssemblyTask(Assembly assembly, Func taskFactory, CancellationToken cancellationToken) { - var task = _beforeAssemblyTasks.GetOrAdd(assembly, a => taskFactory(a).AsTask()); + var task = _beforeAssemblyTasks.GetOrAdd(assembly, a => taskFactory(a, cancellationToken).AsTask()); return new ValueTask(task); } public ValueTask GetOrCreateBeforeClassTask( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicMethods)] - Type testClass, Func taskFactory) + Type testClass, + HookExecutor hookExecutor, + CancellationToken cancellationToken) { - var task = _beforeClassTasks.GetOrAdd(testClass, t => taskFactory(t).AsTask()); - return new ValueTask(task); + if (_beforeClassTasks.TryGetValue(testClass, out var existingTask)) + { + return new ValueTask(existingTask); + } + + lock (_classLock) + { + if (_beforeClassTasks.TryGetValue(testClass, out existingTask)) + { + return new ValueTask(existingTask); + } + + // Call ExecuteBeforeClassHooksAsync directly with the annotated testClass + // The factory ignores the key since we've already created the task with the annotated type + var newTask = hookExecutor.ExecuteBeforeClassHooksAsync(testClass, cancellationToken).AsTask(); + _beforeClassTasks.GetOrAdd(testClass, _ => newTask); + return new ValueTask(newTask); + } } } diff --git a/TUnit.Engine/Services/DataSourceInitializer.cs b/TUnit.Engine/Services/DataSourceInitializer.cs deleted file mode 100644 index ae69a2f52e..0000000000 --- a/TUnit.Engine/Services/DataSourceInitializer.cs +++ /dev/null @@ -1,241 +0,0 @@ -using System.Collections.Concurrent; -using TUnit.Core; -using TUnit.Core.Interfaces; -using TUnit.Core.PropertyInjection; - -namespace TUnit.Engine.Services; - -/// -/// Centralized service responsible for initializing data source instances. -/// Ensures all data sources are properly initialized before use, regardless of where they're used -/// (properties, constructor arguments, or method arguments). -/// -internal sealed class DataSourceInitializer -{ - private readonly ConcurrentDictionary> _initializationTasks = new(); - private PropertyInjectionService? _propertyInjectionService; - - /// - /// Completes initialization by providing the PropertyInjectionService. - /// This two-phase initialization breaks the circular dependency. - /// - public void Initialize(PropertyInjectionService propertyInjectionService) - { - _propertyInjectionService = propertyInjectionService ?? throw new ArgumentNullException(nameof(propertyInjectionService)); - } - - /// - /// Ensures a data source instance is fully initialized before use. - /// This includes property injection and calling IAsyncInitializer if implemented. - /// Optimized with fast-path for already-initialized data sources. - /// - public async ValueTask EnsureInitializedAsync( - T dataSource, - ConcurrentDictionary? objectBag = null, - MethodMetadata? methodMetadata = null, - TestContextEvents? events = null, - CancellationToken cancellationToken = default) where T : notnull - { - if (dataSource == null) - { - throw new ArgumentNullException(nameof(dataSource)); - } - - // Fast path: Check if already initialized (avoids Task allocation) - if (_initializationTasks.TryGetValue(dataSource, out var existingTcs) && existingTcs.Task.IsCompleted) - { - // Already initialized - return synchronously without allocating a Task - if (existingTcs.Task.IsFaulted) - { - // Re-throw the original exception - await existingTcs.Task.ConfigureAwait(false); - } - - return dataSource; - } - - // Slow path: Need to initialize or wait for initialization - var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - existingTcs = _initializationTasks.GetOrAdd(dataSource, tcs); - - if (existingTcs == tcs) - { - try - { - await InitializeDataSourceAsync(dataSource, objectBag, methodMetadata, events, cancellationToken).ConfigureAwait(false); - tcs.SetResult(true); - } - catch (Exception ex) - { - tcs.SetException(ex); - throw; - } - } - else - { - // Another thread is initializing or already initialized - wait for it - await existingTcs.Task.ConfigureAwait(false); - - // Check cancellation after waiting - if (cancellationToken.CanBeCanceled) - { - cancellationToken.ThrowIfCancellationRequested(); - } - } - - return dataSource; - } - - /// - /// Initializes a data source instance with the complete lifecycle. - /// - private async Task InitializeDataSourceAsync( - object dataSource, - ConcurrentDictionary? objectBag, - MethodMetadata? methodMetadata, - TestContextEvents? events, - CancellationToken cancellationToken) - { - try - { - // Ensure we have required context - objectBag ??= new ConcurrentDictionary(); - events ??= new TestContextEvents(); - - // Initialize the data source directly here - // Step 1: Property injection (if PropertyInjectionService has been initialized) - if (_propertyInjectionService != null && PropertyInjectionCache.HasInjectableProperties(dataSource.GetType())) - { - await _propertyInjectionService.InjectPropertiesIntoObjectAsync( - dataSource, objectBag, methodMetadata, events); - } - - // Step 2: Initialize nested property-injected objects (deepest first) - // This ensures that when the parent's IAsyncInitializer runs, all nested objects are already initialized - await InitializeNestedObjectsAsync(dataSource, cancellationToken); - - // Step 3: IAsyncInitializer on the data source itself - if (dataSource is IAsyncInitializer asyncInitializer) - { - await ObjectInitializer.InitializeAsync(asyncInitializer, cancellationToken); - } - } - catch (Exception ex) - { - throw new InvalidOperationException( - $"Failed to initialize data source of type '{dataSource.GetType().Name}': {ex.Message}", ex); - } - } - - /// - /// Initializes all nested property-injected objects in depth-first order. - /// This ensures that when the parent's IAsyncInitializer runs, all nested dependencies are already initialized. - /// - private async Task InitializeNestedObjectsAsync(object rootObject, CancellationToken cancellationToken) - { - var objectsByDepth = new Dictionary>(capacity: 4); - var visitedObjects = new HashSet(); - - // Collect all nested property-injected objects grouped by depth - CollectNestedObjects(rootObject, objectsByDepth, visitedObjects, currentDepth: 1); - - // Initialize objects deepest-first (highest depth to lowest) - var depths = objectsByDepth.Keys.OrderByDescending(depth => depth); - - foreach (var depth in depths) - { - var objectsAtDepth = objectsByDepth[depth]; - - // Initialize all objects at this depth in parallel - await Task.WhenAll(objectsAtDepth.Select(obj => ObjectInitializer.InitializeAsync(obj, cancellationToken).AsTask())); - } - } - - /// - /// Recursively collects all nested property-injected objects grouped by depth. - /// - private void CollectNestedObjects( - object obj, - Dictionary> objectsByDepth, - HashSet visitedObjects, - int currentDepth) - { - var plan = PropertyInjectionCache.GetOrCreatePlan(obj.GetType()); - - if (!SourceRegistrar.IsEnabled) - { - // Reflection mode - foreach (var prop in plan.ReflectionProperties) - { - var value = prop.Property.GetValue(obj); - - if (value == null || !visitedObjects.Add(value)) - { - continue; - } - - // Add to the current depth level if it has injectable properties or implements IAsyncInitializer - if (PropertyInjectionCache.HasInjectableProperties(value.GetType()) || value is IAsyncInitializer) - { - if (!objectsByDepth.ContainsKey(currentDepth)) - { - objectsByDepth[currentDepth] = []; - } - - objectsByDepth[currentDepth].Add(value); - } - - // Recursively collect nested objects - if (PropertyInjectionCache.HasInjectableProperties(value.GetType())) - { - CollectNestedObjects(value, objectsByDepth, visitedObjects, currentDepth + 1); - } - } - } - else - { - // Source-generated mode - foreach (var metadata in plan.SourceGeneratedProperties) - { - var property = metadata.ContainingType.GetProperty(metadata.PropertyName); - - if (property == null || !property.CanRead) - { - continue; - } - - var value = property.GetValue(obj); - - if (value == null || !visitedObjects.Add(value)) - { - continue; - } - - // Add to the current depth level if it has injectable properties or implements IAsyncInitializer - if (PropertyInjectionCache.HasInjectableProperties(value.GetType()) || value is IAsyncInitializer) - { - if (!objectsByDepth.ContainsKey(currentDepth)) - { - objectsByDepth[currentDepth] = []; - } - - objectsByDepth[currentDepth].Add(value); - } - - // Recursively collect nested objects - if (PropertyInjectionCache.HasInjectableProperties(value.GetType())) - { - CollectNestedObjects(value, objectsByDepth, visitedObjects, currentDepth + 1); - } - } - } - } - - /// - /// Clears the initialization cache. Should be called at the end of test sessions. - /// - public void ClearCache() - { - _initializationTasks.Clear(); - } -} diff --git a/TUnit.Engine/Services/EventReceiverOrchestrator.cs b/TUnit.Engine/Services/EventReceiverOrchestrator.cs index 014221efca..0d127feb35 100644 --- a/TUnit.Engine/Services/EventReceiverOrchestrator.cs +++ b/TUnit.Engine/Services/EventReceiverOrchestrator.cs @@ -5,7 +5,6 @@ using TUnit.Core.Enums; using TUnit.Core.Helpers; using TUnit.Core.Interfaces; -using TUnit.Core.Tracking; using TUnit.Engine.Events; using TUnit.Engine.Extensions; using TUnit.Engine.Logging; @@ -17,7 +16,6 @@ internal sealed class EventReceiverOrchestrator : IDisposable { private readonly EventReceiverRegistry _registry = new(); private readonly TUnitFrameworkLogger _logger; - private readonly TrackableObjectGraphProvider _trackableObjectGraphProvider; // Track which assemblies/classes/sessions have had their "first" event invoked private ThreadSafeDictionary _firstTestInAssemblyTasks = new(); @@ -35,10 +33,9 @@ internal sealed class EventReceiverOrchestrator : IDisposable // Track registered First event receiver types to avoid duplicate registrations private readonly ConcurrentHashSet _registeredFirstEventReceiverTypes = new(); - public EventReceiverOrchestrator(TUnitFrameworkLogger logger, TrackableObjectGraphProvider trackableObjectGraphProvider) + public EventReceiverOrchestrator(TUnitFrameworkLogger logger) { _logger = logger; - _trackableObjectGraphProvider = trackableObjectGraphProvider; } public void RegisterReceivers(TestContext context, CancellationToken cancellationToken) diff --git a/TUnit.Engine/Services/HookExecutor.cs b/TUnit.Engine/Services/HookExecutor.cs index 46a58414a6..5421310afd 100644 --- a/TUnit.Engine/Services/HookExecutor.cs +++ b/TUnit.Engine/Services/HookExecutor.cs @@ -57,7 +57,7 @@ public async ValueTask ExecuteBeforeTestSessionHooksAsync(CancellationToken canc ExceptionDispatchInfo.Capture(skipEx).Throw(); } - throw new BeforeTestSessionException("BeforeTestSession hook failed", ex); + throw new BeforeTestSessionException($"BeforeTestSession hook failed: {ex.Message}", ex); } } } @@ -83,7 +83,7 @@ public async ValueTask> ExecuteAfterTestSessionHooksAsync(Cancel { // Collect hook exceptions instead of throwing immediately // This allows all hooks to run even if some fail - exceptions.Add(new AfterTestSessionException("AfterTestSession hook failed", ex)); + exceptions.Add(new AfterTestSessionException($"AfterTestSession hook failed: {ex.Message}", ex)); } } @@ -119,7 +119,7 @@ public async ValueTask ExecuteBeforeAssemblyHooksAsync(Assembly assembly, Cancel ExceptionDispatchInfo.Capture(skipEx).Throw(); } - throw new BeforeAssemblyException("BeforeAssembly hook failed", ex); + throw new BeforeAssemblyException($"BeforeAssembly hook failed: {ex.Message}", ex); } } } @@ -146,7 +146,7 @@ public async ValueTask> ExecuteAfterAssemblyHooksAsync(Assembly { // Collect hook exceptions instead of throwing immediately // This allows all hooks to run even if some fail - exceptions.Add(new AfterAssemblyException("AfterAssembly hook failed", ex)); + exceptions.Add(new AfterAssemblyException($"AfterAssembly hook failed: {ex.Message}", ex)); } } @@ -184,7 +184,7 @@ public async ValueTask ExecuteBeforeClassHooksAsync( ExceptionDispatchInfo.Capture(skipEx).Throw(); } - throw new BeforeClassException("BeforeClass hook failed", ex); + throw new BeforeClassException($"BeforeClass hook failed: {ex.Message}", ex); } } } @@ -213,7 +213,7 @@ public async ValueTask> ExecuteAfterClassHooksAsync( { // Collect hook exceptions instead of throwing immediately // This allows all hooks to run even if some fail - exceptions.Add(new AfterClassException("AfterClass hook failed", ex)); + exceptions.Add(new AfterClassException($"AfterClass hook failed: {ex.Message}", ex)); } } @@ -248,7 +248,7 @@ public async ValueTask ExecuteBeforeTestHooksAsync(AbstractExecutableTest test, ExceptionDispatchInfo.Capture(skipEx).Throw(); } - throw new BeforeTestException("BeforeEveryTest hook failed", ex); + throw new BeforeTestException($"BeforeEveryTest hook failed: {ex.Message}", ex); } } } @@ -277,7 +277,7 @@ public async ValueTask ExecuteBeforeTestHooksAsync(AbstractExecutableTest test, ExceptionDispatchInfo.Capture(skipEx).Throw(); } - throw new BeforeTestException("BeforeTest hook failed", ex); + throw new BeforeTestException($"BeforeTest hook failed: {ex.Message}", ex); } } } @@ -347,7 +347,7 @@ public async ValueTask ExecuteBeforeTestDiscoveryHooksAsync(CancellationToken ca } catch (Exception ex) { - throw new BeforeTestDiscoveryException("BeforeTestDiscovery hook failed", ex); + throw new BeforeTestDiscoveryException($"BeforeTestDiscovery hook failed: {ex.Message}", ex); } } } @@ -370,7 +370,7 @@ public async ValueTask ExecuteAfterTestDiscoveryHooksAsync(CancellationToken can } catch (Exception ex) { - throw new AfterTestDiscoveryException("AfterTestDiscovery hook failed", ex); + throw new AfterTestDiscoveryException($"AfterTestDiscovery hook failed: {ex.Message}", ex); } } } diff --git a/TUnit.Engine/Services/ObjectGraphDiscoveryService.cs b/TUnit.Engine/Services/ObjectGraphDiscoveryService.cs new file mode 100644 index 0000000000..1a0636f605 --- /dev/null +++ b/TUnit.Engine/Services/ObjectGraphDiscoveryService.cs @@ -0,0 +1,45 @@ +using TUnit.Core; +using TUnit.Core.Discovery; +using TUnit.Core.Interfaces; + +namespace TUnit.Engine.Services; + +/// +/// Service for discovering and organizing object graphs in TUnit.Engine. +/// Delegates to in TUnit.Core for the actual discovery logic. +/// +internal sealed class ObjectGraphDiscoveryService +{ + private readonly IObjectGraphDiscoverer _discoverer; + + /// + /// Creates a new instance with the default discoverer. + /// + public ObjectGraphDiscoveryService() : this(new ObjectGraphDiscoverer()) + { + } + + /// + /// Creates a new instance with a custom discoverer (for testing). + /// + public ObjectGraphDiscoveryService(IObjectGraphDiscoverer discoverer) + { + _discoverer = discoverer; + } + + /// + /// Discovers all objects from test context arguments and properties, organized by depth level. + /// + public IObjectGraph DiscoverObjectGraph(TestContext testContext, CancellationToken cancellationToken = default) + { + return _discoverer.DiscoverObjectGraph(testContext, cancellationToken); + } + + /// + /// Discovers nested objects from a single root object, organized by depth. + /// + public IObjectGraph DiscoverNestedObjectGraph(object rootObject, CancellationToken cancellationToken = default) + { + return _discoverer.DiscoverNestedObjectGraph(rootObject, cancellationToken); + } +} diff --git a/TUnit.Engine/Services/ObjectLifecycleService.cs b/TUnit.Engine/Services/ObjectLifecycleService.cs new file mode 100644 index 0000000000..8ab1773f6a --- /dev/null +++ b/TUnit.Engine/Services/ObjectLifecycleService.cs @@ -0,0 +1,464 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using TUnit.Core; +using TUnit.Core.Helpers; +using TUnit.Core.Interfaces; +using TUnit.Core.PropertyInjection; +using TUnit.Core.PropertyInjection.Initialization; +using TUnit.Core.Tracking; + +namespace TUnit.Engine.Services; + +/// +/// Unified service for managing object lifecycle. +/// Orchestrates: registration, property injection, initialization (IAsyncInitializer), and tracking. +/// Replaces the fragmented: PropertyInjectionService, DataSourceInitializer, PropertyInitializationOrchestrator, +/// PropertyDataResolver, and ObjectRegistrationService. +/// +/// Uses Lazy<T> for dependencies to break circular references without manual Initialize() calls. +/// Follows clear phase separation: Register → Inject → Initialize → Cleanup. +/// +/// +/// Implements to allow PropertyInjector to call back for initialization +/// without creating a direct dependency (breaking the circular reference pattern). +/// +internal sealed class ObjectLifecycleService : IObjectRegistry, IInitializationCallback +{ + private readonly Lazy _propertyInjector; + private readonly ObjectGraphDiscoveryService _objectGraphDiscoveryService; + private readonly ObjectTracker _objectTracker; + + // Track initialization state per object + // Use ReferenceEqualityComparer to prevent objects with custom Equals from sharing initialization state + private readonly ConcurrentDictionary> _initializationTasks = + new(Core.Helpers.ReferenceEqualityComparer.Instance); + + public ObjectLifecycleService( + Lazy propertyInjector, + ObjectGraphDiscoveryService objectGraphDiscoveryService, + ObjectTracker objectTracker) + { + _propertyInjector = propertyInjector; + _objectGraphDiscoveryService = objectGraphDiscoveryService; + _objectTracker = objectTracker; + } + + private PropertyInjector PropertyInjector => _propertyInjector.Value; + + #region Phase 1: Registration (Discovery Time) + + /// + /// Registers a test for lifecycle management during discovery. + /// Resolves and caches property values (to create shared objects early) without setting them on the placeholder instance. + /// Tracks the resolved objects so reference counting works correctly across all tests. + /// Does NOT call IAsyncInitializer (deferred to execution). + /// + public async Task RegisterTestAsync(TestContext testContext) + { + var objectBag = testContext.StateBag.Items; + var methodMetadata = testContext.Metadata.TestDetails.MethodMetadata; + var events = testContext.InternalEvents; + var testClassType = testContext.Metadata.TestDetails.ClassType; + + // Resolve property values (creating shared objects) and cache them WITHOUT setting on placeholder instance + // This ensures shared objects are created once and tracked with the correct reference count + await PropertyInjector.ResolveAndCachePropertiesAsync(testClassType, objectBag, methodMetadata, events, testContext); + + // Track the cached objects so they get the correct reference count + _objectTracker.TrackObjects(testContext); + } + + /// + /// IObjectRegistry implementation - registers a single object. + /// Injects properties but does NOT call IAsyncInitializer (deferred to execution). + /// + public async Task RegisterObjectAsync( + object instance, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + // Inject properties during registration + await PropertyInjector.InjectPropertiesAsync(instance, objectBag, methodMetadata, events); + } + + /// + /// IObjectRegistry implementation - registers multiple argument objects. + /// + public async Task RegisterArgumentsAsync( + object?[] arguments, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events) + { + if (arguments == null || arguments.Length == 0) + { + return; + } + + // Pre-allocate with expected capacity to avoid resizing + var tasks = new List(arguments.Length); + foreach (var argument in arguments) + { + if (argument != null) + { + tasks.Add(RegisterObjectAsync(argument, objectBag, methodMetadata, events)); + } + } + + if (tasks.Count > 0) + { + await Task.WhenAll(tasks); + } + } + + #endregion + + #region Phase 2: Preparation (Execution Time) + + /// + /// Prepares a test for execution. + /// Sets already-resolved cached property values on the current instance. + /// This is needed because retries create new instances that don't have properties set yet. + /// Does NOT call IAsyncInitializer - that is deferred until after BeforeClass hooks via InitializeTestObjectsAsync. + /// + public void PrepareTest(TestContext testContext) + { + var testClassInstance = testContext.Metadata.TestDetails.ClassInstance; + + // Set already-cached property values on the current instance + // Properties were resolved and cached during RegisterTestAsync, so shared objects are already created + // We just need to set them on the actual test instance (retries create new instances) + SetCachedPropertiesOnInstance(testClassInstance, testContext); + } + + /// + /// Initializes test objects (IAsyncInitializer) after BeforeClass hooks have run. + /// This ensures resources like Docker containers are not started until needed. + /// + public async Task InitializeTestObjectsAsync(TestContext testContext, CancellationToken cancellationToken) + { + // Initialize all tracked objects (IAsyncInitializer) depth-first + await InitializeTrackedObjectsAsync(testContext, cancellationToken); + } + + /// + /// Sets already-cached property values on a test class instance. + /// This is used to apply cached property values to new instances created during retries. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT")] + private void SetCachedPropertiesOnInstance(object instance, TestContext testContext) + { + var plan = PropertyInjectionCache.GetOrCreatePlan(instance.GetType()); + + if (!plan.HasProperties) + { + return; + } + + var cachedProperties = testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments; + + if (plan.SourceGeneratedProperties.Length > 0) + { + foreach (var metadata in plan.SourceGeneratedProperties) + { + var cacheKey = PropertyCacheKeyGenerator.GetCacheKey(metadata); + + if (cachedProperties.TryGetValue(cacheKey, out var cachedValue) && cachedValue != null) + { + // Set the cached value on the new instance + metadata.SetProperty(instance, cachedValue); + } + } + } + else if (plan.ReflectionProperties.Length > 0) + { + foreach (var (property, _) in plan.ReflectionProperties) + { + var cacheKey = PropertyCacheKeyGenerator.GetCacheKey(property); + + if (cachedProperties.TryGetValue(cacheKey, out var cachedValue) && cachedValue != null) + { + // Set the cached value on the new instance + var setter = PropertySetterFactory.CreateSetter(property); + setter(instance, cachedValue); + } + } + } + } + + /// + /// Initializes all tracked objects depth-first (deepest objects first). + /// This is called during test execution (after BeforeClass hooks) to initialize IAsyncInitializer objects. + /// Objects at the same level are initialized in parallel. + /// + private async Task InitializeTrackedObjectsAsync(TestContext testContext, CancellationToken cancellationToken) + { + // Get levels without LINQ - use Array.Sort with reverse comparison for descending order + var trackedObjects = testContext.TrackedObjects; + var levelCount = trackedObjects.Count; + + if (levelCount > 0) + { + var levels = new int[levelCount]; + trackedObjects.Keys.CopyTo(levels, 0); + Array.Sort(levels, (a, b) => b.CompareTo(a)); // Descending order + + foreach (var level in levels) + { + if (!trackedObjects.TryGetValue(level, out var objectsAtLevel)) + { + continue; + } + + // Copy to array under lock to prevent concurrent modification + object[] objectsCopy; + lock (objectsAtLevel) + { + objectsCopy = new object[objectsAtLevel.Count]; + objectsAtLevel.CopyTo(objectsCopy); + } + + // Initialize all objects at this level in parallel + var tasks = new List(objectsCopy.Length); + foreach (var obj in objectsCopy) + { + tasks.Add(InitializeObjectWithNestedAsync(obj, cancellationToken)); + } + + if (tasks.Count > 0) + { + await Task.WhenAll(tasks); + } + } + } + + // Finally initialize the test class and its nested objects + var classInstance = testContext.Metadata.TestDetails.ClassInstance; + await InitializeNestedObjectsForExecutionAsync(classInstance, cancellationToken); + await ObjectInitializer.InitializeAsync(classInstance, cancellationToken); + } + + /// + /// Initializes an object and its nested objects. + /// + private async Task InitializeObjectWithNestedAsync(object obj, CancellationToken cancellationToken) + { + // First initialize nested objects depth-first + await InitializeNestedObjectsForExecutionAsync(obj, cancellationToken); + + // Then initialize the object itself + await ObjectInitializer.InitializeAsync(obj, cancellationToken); + } + + /// + /// Initializes nested objects during execution phase - all IAsyncInitializer objects. + /// + private Task InitializeNestedObjectsForExecutionAsync(object rootObject, CancellationToken cancellationToken) + { + return InitializeNestedObjectsAsync( + rootObject, + ObjectInitializer.InitializeAsync, + cancellationToken); + } + + #endregion + + #region Phase 3: Object Initialization + + /// + /// Injects properties into an object without calling IAsyncInitializer. + /// Used during test discovery to prepare data sources without triggering async initialization. + /// + public async ValueTask InjectPropertiesAsync( + T obj, + ConcurrentDictionary? objectBag = null, + MethodMetadata? methodMetadata = null, + TestContextEvents? events = null) where T : notnull + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + objectBag ??= new ConcurrentDictionary(); + events ??= new TestContextEvents(); + + // Only inject properties, do not call IAsyncInitializer + await PropertyInjector.InjectPropertiesAsync(obj, objectBag, methodMetadata, events); + + return obj; + } + + /// + /// Ensures an object is fully initialized (property injection + IAsyncInitializer). + /// Thread-safe with fast-path for already-initialized objects. + /// Called during test execution to initialize all IAsyncInitializer objects. + /// + public async ValueTask EnsureInitializedAsync( + T obj, + ConcurrentDictionary? objectBag = null, + MethodMetadata? methodMetadata = null, + TestContextEvents? events = null, + CancellationToken cancellationToken = default) where T : notnull + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + // Fast path: already processed by this service + if (_initializationTasks.TryGetValue(obj, out var existingTcs) && existingTcs.Task.IsCompleted) + { + if (existingTcs.Task.IsFaulted) + { + await existingTcs.Task.ConfigureAwait(false); + } + + // EnsureInitializedAsync is only called during discovery (from PropertyInjector). + // If the object is shared and has already been processed, just return it. + // Regular IAsyncInitializer objects will be initialized during execution via InitializeTrackedObjectsAsync. + return obj; + } + + // Slow path: need to initialize + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + existingTcs = _initializationTasks.GetOrAdd(obj, tcs); + + if (existingTcs == tcs) + { + try + { + await InitializeObjectCoreAsync(obj, objectBag, methodMetadata, events, cancellationToken); + tcs.SetResult(true); + } + catch (OperationCanceledException) + { + // Propagate cancellation without caching failure - allows retry after cancel + _initializationTasks.TryRemove(obj, out _); + tcs.SetCanceled(); + throw; + } + catch (Exception ex) + { + // Remove failed initialization from cache to allow retry + // This is important for transient failures that may succeed on retry + _initializationTasks.TryRemove(obj, out _); + tcs.SetException(ex); + throw; + } + } + else + { + await existingTcs.Task.ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + } + + return obj; + } + + /// + /// Core initialization: property injection + IAsyncDiscoveryInitializer only. + /// Regular IAsyncInitializer objects are NOT initialized here - they are deferred to execution phase. + /// + private async Task InitializeObjectCoreAsync( + object obj, + ConcurrentDictionary? objectBag, + MethodMetadata? methodMetadata, + TestContextEvents? events, + CancellationToken cancellationToken) + { + objectBag ??= new ConcurrentDictionary(); + events ??= new TestContextEvents(); + + // Let exceptions propagate naturally - don't wrap in InvalidOperationException + // This aligns with ObjectInitializer behavior and provides cleaner stack traces + + // Step 1: Inject properties + await PropertyInjector.InjectPropertiesAsync(obj, objectBag, methodMetadata, events); + + // Step 2: Initialize nested objects depth-first (discovery-only) + await InitializeNestedObjectsForDiscoveryAsync(obj, cancellationToken); + + // Step 3: Call IAsyncDiscoveryInitializer only (not regular IAsyncInitializer) + // Regular IAsyncInitializer objects are deferred to execution phase via InitializeTestObjectsAsync + await ObjectInitializer.InitializeForDiscoveryAsync(obj, cancellationToken); + } + + /// + /// Initializes nested objects during discovery phase - only IAsyncDiscoveryInitializer objects. + /// + private Task InitializeNestedObjectsForDiscoveryAsync(object rootObject, CancellationToken cancellationToken) + { + return InitializeNestedObjectsAsync( + rootObject, + ObjectInitializer.InitializeForDiscoveryAsync, + cancellationToken); + } + + /// + /// Shared implementation for nested object initialization (DRY). + /// Discovers nested objects and initializes them depth-first using the provided initializer. + /// + /// The root object to discover nested objects from. + /// The initializer function to call for each object. + /// Cancellation token. + private async Task InitializeNestedObjectsAsync( + object rootObject, + Func initializer, + CancellationToken cancellationToken) + { + var graph = _objectGraphDiscoveryService.DiscoverNestedObjectGraph(rootObject, cancellationToken); + + // Initialize from deepest to shallowest (skip depth 0 which is the root itself) + foreach (var depth in graph.GetDepthsDescending()) + { + if (depth == 0) + { + continue; // Root handled separately + } + + var objectsAtDepth = graph.GetObjectsAtDepth(depth); + + // Pre-allocate task list without LINQ Select + var tasks = new List(); + foreach (var obj in objectsAtDepth) + { + tasks.Add(initializer(obj, cancellationToken).AsTask()); + } + + if (tasks.Count > 0) + { + await Task.WhenAll(tasks); + } + } + } + + #endregion + + #region Phase 4: Cleanup + + /// + /// Cleans up after test execution. + /// Decrements reference counts and disposes objects when count reaches zero. + /// + public async Task CleanupTestAsync(TestContext testContext, List cleanupExceptions) + { + await _objectTracker.UntrackObjects(testContext, cleanupExceptions); + } + + #endregion + + /// + /// Clears the initialization cache. Called at end of test session. + /// + public void ClearCache() + { + _initializationTasks.Clear(); + } +} diff --git a/TUnit.Engine/Services/ObjectRegistrationService.cs b/TUnit.Engine/Services/ObjectRegistrationService.cs deleted file mode 100644 index 26b8bc2bbe..0000000000 --- a/TUnit.Engine/Services/ObjectRegistrationService.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using TUnit.Core; -using TUnit.Core.PropertyInjection; -using TUnit.Core.Tracking; - -namespace TUnit.Engine.Services; - -/// -/// Handles object registration during the test discovery/registration phase. -/// Responsibilities: Create instances, inject properties, track for disposal (ONCE per object). -/// Does NOT call IAsyncInitializer - that's deferred to ObjectInitializationService during execution. -/// -internal sealed class ObjectRegistrationService : IObjectRegistry -{ - private readonly PropertyInjectionService _propertyInjectionService; - - public ObjectRegistrationService( - PropertyInjectionService propertyInjectionService) - { - _propertyInjectionService = propertyInjectionService ?? throw new ArgumentNullException(nameof(propertyInjectionService)); - } - - /// - /// Registers a single object during the registration phase. - /// Injects properties, tracks for disposal (once), but does NOT call IAsyncInitializer. - /// - /// The object instance to register. Must not be null. - /// Shared object bag for the test context. Must not be null. - /// Method metadata for the test. Can be null. - /// Test context events for tracking. Must not be null and must be unique per test permutation. - public async Task RegisterObjectAsync( - object instance, - ConcurrentDictionary objectBag, - MethodMetadata? methodMetadata, - TestContextEvents events) - { - if (instance == null) - { - throw new ArgumentNullException(nameof(instance)); - } - - if (objectBag == null) - { - throw new ArgumentNullException(nameof(objectBag)); - } - - if (events == null) - { - throw new ArgumentNullException(nameof(events), "TestContextEvents must not be null. Each test permutation must have a unique TestContextEvents instance for proper disposal tracking."); - } - - if (RequiresPropertyInjection(instance)) - { - await _propertyInjectionService.InjectPropertiesIntoObjectAsync( - instance, - objectBag, - methodMetadata, - events); - } - } - - /// - /// Registers multiple objects (e.g., constructor/method arguments) in parallel. - /// Used during test registration to prepare arguments without executing expensive operations. - /// - public async Task RegisterArgumentsAsync( - object?[] arguments, - ConcurrentDictionary objectBag, - MethodMetadata? methodMetadata, - TestContextEvents events) - { - if (arguments == null || arguments.Length == 0) - { - return; - } - - // Process arguments in parallel for performance - var tasks = new List(); - foreach (var argument in arguments) - { - if (argument != null) - { - tasks.Add(RegisterObjectAsync(argument, objectBag, methodMetadata, events)); - } - } - - await Task.WhenAll(tasks); - } - - /// - /// Determines if an object requires property injection. - /// - #if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Property injection cache handles both AOT and reflection modes appropriately")] - #endif - private bool RequiresPropertyInjection(object instance) - { - return PropertyInjectionCache.HasInjectableProperties(instance.GetType()); - } -} diff --git a/TUnit.Engine/Services/PropertyDataResolver.cs b/TUnit.Engine/Services/PropertyDataResolver.cs deleted file mode 100644 index bd50c6d5fc..0000000000 --- a/TUnit.Engine/Services/PropertyDataResolver.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using TUnit.Core; -using TUnit.Core.Helpers; -using TUnit.Core.Interfaces; -using TUnit.Core.PropertyInjection; -using TUnit.Core.PropertyInjection.Initialization; - -namespace TUnit.Engine.Services; - -/// -/// Handles all data source resolution logic for property initialization. -/// Follows Single Responsibility Principle by focusing only on data resolution. -/// -internal static class PropertyDataResolver -{ - /// - /// Resolves data from a property's data source. - /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Property data resolution uses reflection on property types")] -#endif - public static async Task ResolvePropertyDataAsync(PropertyInitializationContext context, DataSourceInitializer dataSourceInitializer, IObjectRegistry objectRegistry) - { - var dataSource = await GetInitializedDataSourceAsync(context, dataSourceInitializer); - if (dataSource == null) - { - return null; - } - - var dataGeneratorMetadata = CreateDataGeneratorMetadata(context, dataSource); - var dataRows = dataSource.GetDataRowsAsync(dataGeneratorMetadata); - - // Get the first value from the data source - await foreach (var factory in dataRows) - { - var args = await factory(); - var value = ResolveValueFromArgs(context.PropertyType, args); - - // Resolve any Func wrappers - value = await ResolveDelegateValue(value); - - // Initialize the resolved value if needed - if (value != null) - { - // Ensure the value is fully initialized (property injection + IAsyncInitializer) - // DataSourceInitializer handles both data sources and regular objects - if (value is IDataSourceAttribute dataSourceValue) - { - value = await dataSourceInitializer.EnsureInitializedAsync( - dataSourceValue, - context.ObjectBag, - context.MethodMetadata, - context.Events); - } - else if (PropertyInjectionCache.HasInjectableProperties(value.GetType()) || value is IAsyncInitializer) - { - value = await dataSourceInitializer.EnsureInitializedAsync( - value, - context.ObjectBag, - context.MethodMetadata, - context.Events); - } - - return value; - } - } - - return null; - } - - /// - /// Gets an initialized data source from the context. - /// Ensures the data source is fully initialized (including property injection) before returning it. - /// - private static async Task GetInitializedDataSourceAsync(PropertyInitializationContext context, DataSourceInitializer dataSourceInitializer) - { - IDataSourceAttribute? dataSource = null; - - if (context.DataSource != null) - { - dataSource = context.DataSource; - } - else if (context.SourceGeneratedMetadata != null) - { - // Create a new data source instance - dataSource = context.SourceGeneratedMetadata.CreateDataSource(); - } - - if (dataSource == null) - { - return null; - } - - // Ensure the data source is fully initialized before use - // This handles property injection and IAsyncInitializer - return await dataSourceInitializer.EnsureInitializedAsync( - dataSource, - context.ObjectBag, - context.MethodMetadata, - context.Events); - } - - /// - /// Creates data generator metadata for the property. - /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Data generator metadata creation uses reflection on property types")] -#endif - private static DataGeneratorMetadata CreateDataGeneratorMetadata( - PropertyInitializationContext context, - IDataSourceAttribute dataSource) - { - if (context.SourceGeneratedMetadata != null) - { - // Source-generated mode - if (context.SourceGeneratedMetadata.ContainingType == null) - { - throw new InvalidOperationException( - $"ContainingType is null for property '{context.PropertyName}'. " + - $"This may indicate an issue with source generator for type '{context.PropertyType.Name}'."); - } - - var propertyMetadata = new PropertyMetadata - { - IsStatic = false, - Name = context.PropertyName, - ClassMetadata = ClassMetadataHelper.GetOrCreateClassMetadata(context.SourceGeneratedMetadata.ContainingType), - Type = context.PropertyType, - ReflectionInfo = PropertyHelper.GetPropertyInfo(context.SourceGeneratedMetadata.ContainingType, context.PropertyName), - Getter = parent => PropertyHelper.GetPropertyInfo(context.SourceGeneratedMetadata.ContainingType, context.PropertyName).GetValue(parent!)!, - ContainingTypeMetadata = ClassMetadataHelper.GetOrCreateClassMetadata(context.SourceGeneratedMetadata.ContainingType) - }; - - return DataGeneratorMetadataCreator.CreateForPropertyInjection( - propertyMetadata, - context.MethodMetadata, - dataSource, - context.TestContext, - context.TestContext?.Metadata.TestDetails.ClassInstance, - context.Events, - context.ObjectBag); - } - else if (context.PropertyInfo != null) - { - // Reflection mode - return DataGeneratorMetadataCreator.CreateForPropertyInjection( - context.PropertyInfo, - context.PropertyInfo.DeclaringType!, - context.MethodMetadata, - dataSource, - context.TestContext, - context.Instance, - context.Events, - context.ObjectBag); - } - - throw new InvalidOperationException("Cannot create data generator metadata: no property information available"); - } - - /// - /// Resolves value from data source arguments, handling tuples. - /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Value resolution may create tuple types dynamically")] -#endif - private static object? ResolveValueFromArgs( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] - Type propertyType, - object?[]? args) - { - return TupleValueResolver.ResolveTupleValue(propertyType, args); - } - - /// - /// Resolves delegate values by invoking them. - /// - private static async ValueTask ResolveDelegateValue(object? value) - { - return await PropertyValueProcessor.ResolveTestDataValueAsync(typeof(object), value); - } -} diff --git a/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs b/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs deleted file mode 100644 index 7462566749..0000000000 --- a/TUnit.Engine/Services/PropertyInitializationOrchestrator.cs +++ /dev/null @@ -1,212 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using TUnit.Core; -using TUnit.Core.Interfaces.SourceGenerator; -using TUnit.Core.PropertyInjection; -using TUnit.Core.PropertyInjection.Initialization; - -namespace TUnit.Engine.Services; - -/// -/// Orchestrates the entire property initialization process. -/// Coordinates between different components and manages the initialization flow. -/// -internal sealed class PropertyInitializationOrchestrator -{ - internal readonly DataSourceInitializer _dataSourceInitializer; - private readonly IObjectRegistry _objectRegistry; - - public PropertyInitializationOrchestrator(DataSourceInitializer dataSourceInitializer, IObjectRegistry? objectRegistry) - { - _dataSourceInitializer = dataSourceInitializer ?? throw new ArgumentNullException(nameof(dataSourceInitializer)); - _objectRegistry = objectRegistry!; - } - - /// - /// Initializes all properties for an instance using source-generated metadata. - /// Properties are initialized in parallel for better performance. - /// - private async Task InitializeSourceGeneratedPropertiesAsync( - object instance, - PropertyInjectionMetadata[] properties, - ConcurrentDictionary objectBag, - MethodMetadata? methodMetadata, - TestContextEvents events, - ConcurrentDictionary visitedObjects) - { - if (properties.Length == 0) - { - return; - } - - var tasks = properties.Select(metadata => - InitializeSourceGeneratedPropertyAsync(instance, metadata, objectBag, methodMetadata, events, visitedObjects)); - - await Task.WhenAll(tasks); - } - - /// - /// Initializes all properties for an instance using reflection. - /// Properties are initialized in parallel for better performance. - /// -#if NET6_0_OR_GREATER - [RequiresUnreferencedCode("Reflection-based property initialization uses PropertyInfo")] -#endif - private async Task InitializeReflectionPropertiesAsync( - object instance, - (PropertyInfo Property, IDataSourceAttribute DataSource)[] properties, - ConcurrentDictionary objectBag, - MethodMetadata? methodMetadata, - TestContextEvents events, - ConcurrentDictionary visitedObjects) - { - if (properties.Length == 0) - { - return; - } - - var tasks = properties.Select(pair => - InitializeReflectionPropertyAsync(instance, pair.Property, pair.DataSource, objectBag, methodMetadata, events, visitedObjects)); - - await Task.WhenAll(tasks); - } - - /// - /// Initializes a single property using source-generated metadata. - /// - #if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Property data resolver is called for source-generated properties which are AOT-safe")] - #endif - private async Task InitializeSourceGeneratedPropertyAsync( - object instance, - PropertyInjectionMetadata metadata, - ConcurrentDictionary objectBag, - MethodMetadata? methodMetadata, - TestContextEvents events, - ConcurrentDictionary visitedObjects) - { - object? resolvedValue = null; - var testContext = TestContext.Current; - - // Check if property was pre-resolved during registration - if (testContext?.Metadata.TestDetails.TestClassInjectedPropertyArguments.TryGetValue(metadata.PropertyName, out resolvedValue) == true) - { - // Use pre-resolved value - it was already initialized during first resolution - } - else - { - // Resolve the property value from the data source - resolvedValue = await PropertyDataResolver.ResolvePropertyDataAsync( - new PropertyInitializationContext - { - Instance = instance, - SourceGeneratedMetadata = metadata, - PropertyName = metadata.PropertyName, - PropertyType = metadata.PropertyType, - PropertySetter = metadata.SetProperty, - ObjectBag = objectBag, - MethodMetadata = methodMetadata, - Events = events, - VisitedObjects = visitedObjects, - TestContext = testContext, - IsNestedProperty = false - }, - _dataSourceInitializer, - _objectRegistry); - - if (resolvedValue == null) - { - return; - } - } - - // Set the property value - metadata.SetProperty(instance, resolvedValue); - - // Store for potential reuse (using TryAdd for thread-safe concurrent access) - if (testContext != null) - { - ((ConcurrentDictionary)testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments) - .TryAdd(metadata.PropertyName, resolvedValue); - } - } - - /// - /// Initializes a single property using reflection. - /// - #if NET6_0_OR_GREATER - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection-based property initialization is only used in reflection mode, not in AOT")] - #endif - private async Task InitializeReflectionPropertyAsync( - object instance, - PropertyInfo property, - IDataSourceAttribute dataSource, - ConcurrentDictionary objectBag, - MethodMetadata? methodMetadata, - TestContextEvents events, - ConcurrentDictionary visitedObjects) - { - var testContext = TestContext.Current; - var propertySetter = PropertySetterFactory.CreateSetter(property); - - // Resolve the property value from the data source - var resolvedValue = await PropertyDataResolver.ResolvePropertyDataAsync( - new PropertyInitializationContext - { - Instance = instance, - PropertyInfo = property, - DataSource = dataSource, - PropertyName = property.Name, - PropertyType = property.PropertyType, - PropertySetter = propertySetter, - ObjectBag = objectBag, - MethodMetadata = methodMetadata, - Events = events, - VisitedObjects = visitedObjects, - TestContext = testContext, - IsNestedProperty = false - }, - _dataSourceInitializer, - _objectRegistry); - - if (resolvedValue == null) - { - return; - } - - // Set the property value - propertySetter(instance, resolvedValue); - } - - /// - /// Handles the complete initialization flow for an object with properties. - /// - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Specific source gen path")] - public async Task InitializeObjectWithPropertiesAsync( - object instance, - PropertyInjectionPlan plan, - ConcurrentDictionary objectBag, - MethodMetadata? methodMetadata, - TestContextEvents events, - ConcurrentDictionary visitedObjects) - { - if (plan.HasProperties == false) - { - return; - } - - // Initialize properties based on the mode (source-generated or reflection) - if (SourceRegistrar.IsEnabled) - { - await InitializeSourceGeneratedPropertiesAsync( - instance, plan.SourceGeneratedProperties, objectBag, methodMetadata, events, visitedObjects); - } - else - { - await InitializeReflectionPropertiesAsync( - instance, plan.ReflectionProperties, objectBag, methodMetadata, events, visitedObjects); - } - } - -} diff --git a/TUnit.Engine/Services/PropertyInjectionService.cs b/TUnit.Engine/Services/PropertyInjectionService.cs deleted file mode 100644 index 9cdde83a2d..0000000000 --- a/TUnit.Engine/Services/PropertyInjectionService.cs +++ /dev/null @@ -1,204 +0,0 @@ -using System.Collections.Concurrent; -using TUnit.Core; -using TUnit.Core.PropertyInjection; - -namespace TUnit.Engine.Services; - -/// -/// Internal service for property injection. -/// Used by ObjectRegistrationService during registration phase. -/// -internal sealed class PropertyInjectionService -{ - private readonly DataSourceInitializer _dataSourceInitializer; - private PropertyInitializationOrchestrator _orchestrator; - - // Simple object pool for visited objects dictionaries to reduce allocations - private static readonly ConcurrentBag> _visitedObjectsPool = new(); - - public PropertyInjectionService(DataSourceInitializer dataSourceInitializer) - { - _dataSourceInitializer = dataSourceInitializer ?? throw new ArgumentNullException(nameof(dataSourceInitializer)); - _orchestrator = new PropertyInitializationOrchestrator(dataSourceInitializer, null!); - } - - /// - /// Completes initialization by providing the ObjectRegistrationService as IObjectRegistry. - /// This two-phase initialization breaks the circular dependency while maintaining type safety. - /// - public void Initialize(IObjectRegistry objectRegistry) - { - _orchestrator = new PropertyInitializationOrchestrator(_dataSourceInitializer, objectRegistry); - } - - /// - /// Injects properties with data sources into argument objects just before test execution. - /// This ensures properties are only initialized when the test is about to run. - /// Arguments are processed in parallel for better performance. - /// - public async Task InjectPropertiesIntoArgumentsAsync(object?[] arguments, ConcurrentDictionary objectBag, MethodMetadata methodMetadata, TestContextEvents events) - { - if (arguments.Length == 0) - { - return; - } - - var injectableArgs = arguments - .Where(argument => argument != null && PropertyInjectionCache.HasInjectableProperties(argument.GetType())) - .ToArray(); - - if (injectableArgs.Length == 0) - { - return; - } - - var argumentTasks = injectableArgs - .Select(argument => InjectPropertiesIntoObjectAsync(argument!, objectBag, methodMetadata, events)) - .ToArray(); - - await Task.WhenAll(argumentTasks); - } - - - /// - /// Recursively injects properties with data sources into a single object. - /// Uses source generation mode when available, falls back to reflection mode. - /// After injection, handles tracking, initialization, and recursive injection. - /// - /// The object instance to inject properties into. - /// Shared object bag for the test context. Must not be null. - /// Method metadata for the test. Can be null. - /// Test context events for tracking. Must not be null and must be unique per test permutation. - public async Task InjectPropertiesIntoObjectAsync(object instance, ConcurrentDictionary objectBag, MethodMetadata? methodMetadata, TestContextEvents events) - { - if (objectBag == null) - { - throw new ArgumentNullException(nameof(objectBag)); - } - - if (events == null) - { - throw new ArgumentNullException(nameof(events), "TestContextEvents must not be null. Each test permutation must have a unique TestContextEvents instance for proper disposal tracking."); - } - - // Rent dictionary from pool to avoid allocations - if (!_visitedObjectsPool.TryTake(out var visitedObjects)) - { -#if NETSTANDARD2_0 - visitedObjects = new ConcurrentDictionary(); -#else - visitedObjects = new ConcurrentDictionary(ReferenceEqualityComparer.Instance); -#endif - } - - try - { - await InjectPropertiesIntoObjectAsyncCore(instance, objectBag, methodMetadata, events, visitedObjects); - } - finally - { - // Clear and return to pool (reject if too large to avoid memory bloat) - visitedObjects.Clear(); - if (visitedObjects.Count == 0) - { - _visitedObjectsPool.Add(visitedObjects); - } - } - } - - internal async Task InjectPropertiesIntoObjectAsyncCore(object instance, ConcurrentDictionary objectBag, MethodMetadata? methodMetadata, TestContextEvents events, ConcurrentDictionary visitedObjects) - { - if (instance == null) - { - return; - } - - // Prevent cycles - if (!visitedObjects.TryAdd(instance, 0)) - { - return; - } - - try - { - // Use optimized single-lookup pattern with fast-path for already-injected instances - await PropertyInjectionCache.EnsureInjectedAsync(instance, async _ => - { - var plan = PropertyInjectionCache.GetOrCreatePlan(instance.GetType()); - - await _orchestrator.InitializeObjectWithPropertiesAsync( - instance, plan, objectBag, methodMetadata, events, visitedObjects); - }); - - await RecurseIntoNestedPropertiesAsync(instance, objectBag, methodMetadata, events, visitedObjects); - } - catch (Exception ex) - { - var detailedMessage = $"Failed to inject properties for type '{instance.GetType().Name}': {ex.Message}"; - - if (ex.StackTrace != null) - { - detailedMessage += $"\nStack trace: {ex.StackTrace}"; - } - - throw new InvalidOperationException(detailedMessage, ex); - } - } - - /// - /// Recursively injects properties into nested objects that have injectable properties. - /// This is called after the direct properties of an object have been initialized. - /// - private async Task RecurseIntoNestedPropertiesAsync( - object instance, - ConcurrentDictionary objectBag, - MethodMetadata? methodMetadata, - TestContextEvents events, - ConcurrentDictionary visitedObjects) - { - var plan = PropertyInjectionCache.GetOrCreatePlan(instance.GetType()); - if (!plan.HasProperties) - { - return; - } - - if (SourceRegistrar.IsEnabled) - { - foreach (var metadata in plan.SourceGeneratedProperties) - { - var property = metadata.ContainingType.GetProperty(metadata.PropertyName); - if (property == null || !property.CanRead) - { - continue; - } - - var propertyValue = property.GetValue(instance); - if (propertyValue == null) - { - continue; - } - - if (PropertyInjectionCache.HasInjectableProperties(propertyValue.GetType())) - { - await InjectPropertiesIntoObjectAsyncCore(propertyValue, objectBag, methodMetadata, events, visitedObjects); - } - } - } - else - { - foreach (var (property, _) in plan.ReflectionProperties) - { - var propertyValue = property.GetValue(instance); - if (propertyValue == null) - { - continue; - } - - if (PropertyInjectionCache.HasInjectableProperties(propertyValue.GetType())) - { - await InjectPropertiesIntoObjectAsyncCore(propertyValue, objectBag, methodMetadata, events, visitedObjects); - } - } - } - } -} diff --git a/TUnit.Engine/Services/PropertyInjector.cs b/TUnit.Engine/Services/PropertyInjector.cs new file mode 100644 index 0000000000..e0008d17ac --- /dev/null +++ b/TUnit.Engine/Services/PropertyInjector.cs @@ -0,0 +1,616 @@ +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using TUnit.Core; +using TUnit.Core.Helpers; +using TUnit.Core.Interfaces; +using TUnit.Core.Interfaces.SourceGenerator; +using TUnit.Core.PropertyInjection; +using TUnit.Core.PropertyInjection.Initialization; + +namespace TUnit.Engine.Services; + +/// +/// Pure property injection service. +/// Follows Single Responsibility Principle - only injects property values, doesn't initialize objects. +/// Uses Lazy initialization to break circular dependencies without manual Initialize() calls. +/// +/// +/// Depends on rather than a concrete service, +/// enabling testability and following Dependency Inversion Principle. +/// +internal sealed class PropertyInjector +{ + private readonly Lazy _initializationCallback; + private readonly string _testSessionId; + + // Object pool for visited dictionaries to reduce allocations + private static readonly ConcurrentBag> _visitedObjectsPool = new(); + + public PropertyInjector(Lazy initializationCallback, string testSessionId) + { + _initializationCallback = initializationCallback; + _testSessionId = testSessionId; + } + + /// + /// Resolves and caches property values for a test class type WITHOUT setting them on an instance. + /// Used during registration to create shared objects early and enable proper reference counting. + /// + public async Task ResolveAndCachePropertiesAsync( + Type testClassType, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + TestContext testContext) + { + // Skip property resolution if this test is reusing the discovery instance (already initialized) + if (testContext.IsDiscoveryInstanceReused) + { + return; + } + + var plan = PropertyInjectionCache.GetOrCreatePlan(testClassType); + + if (!plan.HasProperties) + { + return; + } + + // Resolve properties based on what's available in the plan + if (plan.SourceGeneratedProperties.Length > 0) + { + await ResolveAndCacheSourceGeneratedPropertiesAsync( + plan.SourceGeneratedProperties, objectBag, methodMetadata, events, testContext); + } + else if (plan.ReflectionProperties.Length > 0) + { + await ResolveAndCacheReflectionPropertiesAsync( + plan.ReflectionProperties, objectBag, methodMetadata, events, testContext); + } + } + + /// + /// Injects properties into an object and recursively into nested objects. + /// + public async Task InjectPropertiesAsync( + object instance, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + if (objectBag == null) + { + throw new ArgumentNullException(nameof(objectBag)); + } + + if (events == null) + { + throw new ArgumentNullException(nameof(events)); + } + + // Rent dictionary from pool + if (!_visitedObjectsPool.TryTake(out var visitedObjects)) + { +#if NETSTANDARD2_0 + visitedObjects = new ConcurrentDictionary(); +#else + visitedObjects = new ConcurrentDictionary(Core.Helpers.ReferenceEqualityComparer.Instance); +#endif + } + + try + { + await InjectPropertiesRecursiveAsync(instance, objectBag, methodMetadata, events, visitedObjects); + } + finally + { + visitedObjects.Clear(); + _visitedObjectsPool.Add(visitedObjects); + } + } + + /// + /// Injects properties into multiple argument objects in parallel. + /// + public async Task InjectPropertiesIntoArgumentsAsync( + object?[] arguments, + ConcurrentDictionary objectBag, + MethodMetadata methodMetadata, + TestContextEvents events) + { + if (arguments.Length == 0) + { + return; + } + + // Build list of injectable args without LINQ + var injectableArgs = new List(arguments.Length); + foreach (var arg in arguments) + { + if (arg != null && PropertyInjectionCache.HasInjectableProperties(arg.GetType())) + { + injectableArgs.Add(arg); + } + } + + if (injectableArgs.Count == 0) + { + return; + } + + // Build task list without LINQ Select + var tasks = new List(injectableArgs.Count); + foreach (var arg in injectableArgs) + { + tasks.Add(InjectPropertiesAsync(arg, objectBag, methodMetadata, events)); + } + + await Task.WhenAll(tasks); + } + + private async Task InjectPropertiesRecursiveAsync( + object instance, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + if (instance == null) + { + return; + } + + // Prevent cycles + if (!visitedObjects.TryAdd(instance, 0)) + { + return; + } + + try + { + var plan = PropertyInjectionCache.GetOrCreatePlan(instance.GetType()); + + if (plan.HasProperties) + { + // Initialize properties based on what's available in the plan + if (plan.SourceGeneratedProperties.Length > 0) + { + await InjectSourceGeneratedPropertiesAsync( + instance, plan.SourceGeneratedProperties, objectBag, methodMetadata, events, visitedObjects); + } + else if (plan.ReflectionProperties.Length > 0) + { + await InjectReflectionPropertiesAsync( + instance, plan.ReflectionProperties, objectBag, methodMetadata, events, visitedObjects); + } + } + + // Recurse into nested properties + await RecurseIntoNestedPropertiesAsync(instance, plan, objectBag, methodMetadata, events, visitedObjects); + } + catch (Exception ex) + { + throw new InvalidOperationException( + $"Failed to inject properties for type '{instance.GetType().Name}': {ex.Message}", ex); + } + } + + private Task InjectSourceGeneratedPropertiesAsync( + object instance, + PropertyInjectionMetadata[] properties, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + return ParallelTaskHelper.ForEachAsync(properties, + prop => InjectSourceGeneratedPropertyAsync(instance, prop, objectBag, methodMetadata, events, visitedObjects)); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Source-gen properties are AOT-safe")] + private async Task InjectSourceGeneratedPropertyAsync( + object instance, + PropertyInjectionMetadata metadata, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + // First check if the property already has a value - skip if it does + // This handles nested objects that were already constructed with their properties set + var property = metadata.ContainingType.GetProperty(metadata.PropertyName); + if (property != null && property.CanRead) + { + var existingValue = property.GetValue(instance); + if (existingValue != null) + { + // Property already has a value, don't overwrite it + return; + } + } + + var testContext = TestContext.Current; + object? resolvedValue = null; + + // Use a composite key to avoid conflicts when nested classes have properties with the same name + var cacheKey = PropertyCacheKeyGenerator.GetCacheKey(metadata); + + // Check if property was pre-resolved during registration + if (testContext?.Metadata.TestDetails.TestClassInjectedPropertyArguments.TryGetValue(cacheKey, out resolvedValue) == true) + { + // Use pre-resolved value + } + else + { + // Resolve the property value from the data source + resolvedValue = await ResolvePropertyDataAsync( + new PropertyInitializationContext + { + Instance = instance, + SourceGeneratedMetadata = metadata, + PropertyName = metadata.PropertyName, + PropertyType = metadata.PropertyType, + PropertySetter = metadata.SetProperty, + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = visitedObjects, + TestContext = testContext, + IsNestedProperty = false + }); + + if (resolvedValue == null) + { + return; + } + } + + // Set the property value + metadata.SetProperty(instance, resolvedValue); + + // Store for potential reuse with composite key + if (testContext != null) + { + ((ConcurrentDictionary)testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments) + .TryAdd(cacheKey, resolvedValue); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT")] + private Task InjectReflectionPropertiesAsync( + object instance, + (PropertyInfo Property, IDataSourceAttribute DataSource)[] properties, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + return ParallelTaskHelper.ForEachAsync(properties, + pair => InjectReflectionPropertyAsync(instance, pair.Property, pair.DataSource, objectBag, methodMetadata, events, visitedObjects)); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT")] + private async Task InjectReflectionPropertyAsync( + object instance, + PropertyInfo property, + IDataSourceAttribute dataSource, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + var testContext = TestContext.Current; + var propertySetter = PropertySetterFactory.CreateSetter(property); + + var resolvedValue = await ResolvePropertyDataAsync( + new PropertyInitializationContext + { + Instance = instance, + PropertyInfo = property, + DataSource = dataSource, + PropertyName = property.Name, + PropertyType = property.PropertyType, + PropertySetter = propertySetter, + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = visitedObjects, + TestContext = testContext, + IsNestedProperty = false + }); + + if (resolvedValue == null) + { + return; + } + + propertySetter(instance, resolvedValue); + } + + private async Task RecurseIntoNestedPropertiesAsync( + object instance, + PropertyInjectionPlan plan, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + ConcurrentDictionary visitedObjects) + { + if (!plan.HasProperties) + { + return; + } + + if (plan.SourceGeneratedProperties.Length > 0) + { + foreach (var metadata in plan.SourceGeneratedProperties) + { + var property = metadata.ContainingType.GetProperty(metadata.PropertyName); + if (property == null || !property.CanRead) + { + continue; + } + + var propertyValue = property.GetValue(instance); + if (propertyValue == null) + { + continue; + } + + if (PropertyInjectionCache.HasInjectableProperties(propertyValue.GetType())) + { + await InjectPropertiesRecursiveAsync(propertyValue, objectBag, methodMetadata, events, visitedObjects); + } + } + } + else if (plan.ReflectionProperties.Length > 0) + { + foreach (var (property, _) in plan.ReflectionProperties) + { + var propertyValue = property.GetValue(instance); + if (propertyValue == null) + { + continue; + } + + if (PropertyInjectionCache.HasInjectableProperties(propertyValue.GetType())) + { + await InjectPropertiesRecursiveAsync(propertyValue, objectBag, methodMetadata, events, visitedObjects); + } + } + } + } + + private Task ResolveAndCacheSourceGeneratedPropertiesAsync( + PropertyInjectionMetadata[] properties, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + TestContext testContext) + { + return ParallelTaskHelper.ForEachAsync(properties, + prop => ResolveAndCacheSourceGeneratedPropertyAsync(prop, objectBag, methodMetadata, events, testContext)); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Source-gen properties are AOT-safe")] + private async Task ResolveAndCacheSourceGeneratedPropertyAsync( + PropertyInjectionMetadata metadata, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + TestContext testContext) + { + var cacheKey = PropertyCacheKeyGenerator.GetCacheKey(metadata); + + // Check if already cached + if (testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments.ContainsKey(cacheKey)) + { + return; + } + + // Resolve the property value from the data source + var resolvedValue = await ResolvePropertyDataAsync( + new PropertyInitializationContext + { + Instance = PlaceholderInstance.Instance, // Use placeholder during registration + SourceGeneratedMetadata = metadata, + PropertyName = metadata.PropertyName, + PropertyType = metadata.PropertyType, + PropertySetter = metadata.SetProperty, + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = new ConcurrentDictionary(), // Empty dictionary for cycle detection + TestContext = testContext, + IsNestedProperty = false + }); + + if (resolvedValue != null) + { + // Cache the resolved value + ((ConcurrentDictionary)testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments) + .TryAdd(cacheKey, resolvedValue); + } + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT")] + private Task ResolveAndCacheReflectionPropertiesAsync( + (PropertyInfo Property, IDataSourceAttribute DataSource)[] properties, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + TestContext testContext) + { + return ParallelTaskHelper.ForEachAsync(properties, + pair => ResolveAndCacheReflectionPropertyAsync(pair.Property, pair.DataSource, objectBag, methodMetadata, events, testContext)); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Reflection mode is not used in AOT")] + private async Task ResolveAndCacheReflectionPropertyAsync( + PropertyInfo property, + IDataSourceAttribute dataSource, + ConcurrentDictionary objectBag, + MethodMetadata? methodMetadata, + TestContextEvents events, + TestContext testContext) + { + var cacheKey = PropertyCacheKeyGenerator.GetCacheKey(property); + + // Check if already cached + if (testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments.ContainsKey(cacheKey)) + { + return; + } + + var propertySetter = PropertySetterFactory.CreateSetter(property); + + var resolvedValue = await ResolvePropertyDataAsync( + new PropertyInitializationContext + { + Instance = PlaceholderInstance.Instance, // Use placeholder during registration + PropertyInfo = property, + DataSource = dataSource, + PropertyName = property.Name, + PropertyType = property.PropertyType, + PropertySetter = propertySetter, + ObjectBag = objectBag, + MethodMetadata = methodMetadata, + Events = events, + VisitedObjects = new ConcurrentDictionary(), // Empty dictionary for cycle detection + TestContext = testContext, + IsNestedProperty = false + }); + + if (resolvedValue != null) + { + // Cache the resolved value + ((ConcurrentDictionary)testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments) + .TryAdd(cacheKey, resolvedValue); + } + } + + /// + /// Resolves data from a property's data source. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Property data resolution handles both modes")] + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "PropertyType is properly preserved through source generation")] + private async Task ResolvePropertyDataAsync(PropertyInitializationContext context) + { + var dataSource = await GetInitializedDataSourceAsync(context); + if (dataSource == null) + { + return null; + } + + var dataGeneratorMetadata = CreateDataGeneratorMetadata(context, dataSource); + var dataRows = dataSource.GetDataRowsAsync(dataGeneratorMetadata); + + await foreach (var factory in dataRows) + { + var args = await factory(); + var value = TupleValueResolver.ResolveTupleValue(context.PropertyType, args); + + // Resolve any Func wrappers + value = await PropertyValueProcessor.ResolveTestDataValueAsync(typeof(object), value); + + if (value != null) + { + // EnsureInitializedAsync handles property injection and initialization. + // ObjectInitializer is phase-aware: during Discovery phase, only IAsyncDiscoveryInitializer + // objects are initialized; regular IAsyncInitializer objects are deferred to Execution phase. + await _initializationCallback.Value.EnsureInitializedAsync( + value, + context.ObjectBag, + context.MethodMetadata, + context.Events); + + return value; + } + } + + return null; + } + + private async Task GetInitializedDataSourceAsync(PropertyInitializationContext context) + { + IDataSourceAttribute? dataSource = null; + + if (context.DataSource != null) + { + dataSource = context.DataSource; + } + else if (context.SourceGeneratedMetadata != null) + { + dataSource = context.SourceGeneratedMetadata.CreateDataSource(); + } + + if (dataSource == null) + { + return null; + } + + // Ensure the data source is initialized + return await _initializationCallback.Value.EnsureInitializedAsync( + dataSource, + context.ObjectBag, + context.MethodMetadata, + context.Events); + } + + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Metadata creation handles both modes")] + [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "ContainingType and PropertyType are preserved through source generation")] + private DataGeneratorMetadata CreateDataGeneratorMetadata( + PropertyInitializationContext context, + IDataSourceAttribute dataSource) + { + if (context.SourceGeneratedMetadata != null) + { + if (context.SourceGeneratedMetadata.ContainingType == null) + { + throw new InvalidOperationException( + $"ContainingType is null for property '{context.PropertyName}'."); + } + + var propertyMetadata = new PropertyMetadata + { + IsStatic = false, + Name = context.PropertyName, + ClassMetadata = ClassMetadataHelper.GetOrCreateClassMetadata(context.SourceGeneratedMetadata.ContainingType), + Type = context.PropertyType, + ReflectionInfo = PropertyHelper.GetPropertyInfo(context.SourceGeneratedMetadata.ContainingType, context.PropertyName), + Getter = parent => PropertyHelper.GetPropertyInfo(context.SourceGeneratedMetadata.ContainingType, context.PropertyName).GetValue(parent!)!, + ContainingTypeMetadata = ClassMetadataHelper.GetOrCreateClassMetadata(context.SourceGeneratedMetadata.ContainingType) + }; + + return DataGeneratorMetadataCreator.CreateForPropertyInjection( + propertyMetadata, + context.MethodMetadata, + dataSource, + _testSessionId, + context.TestContext, + context.TestContext?.Metadata.TestDetails.ClassInstance, + context.Events, + context.ObjectBag); + } + else if (context.PropertyInfo != null) + { + return DataGeneratorMetadataCreator.CreateForPropertyInjection( + context.PropertyInfo, + context.PropertyInfo.DeclaringType!, + context.MethodMetadata, + dataSource, + _testSessionId, + context.TestContext, + context.Instance, + context.Events, + context.ObjectBag); + } + + throw new InvalidOperationException("Cannot create data generator metadata: no property information available"); + } +} diff --git a/TUnit.Engine/Services/TestArgumentRegistrationService.cs b/TUnit.Engine/Services/TestArgumentRegistrationService.cs index dd9c4e10c5..407df90e4e 100644 --- a/TUnit.Engine/Services/TestArgumentRegistrationService.cs +++ b/TUnit.Engine/Services/TestArgumentRegistrationService.cs @@ -1,160 +1,48 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using TUnit.Core; -using TUnit.Core.Data; -using TUnit.Core.Enums; -using TUnit.Core.Interfaces; -using TUnit.Core.Interfaces.SourceGenerator; -using TUnit.Core.PropertyInjection; -using TUnit.Core.Tracking; namespace TUnit.Engine.Services; /// -/// Service that handles registration of test arguments (constructor args, method args) during test discovery. -/// Implements ITestRegisteredEventReceiver to register objects when tests are registered. -/// Renamed from TestArgumentTrackingService to clarify it's for the registration phase. +/// Internal service that handles registration of test arguments during test discovery. +/// Not a user-extensibility point - called directly by TestBuilder. +/// Simplified to use ObjectLifecycleService for all object registration. /// -internal sealed class TestArgumentRegistrationService : ITestRegisteredEventReceiver +internal sealed class TestArgumentRegistrationService { - private readonly ObjectRegistrationService _objectRegistrationService; - private readonly ObjectTracker _objectTracker; + private readonly ObjectLifecycleService _objectLifecycleService; - public TestArgumentRegistrationService(ObjectRegistrationService objectRegistrationService, ObjectTracker objectTracker) + public TestArgumentRegistrationService(ObjectLifecycleService objectLifecycleService) { - _objectRegistrationService = objectRegistrationService; - _objectTracker = objectTracker; + _objectLifecycleService = objectLifecycleService; } - public int Order => int.MinValue; // Run first to ensure registration happens before other event receivers - /// - /// Called when a test is registered. This is the correct time to register constructor and method arguments + /// Called when a test is registered. Registers constructor and method arguments /// for proper reference counting and disposal tracking. + /// Property values are resolved lazily during test execution (not during discovery). /// - public async ValueTask OnTestRegistered(TestRegisteredContext context) + public async ValueTask RegisterTestArgumentsAsync(TestContext testContext) { - var testContext = context.TestContext; + TestContext.Current = testContext; + var classArguments = testContext.Metadata.TestDetails.TestClassArguments; var methodArguments = testContext.Metadata.TestDetails.TestMethodArguments; - // Register class arguments (registration phase - property injection + tracking, NO IAsyncInitializer) - await _objectRegistrationService.RegisterArgumentsAsync( + // Register class arguments (property injection during registration) + await _objectLifecycleService.RegisterArgumentsAsync( classArguments, testContext.StateBag.Items, testContext.Metadata.TestDetails.MethodMetadata, testContext.InternalEvents); - // Register method arguments (registration phase) - await _objectRegistrationService.RegisterArgumentsAsync( + // Register method arguments + await _objectLifecycleService.RegisterArgumentsAsync( methodArguments, testContext.StateBag.Items, testContext.Metadata.TestDetails.MethodMetadata, testContext.InternalEvents); - // Register properties that will be injected into the test class - await RegisterPropertiesAsync(testContext); - - _objectTracker.TrackObjects(testContext); - } - - /// - /// Registers properties that will be injected into the test class instance. - /// This ensures proper reference counting for all property-injected instances during discovery. - /// Exceptions during data generation will be caught and associated with the test for reporting. - /// - private async ValueTask RegisterPropertiesAsync(TestContext testContext) - { - try - { - var classType = testContext.Metadata.TestDetails.ClassType; - - // Get the property source for the class - var propertySource = PropertySourceRegistry.GetSource(classType); - if (propertySource?.ShouldInitialize != true) - { - // No properties to inject for this class - return; - } - - // Get all properties that need injection - var propertyMetadata = propertySource.GetPropertyMetadata(); - - foreach (var metadata in propertyMetadata) - { - try - { - // Create the data source for this property - var dataSource = metadata.CreateDataSource(); - - // Create minimal DataGeneratorMetadata for property resolution during registration - var testBuilderContext = new TestBuilderContext - { - TestMetadata = testContext.Metadata.TestDetails.MethodMetadata, - DataSourceAttribute = dataSource, - Events = testContext.InternalEvents, - StateBag = testContext.StateBag.Items - }; - - var dataGenMetadata = new DataGeneratorMetadata - { - TestBuilderContext = new TestBuilderContextAccessor(testBuilderContext), - MembersToGenerate = [], // Properties don't use member generation - TestInformation = testContext.Metadata.TestDetails.MethodMetadata, - Type = DataGeneratorType.Property, - TestSessionId = TestSessionContext.Current?.Id ?? "registration", - TestClassInstance = null, // Not available during registration - ClassInstanceArguments = testContext.Metadata.TestDetails.TestClassArguments - }; - - // Get the data rows from the data source - var dataRows = dataSource.GetDataRowsAsync(dataGenMetadata); - - // Get the first data row (properties get single values, not multiple) - await foreach (var dataRowFunc in dataRows) - { - var dataRow = await dataRowFunc(); - if (dataRow is { Length: > 0 }) - { - var data = dataRow[0]; - - if (data != null) - { - // Store for later injection - testContext.Metadata.TestDetails.TestClassInjectedPropertyArguments[metadata.PropertyName] = data; - - // Register the ClassDataSource instance during registration phase - // This does: property injection + tracking (NO IAsyncInitializer - deferred to execution) - await _objectRegistrationService.RegisterObjectAsync( - data, - testContext.StateBag.Items, - testContext.Metadata.TestDetails.MethodMetadata, - testContext.InternalEvents); - } - } - break; // Only take the first result for property injection - } - } - catch (Exception ex) - { - // Capture the exception for this property and re-throw - // The test building process will handle marking it as failed - var exceptionMessage = $"Failed to generate data for property '{metadata.PropertyName}': {ex.Message}"; - var propertyException = new InvalidOperationException(exceptionMessage, ex); - throw propertyException; - } - } - } - catch (Exception ex) - { - // Capture any top-level exceptions (e.g., getting property source) and re-throw - // The test building process will handle marking it as failed - var exceptionMessage = $"Failed to register properties for test '{testContext.Metadata.TestDetails.TestName}': {ex.Message}"; - var registrationException = new InvalidOperationException(exceptionMessage, ex); - throw registrationException; - } + // Register the test for tracking (inject properties and track objects for disposal) + await _objectLifecycleService.RegisterTestAsync(testContext); } } diff --git a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs index c34181a332..683a9e8f4c 100644 --- a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs +++ b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs @@ -90,7 +90,7 @@ private async ValueTask ExecuteTestInternalAsync(AbstractExecutableTest test, Ca } // Ensure TestSession hooks run before creating test instances - await _testExecutor.EnsureTestSessionHooksExecutedAsync().ConfigureAwait(false); + await _testExecutor.EnsureTestSessionHooksExecutedAsync(cancellationToken).ConfigureAwait(false); // Execute test with retry logic - each retry gets a fresh instance // Timeout is applied per retry attempt, not across all retries @@ -126,9 +126,9 @@ await TimeoutHelper.ExecuteWithTimeoutAsync( try { - await _testInitializer.InitializeTest(test, ct).ConfigureAwait(false); + _testInitializer.PrepareTest(test, ct); test.Context.RestoreExecutionContext(); - await _testExecutor.ExecuteAsync(test, ct).ConfigureAwait(false); + await _testExecutor.ExecuteAsync(test, _testInitializer, ct).ConfigureAwait(false); } finally { diff --git a/TUnit.Engine/Services/TestFilterService.cs b/TUnit.Engine/Services/TestFilterService.cs index ee091095fd..0c0285ba62 100644 --- a/TUnit.Engine/Services/TestFilterService.cs +++ b/TUnit.Engine/Services/TestFilterService.cs @@ -74,7 +74,7 @@ private async Task RegisterTest(AbstractExecutableTest test) try { - await testArgumentRegistrationService.OnTestRegistered(registeredContext); + await testArgumentRegistrationService.RegisterTestArgumentsAsync(test.Context); } catch (Exception ex) { diff --git a/TUnit.Engine/Services/TestRegistry.cs b/TUnit.Engine/Services/TestRegistry.cs index a1538e5e98..cc76b93a1d 100644 --- a/TUnit.Engine/Services/TestRegistry.cs +++ b/TUnit.Engine/Services/TestRegistry.cs @@ -256,7 +256,7 @@ private async Task CreateMetadataFromDynamicDiscoveryResult(Dynami var testName = methodInfo.Name; - return await Task.FromResult(new RuntimeDynamicTestMetadata(result.TestClassType, methodInfo, result) + return await Task.FromResult(new DynamicTestMetadata(result) { TestName = testName, TestClassType = result.TestClassType, @@ -340,81 +340,6 @@ private sealed class PendingDynamicTest } - private sealed class RuntimeDynamicTestMetadata : TestMetadata, IDynamicTestMetadata - { - private readonly DynamicDiscoveryResult _dynamicResult; - private readonly Type _testClass; - private readonly MethodInfo _testMethod; - - public RuntimeDynamicTestMetadata(Type testClass, MethodInfo testMethod, DynamicDiscoveryResult dynamicResult) - { - _testClass = testClass; - _testMethod = testMethod; - _dynamicResult = dynamicResult; - } - - public override Func CreateExecutableTestFactory - { - get => (context, metadata) => - { - var modifiedContext = new ExecutableTestCreationContext - { - TestId = context.TestId, - DisplayName = _dynamicResult.DisplayName ?? context.DisplayName, - Arguments = _dynamicResult.TestMethodArguments ?? context.Arguments, - ClassArguments = _dynamicResult.TestClassArguments ?? context.ClassArguments, - Context = context.Context - }; - - if (_dynamicResult.ParentTestId != null) - { - modifiedContext.Context.ParentTestId = _dynamicResult.ParentTestId; - } - - if (_dynamicResult.Relationship.HasValue) - { - modifiedContext.Context.Relationship = _dynamicResult.Relationship.Value; - } - - if (_dynamicResult.Properties != null) - { - foreach (var kvp in _dynamicResult.Properties) - { - modifiedContext.Context.StateBag.Items[kvp.Key] = kvp.Value; - } - } - - var createInstance = (TestContext testContext) => - { - var instance = metadata.InstanceFactory(Type.EmptyTypes, modifiedContext.ClassArguments); - - foreach (var propertyInjection in metadata.PropertyInjections) - { - var value = propertyInjection.ValueFactory(); - propertyInjection.Setter(instance, value); - } - - return Task.FromResult(instance); - }; - - var invokeTest = metadata.TestInvoker ?? throw new InvalidOperationException("Test invoker is null"); - - return new ExecutableTest(createInstance, - async (instance, args, context, ct) => - { - await invokeTest(instance, args); - }) - { - TestId = modifiedContext.TestId, - Metadata = metadata, - Arguments = modifiedContext.Arguments, - ClassArguments = modifiedContext.ClassArguments, - Context = modifiedContext.Context - }; - }; - } - } - /// /// Optimized method to get dependencies without LINQ allocations /// diff --git a/TUnit.Engine/Services/VerbosityService.cs b/TUnit.Engine/Services/VerbosityService.cs index 18c36aacc2..f297c026bd 100644 --- a/TUnit.Engine/Services/VerbosityService.cs +++ b/TUnit.Engine/Services/VerbosityService.cs @@ -1,7 +1,6 @@ using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Services; using TUnit.Engine.CommandLineProviders; -using TUnit.Engine.Helpers; using LogLevel = TUnit.Core.Logging.LogLevel; #pragma warning disable TPEXP @@ -42,8 +41,6 @@ public string CreateVerbositySummary() $"(Stack traces: {ShowDetailedStackTrace}, "; } - // Use centralized environment variable cache - private static bool GetOutputLevel(ICommandLineOptions commandLineOptions, IServiceProvider serviceProvider) { // Check for --output flag (Microsoft.Testing.Platform extension) @@ -64,8 +61,8 @@ private static LogLevel GetLogLevel(ICommandLineOptions commandLineOptions) return LogLevelCommandProvider.ParseLogLevel(args); } - // Check cached legacy environment variable for backwards compatibility - if (EnvironmentVariableCache.Get("TUNIT_DISCOVERY_DIAGNOSTICS") == "1") + // Check legacy environment variable for backwards compatibility + if (Environment.GetEnvironmentVariable("TUNIT_DISCOVERY_DIAGNOSTICS") == "1") { return LogLevel.Debug; } diff --git a/TUnit.Engine/TestExecutor.cs b/TUnit.Engine/TestExecutor.cs index 18a3a21430..dbb80cc3f6 100644 --- a/TUnit.Engine/TestExecutor.cs +++ b/TUnit.Engine/TestExecutor.cs @@ -19,6 +19,7 @@ internal class TestExecutor private readonly HookExecutor _hookExecutor; private readonly TestLifecycleCoordinator _lifecycleCoordinator; private readonly BeforeHookTaskCache _beforeHookTaskCache; + private readonly AfterHookPairTracker _afterHookPairTracker; private readonly IContextProvider _contextProvider; private readonly EventReceiverOrchestrator _eventReceiverOrchestrator; @@ -26,12 +27,14 @@ public TestExecutor( HookExecutor hookExecutor, TestLifecycleCoordinator lifecycleCoordinator, BeforeHookTaskCache beforeHookTaskCache, + AfterHookPairTracker afterHookPairTracker, IContextProvider contextProvider, EventReceiverOrchestrator eventReceiverOrchestrator) { _hookExecutor = hookExecutor; _lifecycleCoordinator = lifecycleCoordinator; _beforeHookTaskCache = beforeHookTaskCache; + _afterHookPairTracker = afterHookPairTracker; _contextProvider = contextProvider; _eventReceiverOrchestrator = eventReceiverOrchestrator; } @@ -40,19 +43,26 @@ public TestExecutor( /// /// Ensures that Before(TestSession) hooks have been executed. /// This is called before creating test instances to ensure resources are available. + /// Registers the corresponding After(TestSession) hook to run on cancellation. /// - public async Task EnsureTestSessionHooksExecutedAsync() + public async Task EnsureTestSessionHooksExecutedAsync(CancellationToken cancellationToken) { // Get or create and cache Before hooks - these run only once - await _beforeHookTaskCache.GetOrCreateBeforeTestSessionTask(() => - _hookExecutor.ExecuteBeforeTestSessionHooksAsync(CancellationToken.None)).ConfigureAwait(false); + await _beforeHookTaskCache.GetOrCreateBeforeTestSessionTask( + ct => _hookExecutor.ExecuteBeforeTestSessionHooksAsync(ct), + cancellationToken).ConfigureAwait(false); + + // Register After Session hook to run on cancellation (guarantees cleanup) + _afterHookPairTracker.RegisterAfterTestSessionHook( + cancellationToken, + () => new ValueTask>(_hookExecutor.ExecuteAfterTestSessionHooksAsync(CancellationToken.None).AsTask())); } /// /// Creates a test executor delegate that wraps the provided executor with hook orchestration. /// Uses focused services that follow SRP to manage lifecycle and execution. /// - public async ValueTask ExecuteAsync(AbstractExecutableTest executableTest, CancellationToken cancellationToken) + public async ValueTask ExecuteAsync(AbstractExecutableTest executableTest, TestInitializer testInitializer, CancellationToken cancellationToken) { var testClass = executableTest.Metadata.TestClassType; @@ -63,7 +73,7 @@ public async ValueTask ExecuteAsync(AbstractExecutableTest executableTest, Cance try { - await EnsureTestSessionHooksExecutedAsync().ConfigureAwait(false); + await EnsureTestSessionHooksExecutedAsync(cancellationToken).ConfigureAwait(false); await _eventReceiverOrchestrator.InvokeFirstTestInSessionEventReceiversAsync( executableTest.Context, @@ -72,8 +82,16 @@ await _eventReceiverOrchestrator.InvokeFirstTestInSessionEventReceiversAsync( executableTest.Context.ClassContext.AssemblyContext.TestSessionContext.RestoreExecutionContext(); - await _beforeHookTaskCache.GetOrCreateBeforeAssemblyTask(testAssembly, assembly => _hookExecutor.ExecuteBeforeAssemblyHooksAsync(assembly, CancellationToken.None)) - .ConfigureAwait(false); + await _beforeHookTaskCache.GetOrCreateBeforeAssemblyTask( + testAssembly, + (assembly, ct) => _hookExecutor.ExecuteBeforeAssemblyHooksAsync(assembly, ct), + cancellationToken).ConfigureAwait(false); + + // Register After Assembly hook to run on cancellation (guarantees cleanup) + _afterHookPairTracker.RegisterAfterAssemblyHook( + testAssembly, + cancellationToken, + (assembly) => new ValueTask>(_hookExecutor.ExecuteAfterAssemblyHooksAsync(assembly, CancellationToken.None).AsTask())); await _eventReceiverOrchestrator.InvokeFirstTestInAssemblyEventReceiversAsync( executableTest.Context, @@ -82,8 +100,10 @@ await _eventReceiverOrchestrator.InvokeFirstTestInAssemblyEventReceiversAsync( executableTest.Context.ClassContext.AssemblyContext.RestoreExecutionContext(); - await _beforeHookTaskCache.GetOrCreateBeforeClassTask(testClass, _ => _hookExecutor.ExecuteBeforeClassHooksAsync(testClass, CancellationToken.None)) - .ConfigureAwait(false); + await _beforeHookTaskCache.GetOrCreateBeforeClassTask(testClass, _hookExecutor, cancellationToken).ConfigureAwait(false); + + // Register After Class hook to run on cancellation (guarantees cleanup) + _afterHookPairTracker.RegisterAfterClassHook(testClass, _hookExecutor, cancellationToken); await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync( executableTest.Context, @@ -92,6 +112,12 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync( executableTest.Context.ClassContext.RestoreExecutionContext(); + // Initialize test objects (IAsyncInitializer) AFTER BeforeClass hooks + // This ensures resources like Docker containers are not started until needed + await testInitializer.InitializeTestObjectsAsync(executableTest, cancellationToken).ConfigureAwait(false); + + executableTest.Context.RestoreExecutionContext(); + // Early stage test start receivers run before instance-level hooks await _eventReceiverOrchestrator.InvokeTestStartEventReceiversAsync(executableTest.Context, cancellationToken, EventReceiverStage.Early).ConfigureAwait(false); @@ -121,13 +147,16 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync( } finally { + // After hooks must use CancellationToken.None to ensure cleanup runs even when cancelled + // This matches the pattern used for After Class/Assembly hooks in TestCoordinator + // Early stage test end receivers run before instance-level hooks - var earlyStageExceptions = await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, cancellationToken, EventReceiverStage.Early).ConfigureAwait(false); + var earlyStageExceptions = await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, CancellationToken.None, EventReceiverStage.Early).ConfigureAwait(false); - var hookExceptions = await _hookExecutor.ExecuteAfterTestHooksAsync(executableTest, cancellationToken).ConfigureAwait(false); + var hookExceptions = await _hookExecutor.ExecuteAfterTestHooksAsync(executableTest, CancellationToken.None).ConfigureAwait(false); // Late stage test end receivers run after instance-level hooks (default behavior) - var lateStageExceptions = await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, cancellationToken, EventReceiverStage.Late).ConfigureAwait(false); + var lateStageExceptions = await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, CancellationToken.None, EventReceiverStage.Late).ConfigureAwait(false); // Combine all exceptions from event receivers var eventReceiverExceptions = new List(earlyStageExceptions.Count + lateStageExceptions.Count); @@ -201,13 +230,17 @@ internal async Task> ExecuteAfterClassAssemblyHooks(AbstractExec if (flags.ShouldExecuteAfterClass) { - var classExceptions = await _hookExecutor.ExecuteAfterClassHooksAsync(testClass, cancellationToken).ConfigureAwait(false); + // Use AfterHookPairTracker to prevent double execution if already triggered by cancellation + var classExceptions = await _afterHookPairTracker.GetOrCreateAfterClassTask(testClass, _hookExecutor, cancellationToken).ConfigureAwait(false); exceptions.AddRange(classExceptions); } if (flags.ShouldExecuteAfterAssembly) { - var assemblyExceptions = await _hookExecutor.ExecuteAfterAssemblyHooksAsync(testAssembly, cancellationToken).ConfigureAwait(false); + // Use AfterHookPairTracker to prevent double execution if already triggered by cancellation + var assemblyExceptions = await _afterHookPairTracker.GetOrCreateAfterAssemblyTask( + testAssembly, + (assembly) => new ValueTask>(_hookExecutor.ExecuteAfterAssemblyHooksAsync(assembly, cancellationToken).AsTask())).ConfigureAwait(false); exceptions.AddRange(assemblyExceptions); } @@ -217,10 +250,15 @@ internal async Task> ExecuteAfterClassAssemblyHooks(AbstractExec /// /// Execute session-level after hooks once at the end of test execution. /// Returns any exceptions that occurred during hook execution. + /// Uses AfterHookPairTracker to prevent double execution if already triggered by cancellation. /// public async Task> ExecuteAfterTestSessionHooksAsync(CancellationToken cancellationToken) { - return await _hookExecutor.ExecuteAfterTestSessionHooksAsync(cancellationToken).ConfigureAwait(false); + // Use AfterHookPairTracker to prevent double execution if already triggered by cancellation + var exceptions = await _afterHookPairTracker.GetOrCreateAfterTestSessionTask( + () => new ValueTask>(_hookExecutor.ExecuteAfterTestSessionHooksAsync(cancellationToken).AsTask())).ConfigureAwait(false); + + return exceptions; } /// diff --git a/TUnit.Engine/TestInitializer.cs b/TUnit.Engine/TestInitializer.cs index 2c8e0c7156..73fbb7fbc2 100644 --- a/TUnit.Engine/TestInitializer.cs +++ b/TUnit.Engine/TestInitializer.cs @@ -1,56 +1,38 @@ -using System.Collections.Concurrent; using TUnit.Core; -using TUnit.Core.Tracking; -using TUnit.Engine.Extensions; using TUnit.Engine.Services; namespace TUnit.Engine; +/// +/// Simplified test initializer that delegates to ObjectLifecycleService. +/// Follows Single Responsibility Principle - only coordinates test initialization. +/// internal class TestInitializer { private readonly EventReceiverOrchestrator _eventReceiverOrchestrator; - private readonly PropertyInjectionService _propertyInjectionService; - private readonly ObjectTracker _objectTracker; + private readonly ObjectLifecycleService _objectLifecycleService; - public TestInitializer(EventReceiverOrchestrator eventReceiverOrchestrator, - PropertyInjectionService propertyInjectionService, - ObjectTracker objectTracker) + public TestInitializer( + EventReceiverOrchestrator eventReceiverOrchestrator, + ObjectLifecycleService objectLifecycleService) { _eventReceiverOrchestrator = eventReceiverOrchestrator; - _propertyInjectionService = propertyInjectionService; - _objectTracker = objectTracker; + _objectLifecycleService = objectLifecycleService; } - public async ValueTask InitializeTest(AbstractExecutableTest test, CancellationToken cancellationToken) + public void PrepareTest(AbstractExecutableTest test, CancellationToken cancellationToken) { - var testClassInstance = test.Context.Metadata.TestDetails.ClassInstance; - - await _propertyInjectionService.InjectPropertiesIntoObjectAsync( - testClassInstance, - test.Context.StateBag.Items, - test.Context.Metadata.TestDetails.MethodMetadata, - test.Context.InternalEvents); - + // Register event receivers _eventReceiverOrchestrator.RegisterReceivers(test.Context, cancellationToken); - // Shouldn't retrack already tracked objects, but will track any new ones created during retries / initialization - _objectTracker.TrackObjects(test.Context); - - await InitializeTrackedObjects(test.Context, cancellationToken); + // Prepare test: set cached property values on the instance + // Does NOT call IAsyncInitializer - that is deferred until after BeforeClass hooks + _objectLifecycleService.PrepareTest(test.Context); } - private async Task InitializeTrackedObjects(TestContext testContext, CancellationToken cancellationToken) + public async ValueTask InitializeTestObjectsAsync(AbstractExecutableTest test, CancellationToken cancellationToken) { - // Initialize by level (deepest first), with objects at the same level in parallel - var levels = testContext.TrackedObjects.Keys.OrderByDescending(level => level); - - foreach (var level in levels) - { - var objectsAtLevel = testContext.TrackedObjects[level]; - await Task.WhenAll(objectsAtLevel.Select(obj => ObjectInitializer.InitializeAsync(obj, cancellationToken).AsTask())); - } - - // Finally, ensure the test class itself is initialized - await ObjectInitializer.InitializeAsync(testContext.Metadata.TestDetails.ClassInstance, cancellationToken); + // Initialize test objects (IAsyncInitializer) - called after BeforeClass hooks + await _objectLifecycleService.InitializeTestObjectsAsync(test.Context, cancellationToken); } } diff --git a/TUnit.Engine/Xml/JUnitXmlWriter.cs b/TUnit.Engine/Xml/JUnitXmlWriter.cs new file mode 100644 index 0000000000..1c9156d241 --- /dev/null +++ b/TUnit.Engine/Xml/JUnitXmlWriter.cs @@ -0,0 +1,357 @@ +using System.Globalization; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text; +using System.Xml; +using Microsoft.Testing.Platform.Extensions.Messages; + +namespace TUnit.Engine.Xml; + +internal static class JUnitXmlWriter +{ + public static string GenerateXml( + IEnumerable testUpdates, + string? filter) + { + // Get the last state for each test + var lastStates = GetLastStates(testUpdates); + + if (lastStates.Count == 0) + { + return string.Empty; + } + + // Calculate summary statistics + var summary = CalculateSummary(lastStates.Values); + + // Get assembly and framework information + var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults"; + var targetFramework = Assembly.GetExecutingAssembly() + .GetCustomAttributes() + .SingleOrDefault() + ?.FrameworkDisplayName + ?? RuntimeInformation.FrameworkDescription; + + using var stringWriter = new Utf8StringWriter(); + var settings = new XmlWriterSettings + { + Indent = true, + Encoding = Encoding.UTF8, + OmitXmlDeclaration = false, + NewLineOnAttributes = false + }; + + using var xmlWriter = XmlWriter.Create(stringWriter, settings); + + // Write XML structure + xmlWriter.WriteStartDocument(); + + // + xmlWriter.WriteStartElement("testsuites"); + xmlWriter.WriteAttributeString("name", assemblyName); + xmlWriter.WriteAttributeString("tests", summary.Total.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteAttributeString("failures", summary.Failures.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteAttributeString("errors", summary.Errors.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteAttributeString("skipped", summary.Skipped.ToString(CultureInfo.InvariantCulture)); + xmlWriter.WriteAttributeString("time", summary.TotalTime.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture)); + xmlWriter.WriteAttributeString("timestamp", summary.Timestamp.ToString("o", CultureInfo.InvariantCulture)); + + // Write test suite + WriteTestSuite(xmlWriter, lastStates.Values, assemblyName, targetFramework, summary, filter); + + xmlWriter.WriteEndElement(); // testsuites + xmlWriter.WriteEndDocument(); + xmlWriter.Flush(); + return stringWriter.ToString(); + } + + private static void WriteTestSuite( + XmlWriter writer, + IEnumerable tests, + string assemblyName, + string targetFramework, + TestSummary summary, + string? filter) + { + writer.WriteStartElement("testsuite"); + writer.WriteAttributeString("name", assemblyName); + writer.WriteAttributeString("tests", summary.Total.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("failures", summary.Failures.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("errors", summary.Errors.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("skipped", summary.Skipped.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString("time", summary.TotalTime.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture)); + writer.WriteAttributeString("timestamp", summary.Timestamp.ToString("o", CultureInfo.InvariantCulture)); + writer.WriteAttributeString("hostname", Environment.MachineName); + + // Write properties + WriteProperties(writer, targetFramework, filter); + + // Write test cases + foreach (var test in tests) + { + WriteTestCase(writer, test); + } + + writer.WriteEndElement(); // testsuite + } + + private static void WriteProperties(XmlWriter writer, string targetFramework, string? filter) + { + writer.WriteStartElement("properties"); + + writer.WriteStartElement("property"); + writer.WriteAttributeString("name", "framework"); + writer.WriteAttributeString("value", targetFramework); + writer.WriteEndElement(); + + writer.WriteStartElement("property"); + writer.WriteAttributeString("name", "platform"); + writer.WriteAttributeString("value", RuntimeInformation.OSDescription); + writer.WriteEndElement(); + + writer.WriteStartElement("property"); + writer.WriteAttributeString("name", "runtime"); + writer.WriteAttributeString("value", RuntimeInformation.FrameworkDescription); + writer.WriteEndElement(); + + if (!string.IsNullOrEmpty(filter)) + { + writer.WriteStartElement("property"); + writer.WriteAttributeString("name", "filter"); + writer.WriteAttributeString("value", filter); + writer.WriteEndElement(); + } + + writer.WriteEndElement(); // properties + } + + private static void WriteTestCase(XmlWriter writer, TestNodeUpdateMessage test) + { + var testNode = test.TestNode; + + // Get test state + var stateProperty = testNode.Properties.AsEnumerable() + .OfType() + .FirstOrDefault(); + + // Get timing + var timingProperty = testNode.Properties.AsEnumerable() + .OfType() + .FirstOrDefault(); + + var duration = timingProperty?.GlobalTiming.Duration is { } d ? d : TimeSpan.Zero; + + // Get class and method names + var testMethodIdentifier = testNode.Properties.AsEnumerable() + .OfType() + .FirstOrDefault(); + + var className = testMethodIdentifier?.TypeName ?? "UnknownClass"; + var testName = testNode.DisplayName; + + // Write testcase element + writer.WriteStartElement("testcase"); + writer.WriteAttributeString("name", testName); + writer.WriteAttributeString("classname", className); + writer.WriteAttributeString("time", duration.TotalSeconds.ToString("F3", CultureInfo.InvariantCulture)); + + // Write state-specific child elements + switch (stateProperty) + { + case FailedTestNodeStateProperty failed: + WriteFailure(writer, failed); + break; + + case ErrorTestNodeStateProperty error: + WriteError(writer, error); + break; + + case TimeoutTestNodeStateProperty timeout: + WriteTimeoutError(writer, timeout); + break; + + case CancelledTestNodeStateProperty: + WriteCancellationError(writer); + break; + + case SkippedTestNodeStateProperty skipped: + WriteSkipped(writer, skipped); + break; + + case InProgressTestNodeStateProperty: + WriteInProgressError(writer); + break; + + case PassedTestNodeStateProperty: + // No child element for passed tests + break; + } + + writer.WriteEndElement(); // testcase + } + + private static void WriteFailure(XmlWriter writer, FailedTestNodeStateProperty failed) + { + writer.WriteStartElement("failure"); + + var exception = failed.Exception; + if (exception != null) + { + writer.WriteAttributeString("message", exception.Message); + writer.WriteAttributeString("type", exception.GetType().FullName ?? "AssertionException"); + writer.WriteString(exception.ToString()); + } + else + { + var message = failed.Explanation ?? "Test failed"; + writer.WriteAttributeString("message", message); + writer.WriteAttributeString("type", "TestFailedException"); + writer.WriteString(message); + } + + writer.WriteEndElement(); // failure + } + + private static void WriteError(XmlWriter writer, ErrorTestNodeStateProperty error) + { + writer.WriteStartElement("error"); + + var exception = error.Exception; + if (exception != null) + { + writer.WriteAttributeString("message", exception.Message); + writer.WriteAttributeString("type", exception.GetType().FullName ?? "Exception"); + writer.WriteString(exception.ToString()); + } + else + { + var message = error.Explanation ?? "Test error"; + writer.WriteAttributeString("message", message); + writer.WriteAttributeString("type", "TestErrorException"); + writer.WriteString(message); + } + + writer.WriteEndElement(); // error + } + + private static void WriteTimeoutError(XmlWriter writer, TimeoutTestNodeStateProperty timeout) + { + writer.WriteStartElement("error"); + var message = timeout.Explanation ?? "Test timed out"; + writer.WriteAttributeString("message", message); + writer.WriteAttributeString("type", "TimeoutException"); + writer.WriteString(message); + writer.WriteEndElement(); // error + } + + private static void WriteCancellationError(XmlWriter writer) + { + writer.WriteStartElement("error"); + writer.WriteAttributeString("message", "Test was cancelled"); + writer.WriteAttributeString("type", "CancelledException"); + writer.WriteString("Test was cancelled"); + writer.WriteEndElement(); // error + } + + private static void WriteInProgressError(XmlWriter writer) + { + writer.WriteStartElement("error"); + writer.WriteAttributeString("message", "Test never finished"); + writer.WriteAttributeString("type", "InProgressException"); + writer.WriteString("Test never finished"); + writer.WriteEndElement(); // error + } + + private static void WriteSkipped(XmlWriter writer, SkippedTestNodeStateProperty skipped) + { + writer.WriteStartElement("skipped"); + var message = skipped.Explanation ?? "Test skipped"; + writer.WriteAttributeString("message", message); + writer.WriteString(message); + writer.WriteEndElement(); // skipped + } + + private static Dictionary GetLastStates( + IEnumerable tests) + { + var lastStates = new Dictionary(); + + foreach (var test in tests) + { + lastStates[test.TestNode.Uid.Value] = test; + } + + return lastStates; + } + + private static TestSummary CalculateSummary(IEnumerable tests) + { + DateTimeOffset? earliestStartTime = null; + + var summary = new TestSummary(); + + foreach (var test in tests) + { + summary.Total++; + + var stateProperty = test.TestNode.Properties.AsEnumerable() + .OfType() + .FirstOrDefault(); + + var timing = test.TestNode.Properties.AsEnumerable() + .OfType() + .FirstOrDefault(); + + if (timing?.GlobalTiming.Duration is { } durationValue) + { + summary.TotalTime += durationValue; + + // Track the earliest start time from actual test execution + if (timing.GlobalTiming.StartTime is { } startTime) + { + if (earliestStartTime is null || startTime < earliestStartTime) + { + earliestStartTime = startTime; + } + } + } + + switch (stateProperty) + { + case FailedTestNodeStateProperty: + summary.Failures++; + break; + case ErrorTestNodeStateProperty: + case TimeoutTestNodeStateProperty: + case CancelledTestNodeStateProperty: + case InProgressTestNodeStateProperty: + summary.Errors++; + break; + case SkippedTestNodeStateProperty: + summary.Skipped++; + break; + } + } + + // Use earliest test start time, fallback to current time if no timing data available + summary.Timestamp = earliestStartTime ?? DateTimeOffset.Now; + + return summary; + } +} + +file sealed class Utf8StringWriter : StringWriter +{ + public override Encoding Encoding => Encoding.UTF8; +} + +internal sealed class TestSummary +{ + public int Total { get; set; } + public int Failures { get; set; } + public int Errors { get; set; } + public int Skipped { get; set; } + public TimeSpan TotalTime { get; set; } + public DateTimeOffset Timestamp { get; set; } +} diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 9714184f79..41743ad69d 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -21,6 +21,10 @@ namespace public static . That(string? value, [.("value")] string? expression = null) { } [.(1)] public static . That(.? value, [.("value")] string? expression = null) { } + [.(1)] + public static . That(<.?> func, [.("func")] string? expression = null) { } + [.(1)] + public static . That(<.<.?>> func, [.("func")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } @@ -417,10 +421,30 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class CollectionCountValueAssertion : . + public class CollectionCountEqualsAssertion : . where TCollection : . { - public CollectionCountValueAssertion(. collectionContext, ? predicate) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public class CollectionCountSource + where TCollection : . + { + public CollectionCountSource(. collectionContext, <., .?>? assertion) { } + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } + public class CollectionCountWithInlineAssertionAssertion : . + where TCollection : . + { + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } } [.("DoesNotContain")] public class CollectionDoesNotContainAssertion : . @@ -1115,6 +1139,10 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public class StringLengthValueAssertion : . + { + public StringLengthValueAssertion(. stringContext) { } + } public class StringMatchesAssertion : .<..RegexMatchCollection> { public StringMatchesAssertion(. context, . regex) { } @@ -1578,7 +1606,10 @@ namespace .Extensions public static . EqualTo(this . source, TValue? expected, [.("expected")] string? expression = null) { } public static ..HasFlagAssertion HasFlag(this . source, TEnum expectedFlag, [.("expectedFlag")] string? expression = null) where TEnum : struct, { } + [("Use Length() instead, which provides all numeric assertion methods. Example: Asse" + + "(str).Length().IsGreaterThan(5)")] public static ..LengthWrapper HasLength(this . source) { } + [("Use Length().IsEqualTo(expectedLength) instead.")] public static . HasLength(this . source, int expectedLength, [.("expectedLength")] string? expression = null) { } public static . HasMessageContaining(this . source, string expectedSubstring, [.("expectedSubstring")] string? expression = null) where TException : { } @@ -1633,6 +1664,7 @@ namespace .Extensions where TValue : { } public static . IsPositive(this . source) where TValue : struct, { } + public static . Length(this . source) { } [.(1)] public static . Member(this . source, .<>> memberSelector, <.<., TItem>, .<.>> assertions) { } [.("Uses reflection for legacy compatibility. For AOT compatibility, use the Member ThrowsExactly() where TException : { } } + public class AsyncFuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public AsyncFuncCollectionAssertion(<.<.?>> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public abstract class CollectionAssertionBase : ., ., . where TCollection : . { @@ -4346,11 +4386,16 @@ namespace .Sources public . Contains(TItem expected, [.("expected")] string? expression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } - public . Count( predicate, [.("predicate")] string? expression = null) { } + public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } + [.(-1)] + public . Count(<., .?> countAssertion, [.("countAssertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { } protected override string GetExpectation() { } + [("Use Count() instead, which provides all numeric assertion methods. Example: Asser" + + "(list).Count().IsGreaterThan(5)")] public ..CountWrapper HasCount() { } + [("Use Count().IsEqualTo(expectedCount) instead.")] public . HasCount(int expectedCount, [.("expectedCount")] string? expression = null) { } public . HasDistinctItems() { } public . HasSingleItem() { } @@ -4412,6 +4457,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class FuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public FuncCollectionAssertion(<.?> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public class TaskAssertion : ., .<.>, ., . { public TaskAssertion(. task, string? expression) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 665faeb994..90595a8661 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -19,6 +19,8 @@ namespace public static . That(. task, [.("task")] string? expression = null) { } public static . That(string? value, [.("value")] string? expression = null) { } public static . That(.? value, [.("value")] string? expression = null) { } + public static . That(<.?> func, [.("func")] string? expression = null) { } + public static . That(<.<.?>> func, [.("func")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } @@ -414,10 +416,30 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class CollectionCountValueAssertion : . + public class CollectionCountEqualsAssertion : . where TCollection : . { - public CollectionCountValueAssertion(. collectionContext, ? predicate) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public class CollectionCountSource + where TCollection : . + { + public CollectionCountSource(. collectionContext, <., .?>? assertion) { } + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } + public class CollectionCountWithInlineAssertionAssertion : . + where TCollection : . + { + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } } [.("DoesNotContain")] public class CollectionDoesNotContainAssertion : . @@ -1112,6 +1134,10 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public class StringLengthValueAssertion : . + { + public StringLengthValueAssertion(. stringContext) { } + } public class StringMatchesAssertion : .<..RegexMatchCollection> { public StringMatchesAssertion(. context, . regex) { } @@ -1575,7 +1601,10 @@ namespace .Extensions public static . EqualTo(this . source, TValue? expected, [.("expected")] string? expression = null) { } public static ..HasFlagAssertion HasFlag(this . source, TEnum expectedFlag, [.("expectedFlag")] string? expression = null) where TEnum : struct, { } + [("Use Length() instead, which provides all numeric assertion methods. Example: Asse" + + "(str).Length().IsGreaterThan(5)")] public static ..LengthWrapper HasLength(this . source) { } + [("Use Length().IsEqualTo(expectedLength) instead.")] public static . HasLength(this . source, int expectedLength, [.("expectedLength")] string? expression = null) { } public static . HasMessageContaining(this . source, string expectedSubstring, [.("expectedSubstring")] string? expression = null) where TException : { } @@ -1630,6 +1659,7 @@ namespace .Extensions where TValue : { } public static . IsPositive(this . source) where TValue : struct, { } + public static . Length(this . source) { } public static . Member(this . source, .<>> memberSelector, <.<., TItem>, .<.>> assertions) { } [.("Uses reflection for legacy compatibility. For AOT compatibility, use the Member overload with strongly-typed assertions.")] @@ -4313,6 +4343,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class AsyncFuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public AsyncFuncCollectionAssertion(<.<.?>> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public abstract class CollectionAssertionBase : ., ., . where TCollection : . { @@ -4326,11 +4364,15 @@ namespace .Sources public . Contains(TItem expected, [.("expected")] string? expression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } - public . Count( predicate, [.("predicate")] string? expression = null) { } + public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } + public . Count(<., .?> countAssertion, [.("countAssertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { } protected override string GetExpectation() { } + [("Use Count() instead, which provides all numeric assertion methods. Example: Asser" + + "(list).Count().IsGreaterThan(5)")] public ..CountWrapper HasCount() { } + [("Use Count().IsEqualTo(expectedCount) instead.")] public . HasCount(int expectedCount, [.("expectedCount")] string? expression = null) { } public . HasDistinctItems() { } public . HasSingleItem() { } @@ -4392,6 +4434,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class FuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public FuncCollectionAssertion(<.?> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public class TaskAssertion : ., .<.>, ., . { public TaskAssertion(. task, string? expression) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index c8665a0b6a..32e1ddc218 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -21,6 +21,10 @@ namespace public static . That(string? value, [.("value")] string? expression = null) { } [.(1)] public static . That(.? value, [.("value")] string? expression = null) { } + [.(1)] + public static . That(<.?> func, [.("func")] string? expression = null) { } + [.(1)] + public static . That(<.<.?>> func, [.("func")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } @@ -417,10 +421,30 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class CollectionCountValueAssertion : . + public class CollectionCountEqualsAssertion : . where TCollection : . { - public CollectionCountValueAssertion(. collectionContext, ? predicate) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public class CollectionCountSource + where TCollection : . + { + public CollectionCountSource(. collectionContext, <., .?>? assertion) { } + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } + public class CollectionCountWithInlineAssertionAssertion : . + where TCollection : . + { + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } } [.("DoesNotContain")] public class CollectionDoesNotContainAssertion : . @@ -1115,6 +1139,10 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public class StringLengthValueAssertion : . + { + public StringLengthValueAssertion(. stringContext) { } + } public class StringMatchesAssertion : .<..RegexMatchCollection> { public StringMatchesAssertion(. context, . regex) { } @@ -1578,7 +1606,10 @@ namespace .Extensions public static . EqualTo(this . source, TValue? expected, [.("expected")] string? expression = null) { } public static ..HasFlagAssertion HasFlag(this . source, TEnum expectedFlag, [.("expectedFlag")] string? expression = null) where TEnum : struct, { } + [("Use Length() instead, which provides all numeric assertion methods. Example: Asse" + + "(str).Length().IsGreaterThan(5)")] public static ..LengthWrapper HasLength(this . source) { } + [("Use Length().IsEqualTo(expectedLength) instead.")] public static . HasLength(this . source, int expectedLength, [.("expectedLength")] string? expression = null) { } public static . HasMessageContaining(this . source, string expectedSubstring, [.("expectedSubstring")] string? expression = null) where TException : { } @@ -1633,6 +1664,7 @@ namespace .Extensions where TValue : { } public static . IsPositive(this . source) where TValue : struct, { } + public static . Length(this . source) { } [.(1)] public static . Member(this . source, .<>> memberSelector, <.<., TItem>, .<.>> assertions) { } [.("Uses reflection for legacy compatibility. For AOT compatibility, use the Member ThrowsExactly() where TException : { } } + public class AsyncFuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public AsyncFuncCollectionAssertion(<.<.?>> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public abstract class CollectionAssertionBase : ., ., . where TCollection : . { @@ -4346,11 +4386,16 @@ namespace .Sources public . Contains(TItem expected, [.("expected")] string? expression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } - public . Count( predicate, [.("predicate")] string? expression = null) { } + public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } + [.(-1)] + public . Count(<., .?> countAssertion, [.("countAssertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { } protected override string GetExpectation() { } + [("Use Count() instead, which provides all numeric assertion methods. Example: Asser" + + "(list).Count().IsGreaterThan(5)")] public ..CountWrapper HasCount() { } + [("Use Count().IsEqualTo(expectedCount) instead.")] public . HasCount(int expectedCount, [.("expectedCount")] string? expression = null) { } public . HasDistinctItems() { } public . HasSingleItem() { } @@ -4412,6 +4457,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class FuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public FuncCollectionAssertion(<.?> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public class TaskAssertion : ., .<.>, ., . { public TaskAssertion(. task, string? expression) { } diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index bd832a6fca..0ceb47da88 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -19,6 +19,8 @@ namespace public static . That(. task, [.("task")] string? expression = null) { } public static . That(string? value, [.("value")] string? expression = null) { } public static . That(.? value, [.("value")] string? expression = null) { } + public static . That(<.?> func, [.("func")] string? expression = null) { } + public static . That(<.<.?>> func, [.("func")] string? expression = null) { } public static . That(<.> func, [.("func")] string? expression = null) { } public static . That( func, [.("func")] string? expression = null) { } public static . That(. task, [.("task")] string? expression = null) { } @@ -410,10 +412,30 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } - public class CollectionCountValueAssertion : . + public class CollectionCountEqualsAssertion : . where TCollection : . { - public CollectionCountValueAssertion(. collectionContext, ? predicate) { } + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } + } + public class CollectionCountSource + where TCollection : . + { + public CollectionCountSource(. collectionContext, <., .?>? assertion) { } + public . IsEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThan(int expected, [.("expected")] string? expression = null) { } + public . IsGreaterThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsLessThan(int expected, [.("expected")] string? expression = null) { } + public . IsLessThanOrEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsNotEqualTo(int expected, [.("expected")] string? expression = null) { } + public . IsPositive() { } + public . IsZero() { } + } + public class CollectionCountWithInlineAssertionAssertion : . + where TCollection : . + { + protected override .<.> CheckAsync(. metadata) { } + protected override string GetExpectation() { } } [.("DoesNotContain")] public class CollectionDoesNotContainAssertion : . @@ -1044,6 +1066,10 @@ namespace .Conditions protected override .<.> CheckAsync(. metadata) { } protected override string GetExpectation() { } } + public class StringLengthValueAssertion : . + { + public StringLengthValueAssertion(. stringContext) { } + } public class StringMatchesAssertion : .<..RegexMatchCollection> { public StringMatchesAssertion(. context, . regex) { } @@ -1476,7 +1502,10 @@ namespace .Extensions public static . EqualTo(this . source, TValue? expected, [.("expected")] string? expression = null) { } public static ..HasFlagAssertion HasFlag(this . source, TEnum expectedFlag, [.("expectedFlag")] string? expression = null) where TEnum : struct, { } + [("Use Length() instead, which provides all numeric assertion methods. Example: Asse" + + "(str).Length().IsGreaterThan(5)")] public static ..LengthWrapper HasLength(this . source) { } + [("Use Length().IsEqualTo(expectedLength) instead.")] public static . HasLength(this . source, int expectedLength, [.("expectedLength")] string? expression = null) { } public static . HasMessageContaining(this . source, string expectedSubstring, [.("expectedSubstring")] string? expression = null) where TException : { } @@ -1521,6 +1550,7 @@ namespace .Extensions where TValue : { } public static . IsPositive(this . source) where TValue : struct, { } + public static . Length(this . source) { } public static . Member(this . source, .<>> memberSelector, <.<., TItem>, .<.>> assertions) { } public static . Member(this . source, .<>> memberSelector, <.<., TItem>, object> assertions) { } public static . Member(this . source, .<> memberSelector, <., .> assertions) { } @@ -3802,6 +3832,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class AsyncFuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public AsyncFuncCollectionAssertion(<.<.?>> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public abstract class CollectionAssertionBase : ., ., . where TCollection : . { @@ -3815,11 +3853,15 @@ namespace .Sources public . Contains(TItem expected, [.("expected")] string? expression = null) { } public . ContainsOnly( predicate, [.("predicate")] string? expression = null) { } public . Count() { } - public . Count( predicate, [.("predicate")] string? expression = null) { } + public . Count(<., .?> itemAssertion, [.("itemAssertion")] string? expression = null) { } + public . Count(<., .?> countAssertion, [.("countAssertion")] string? expression = null) { } public . DoesNotContain( predicate, [.("predicate")] string? expression = null) { } public . DoesNotContain(TItem expected, [.("expected")] string? expression = null) { } protected override string GetExpectation() { } + [("Use Count() instead, which provides all numeric assertion methods. Example: Asser" + + "(list).Count().IsGreaterThan(5)")] public ..CountWrapper HasCount() { } + [("Use Count().IsEqualTo(expectedCount) instead.")] public . HasCount(int expectedCount, [.("expectedCount")] string? expression = null) { } public . HasDistinctItems() { } public . HasSingleItem() { } @@ -3881,6 +3923,14 @@ namespace .Sources public . ThrowsExactly() where TException : { } } + public class FuncCollectionAssertion : .<., TItem>, ., .<.>, .<.> + { + public FuncCollectionAssertion(<.?> func, string? expression) { } + public . Throws() + where TException : { } + public . ThrowsExactly() + where TException : { } + } public class TaskAssertion : ., .<.>, ., . { public TaskAssertion(. task, string? expression) { } diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 556486d6e4..822e18b671 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -6,6 +6,7 @@ namespace public abstract class AbstractDynamicTest { protected AbstractDynamicTest() { } + public int DynamicTestIndex { get; set; } public abstract .<.DiscoveryResult> GetTests(); } public abstract class AbstractDynamicTest<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicFields | ..NonPublicFields | ..PublicProperties)] T> : .AbstractDynamicTest @@ -371,7 +372,7 @@ namespace public static .ClassMetadata GetOrAdd(string name, <.ClassMetadata> factory) { } } [(.Class | .Method)] - public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData + public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute { public CombinedDataSourcesAttribute() { } [.(typeof(.CombinedDataSourcesAttribute.d__0))] @@ -458,6 +459,7 @@ namespace public DataSourceException(string dataSourceName, innerException) { } public DataSourceException(string dataSourceName, string message) { } public string DataSourceName { get; } + public static .DataSourceException FromNestedFailure(string message, innerException) { } } [(.Class | .Method | .Property | .Parameter, AllowMultiple=true)] public abstract class DataSourceGeneratorAttribute<[.(..PublicConstructors)] T> : .AsyncDataSourceGeneratorAttribute @@ -617,6 +619,7 @@ namespace public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } public string? DisplayName { get; set; } + public int DynamicTestIndex { get; set; } public string? ParentTestId { get; set; } public .? Properties { get; set; } public .? Relationship { get; set; } @@ -644,6 +647,16 @@ namespace { public static T Argument() { } } + public sealed class DynamicTestMetadata : .TestMetadata, .IDynamicTestMetadata + { + public DynamicTestMetadata(.DynamicDiscoveryResult dynamicResult) { } + public override <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; } + public string? DisplayName { get; } + public int DynamicTestIndex { get; } + public string? ParentTestId { get; } + public .? Properties { get; } + public .? Relationship { get; } + } public class DynamicTest<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicFields | ..NonPublicFields | ..PublicProperties)] T> : .AbstractDynamicTest, .IDynamicTestCreatorLocation where T : class { @@ -651,6 +664,7 @@ namespace public .<> Attributes { get; set; } public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } + public string? DisplayName { get; set; } public object?[]? TestClassArguments { get; set; } public .<>? TestMethod { get; set; } public object?[]? TestMethodArguments { get; set; } @@ -853,7 +867,11 @@ namespace string? CreatorFilePath { get; set; } int? CreatorLineNumber { get; set; } } - public interface IDynamicTestMetadata { } + public interface IDynamicTestMetadata + { + string? DisplayName { get; } + int DynamicTestIndex { get; } + } public interface IDynamicTestSource { .<.AbstractDynamicTest> CollectDynamicTests(string sessionId); @@ -881,6 +899,7 @@ namespace public class InstanceMethodDataSourceAttribute : .MethodDataSourceAttribute, .IAccessesInstanceData { public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) { } + public InstanceMethodDataSourceAttribute([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicProperties)] classProvidingDataSource, string methodNameProvidingDataSource) { } } public class InvalidTestMetadataException : .TestBuilderException { @@ -999,10 +1018,6 @@ namespace public . NotInParallelConstraintKeys { get; init; } public int Order { get; set; } } - public static class ObjectInitializer - { - public static . InitializeAsync(object? obj, .CancellationToken cancellationToken = default) { } - } public class ParallelGroupAttribute : .TUnitAttribute, ., . { public ParallelGroupAttribute(string group) { } @@ -1667,6 +1682,22 @@ namespace .DataSources public static string FormatArguments(object?[] arguments, .<> formatters) { } } } +namespace .Discovery +{ + public sealed class InitializerPropertyInfo + { + public InitializerPropertyInfo() { } + public required GetValue { get; init; } + public required string PropertyName { get; init; } + public required PropertyType { get; init; } + } + public static class InitializerPropertyRegistry + { + public static .[]? GetProperties( type) { } + public static bool HasRegistration( type) { } + public static void Register( type, .[] properties) { } + } +} namespace .Enums { public enum DataGeneratorType @@ -2020,14 +2051,23 @@ namespace .Helpers [.("MakeGenericType requires runtime code generation")] public static MakeGenericTypeSafe( genericTypeDefinition, params [] typeArguments) { } } + public static class ParallelTaskHelper + { + public static . ForEachAsync(. items, action) { } + public static . ForEachAsync(T[] items, action) { } + public static . ForEachAsync(. items, action, .CancellationToken cancellationToken) { } + public static . ForEachAsync(T[] items, action, .CancellationToken cancellationToken) { } + public static . ForEachWithIndexAsync(T[] items, action) { } + public static . ForEachWithIndexAsync(T[] items, action, .CancellationToken cancellationToken) { } + } public class ProcessorCountParallelLimit : . { public ProcessorCountParallelLimit() { } public int Limit { get; } } - public class ReferenceEqualityComparer : . + public sealed class ReferenceEqualityComparer : . { - public ReferenceEqualityComparer() { } + public static readonly . Instance; public bool Equals(object? x, object? y) { } public int GetHashCode(object obj) { } } @@ -2198,6 +2238,7 @@ namespace .Interfaces { .<> GenerateDataFactoriesAsync(.DataSourceContext context, .CancellationToken cancellationToken = default); } + public interface IAsyncDiscoveryInitializer : . { } public interface IAsyncInitializer { . InitializeAsync(); @@ -2227,6 +2268,10 @@ namespace .Interfaces { .<> GenerateDataFactories(.DataSourceContext context); } + public interface IDisposer + { + . DisposeAsync(object? obj); + } public interface IEventReceiver { int Order { get; } @@ -2422,6 +2467,7 @@ namespace .Interfaces .TextWriter StandardOutput { get; } .<.Timing> Timings { get; } void AttachArtifact(.Artifact artifact); + void AttachArtifact(string filePath, string? displayName = null, string? description = null); string GetErrorOutput(); string GetStandardOutput(); void RecordTiming(.Timing timing); @@ -2493,7 +2539,7 @@ namespace . public sealed class PropertyInjectionMetadata { public PropertyInjectionMetadata() { } - [.(..PublicProperties)] + [.(..None | ..PublicProperties | ..NonPublicProperties)] public required ContainingType { get; init; } public required <.IDataSourceAttribute> CreateDataSource { get; init; } public required string PropertyName { get; init; } @@ -2586,6 +2632,14 @@ namespace .Models public ? MethodInvoker { get; set; } } } +namespace .PropertyInjection +{ + public static class PropertyCacheKeyGenerator + { + public static string GetCacheKey(.PropertyInfo property) { } + public static string GetCacheKey(..PropertyInjectionMetadata metadata) { } + } +} namespace .Services { [.("Generic type resolution requires runtime type generation")] diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 8bbca55ff3..fbdb6afb31 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -6,6 +6,7 @@ namespace public abstract class AbstractDynamicTest { protected AbstractDynamicTest() { } + public int DynamicTestIndex { get; set; } public abstract .<.DiscoveryResult> GetTests(); } public abstract class AbstractDynamicTest<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicFields | ..NonPublicFields | ..PublicProperties)] T> : .AbstractDynamicTest @@ -371,7 +372,7 @@ namespace public static .ClassMetadata GetOrAdd(string name, <.ClassMetadata> factory) { } } [(.Class | .Method)] - public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData + public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute { public CombinedDataSourcesAttribute() { } [.(typeof(.CombinedDataSourcesAttribute.d__0))] @@ -458,6 +459,7 @@ namespace public DataSourceException(string dataSourceName, innerException) { } public DataSourceException(string dataSourceName, string message) { } public string DataSourceName { get; } + public static .DataSourceException FromNestedFailure(string message, innerException) { } } [(.Class | .Method | .Property | .Parameter, AllowMultiple=true)] public abstract class DataSourceGeneratorAttribute<[.(..PublicConstructors)] T> : .AsyncDataSourceGeneratorAttribute @@ -617,6 +619,7 @@ namespace public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } public string? DisplayName { get; set; } + public int DynamicTestIndex { get; set; } public string? ParentTestId { get; set; } public .? Properties { get; set; } public .? Relationship { get; set; } @@ -644,6 +647,16 @@ namespace { public static T Argument() { } } + public sealed class DynamicTestMetadata : .TestMetadata, .IDynamicTestMetadata + { + public DynamicTestMetadata(.DynamicDiscoveryResult dynamicResult) { } + public override <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; } + public string? DisplayName { get; } + public int DynamicTestIndex { get; } + public string? ParentTestId { get; } + public .? Properties { get; } + public .? Relationship { get; } + } public class DynamicTest<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicFields | ..NonPublicFields | ..PublicProperties)] T> : .AbstractDynamicTest, .IDynamicTestCreatorLocation where T : class { @@ -651,6 +664,7 @@ namespace public .<> Attributes { get; set; } public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } + public string? DisplayName { get; set; } public object?[]? TestClassArguments { get; set; } public .<>? TestMethod { get; set; } public object?[]? TestMethodArguments { get; set; } @@ -853,7 +867,11 @@ namespace string? CreatorFilePath { get; set; } int? CreatorLineNumber { get; set; } } - public interface IDynamicTestMetadata { } + public interface IDynamicTestMetadata + { + string? DisplayName { get; } + int DynamicTestIndex { get; } + } public interface IDynamicTestSource { .<.AbstractDynamicTest> CollectDynamicTests(string sessionId); @@ -881,6 +899,7 @@ namespace public class InstanceMethodDataSourceAttribute : .MethodDataSourceAttribute, .IAccessesInstanceData { public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) { } + public InstanceMethodDataSourceAttribute([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicProperties)] classProvidingDataSource, string methodNameProvidingDataSource) { } } public class InvalidTestMetadataException : .TestBuilderException { @@ -999,10 +1018,6 @@ namespace public . NotInParallelConstraintKeys { get; init; } public int Order { get; set; } } - public static class ObjectInitializer - { - public static . InitializeAsync(object? obj, .CancellationToken cancellationToken = default) { } - } public class ParallelGroupAttribute : .TUnitAttribute, ., . { public ParallelGroupAttribute(string group) { } @@ -1667,6 +1682,22 @@ namespace .DataSources public static string FormatArguments(object?[] arguments, .<> formatters) { } } } +namespace .Discovery +{ + public sealed class InitializerPropertyInfo + { + public InitializerPropertyInfo() { } + public required GetValue { get; init; } + public required string PropertyName { get; init; } + public required PropertyType { get; init; } + } + public static class InitializerPropertyRegistry + { + public static .[]? GetProperties( type) { } + public static bool HasRegistration( type) { } + public static void Register( type, .[] properties) { } + } +} namespace .Enums { public enum DataGeneratorType @@ -2020,14 +2051,23 @@ namespace .Helpers [.("MakeGenericType requires runtime code generation")] public static MakeGenericTypeSafe( genericTypeDefinition, params [] typeArguments) { } } + public static class ParallelTaskHelper + { + public static . ForEachAsync(. items, action) { } + public static . ForEachAsync(T[] items, action) { } + public static . ForEachAsync(. items, action, .CancellationToken cancellationToken) { } + public static . ForEachAsync(T[] items, action, .CancellationToken cancellationToken) { } + public static . ForEachWithIndexAsync(T[] items, action) { } + public static . ForEachWithIndexAsync(T[] items, action, .CancellationToken cancellationToken) { } + } public class ProcessorCountParallelLimit : . { public ProcessorCountParallelLimit() { } public int Limit { get; } } - public class ReferenceEqualityComparer : . + public sealed class ReferenceEqualityComparer : . { - public ReferenceEqualityComparer() { } + public static readonly . Instance; public bool Equals(object? x, object? y) { } public int GetHashCode(object obj) { } } @@ -2198,6 +2238,7 @@ namespace .Interfaces { .<> GenerateDataFactoriesAsync(.DataSourceContext context, .CancellationToken cancellationToken = default); } + public interface IAsyncDiscoveryInitializer : . { } public interface IAsyncInitializer { . InitializeAsync(); @@ -2227,6 +2268,10 @@ namespace .Interfaces { .<> GenerateDataFactories(.DataSourceContext context); } + public interface IDisposer + { + . DisposeAsync(object? obj); + } public interface IEventReceiver { int Order { get; } @@ -2422,6 +2467,7 @@ namespace .Interfaces .TextWriter StandardOutput { get; } .<.Timing> Timings { get; } void AttachArtifact(.Artifact artifact); + void AttachArtifact(string filePath, string? displayName = null, string? description = null); string GetErrorOutput(); string GetStandardOutput(); void RecordTiming(.Timing timing); @@ -2493,7 +2539,7 @@ namespace . public sealed class PropertyInjectionMetadata { public PropertyInjectionMetadata() { } - [.(..PublicProperties)] + [.(..None | ..PublicProperties | ..NonPublicProperties)] public required ContainingType { get; init; } public required <.IDataSourceAttribute> CreateDataSource { get; init; } public required string PropertyName { get; init; } @@ -2586,6 +2632,14 @@ namespace .Models public ? MethodInvoker { get; set; } } } +namespace .PropertyInjection +{ + public static class PropertyCacheKeyGenerator + { + public static string GetCacheKey(.PropertyInfo property) { } + public static string GetCacheKey(..PropertyInjectionMetadata metadata) { } + } +} namespace .Services { [.("Generic type resolution requires runtime type generation")] diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index e7945a9846..870981ebd9 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -6,6 +6,7 @@ namespace public abstract class AbstractDynamicTest { protected AbstractDynamicTest() { } + public int DynamicTestIndex { get; set; } public abstract .<.DiscoveryResult> GetTests(); } public abstract class AbstractDynamicTest<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicFields | ..NonPublicFields | ..PublicProperties)] T> : .AbstractDynamicTest @@ -371,7 +372,7 @@ namespace public static .ClassMetadata GetOrAdd(string name, <.ClassMetadata> factory) { } } [(.Class | .Method)] - public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData + public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute { public CombinedDataSourcesAttribute() { } [.(typeof(.CombinedDataSourcesAttribute.d__0))] @@ -458,6 +459,7 @@ namespace public DataSourceException(string dataSourceName, innerException) { } public DataSourceException(string dataSourceName, string message) { } public string DataSourceName { get; } + public static .DataSourceException FromNestedFailure(string message, innerException) { } } [(.Class | .Method | .Property | .Parameter, AllowMultiple=true)] public abstract class DataSourceGeneratorAttribute<[.(..PublicConstructors)] T> : .AsyncDataSourceGeneratorAttribute @@ -617,6 +619,7 @@ namespace public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } public string? DisplayName { get; set; } + public int DynamicTestIndex { get; set; } public string? ParentTestId { get; set; } public .? Properties { get; set; } public .? Relationship { get; set; } @@ -644,6 +647,16 @@ namespace { public static T Argument() { } } + public sealed class DynamicTestMetadata : .TestMetadata, .IDynamicTestMetadata + { + public DynamicTestMetadata(.DynamicDiscoveryResult dynamicResult) { } + public override <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; } + public string? DisplayName { get; } + public int DynamicTestIndex { get; } + public string? ParentTestId { get; } + public .? Properties { get; } + public .? Relationship { get; } + } public class DynamicTest<[.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..NonPublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicFields | ..NonPublicFields | ..PublicProperties)] T> : .AbstractDynamicTest, .IDynamicTestCreatorLocation where T : class { @@ -651,6 +664,7 @@ namespace public .<> Attributes { get; set; } public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } + public string? DisplayName { get; set; } public object?[]? TestClassArguments { get; set; } public .<>? TestMethod { get; set; } public object?[]? TestMethodArguments { get; set; } @@ -853,7 +867,11 @@ namespace string? CreatorFilePath { get; set; } int? CreatorLineNumber { get; set; } } - public interface IDynamicTestMetadata { } + public interface IDynamicTestMetadata + { + string? DisplayName { get; } + int DynamicTestIndex { get; } + } public interface IDynamicTestSource { .<.AbstractDynamicTest> CollectDynamicTests(string sessionId); @@ -881,6 +899,7 @@ namespace public class InstanceMethodDataSourceAttribute : .MethodDataSourceAttribute, .IAccessesInstanceData { public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) { } + public InstanceMethodDataSourceAttribute([.(..None | ..PublicParameterlessConstructor | ..PublicConstructors | ..PublicMethods | ..NonPublicMethods | ..PublicProperties)] classProvidingDataSource, string methodNameProvidingDataSource) { } } public class InvalidTestMetadataException : .TestBuilderException { @@ -999,10 +1018,6 @@ namespace public . NotInParallelConstraintKeys { get; init; } public int Order { get; set; } } - public static class ObjectInitializer - { - public static . InitializeAsync(object? obj, .CancellationToken cancellationToken = default) { } - } public class ParallelGroupAttribute : .TUnitAttribute, ., . { public ParallelGroupAttribute(string group) { } @@ -1667,6 +1682,22 @@ namespace .DataSources public static string FormatArguments(object?[] arguments, .<> formatters) { } } } +namespace .Discovery +{ + public sealed class InitializerPropertyInfo + { + public InitializerPropertyInfo() { } + public required GetValue { get; init; } + public required string PropertyName { get; init; } + public required PropertyType { get; init; } + } + public static class InitializerPropertyRegistry + { + public static .[]? GetProperties( type) { } + public static bool HasRegistration( type) { } + public static void Register( type, .[] properties) { } + } +} namespace .Enums { public enum DataGeneratorType @@ -2020,14 +2051,23 @@ namespace .Helpers [.("MakeGenericType requires runtime code generation")] public static MakeGenericTypeSafe( genericTypeDefinition, params [] typeArguments) { } } + public static class ParallelTaskHelper + { + public static . ForEachAsync(. items, action) { } + public static . ForEachAsync(T[] items, action) { } + public static . ForEachAsync(. items, action, .CancellationToken cancellationToken) { } + public static . ForEachAsync(T[] items, action, .CancellationToken cancellationToken) { } + public static . ForEachWithIndexAsync(T[] items, action) { } + public static . ForEachWithIndexAsync(T[] items, action, .CancellationToken cancellationToken) { } + } public class ProcessorCountParallelLimit : . { public ProcessorCountParallelLimit() { } public int Limit { get; } } - public class ReferenceEqualityComparer : . + public sealed class ReferenceEqualityComparer : . { - public ReferenceEqualityComparer() { } + public static readonly . Instance; public bool Equals(object? x, object? y) { } public int GetHashCode(object obj) { } } @@ -2198,6 +2238,7 @@ namespace .Interfaces { .<> GenerateDataFactoriesAsync(.DataSourceContext context, .CancellationToken cancellationToken = default); } + public interface IAsyncDiscoveryInitializer : . { } public interface IAsyncInitializer { . InitializeAsync(); @@ -2227,6 +2268,10 @@ namespace .Interfaces { .<> GenerateDataFactories(.DataSourceContext context); } + public interface IDisposer + { + . DisposeAsync(object? obj); + } public interface IEventReceiver { int Order { get; } @@ -2422,6 +2467,7 @@ namespace .Interfaces .TextWriter StandardOutput { get; } .<.Timing> Timings { get; } void AttachArtifact(.Artifact artifact); + void AttachArtifact(string filePath, string? displayName = null, string? description = null); string GetErrorOutput(); string GetStandardOutput(); void RecordTiming(.Timing timing); @@ -2493,7 +2539,7 @@ namespace . public sealed class PropertyInjectionMetadata { public PropertyInjectionMetadata() { } - [.(..PublicProperties)] + [.(..None | ..PublicProperties | ..NonPublicProperties)] public required ContainingType { get; init; } public required <.IDataSourceAttribute> CreateDataSource { get; init; } public required string PropertyName { get; init; } @@ -2586,6 +2632,14 @@ namespace .Models public ? MethodInvoker { get; set; } } } +namespace .PropertyInjection +{ + public static class PropertyCacheKeyGenerator + { + public static string GetCacheKey(.PropertyInfo property) { } + public static string GetCacheKey(..PropertyInjectionMetadata metadata) { } + } +} namespace .Services { [.("Generic type resolution requires runtime type generation")] diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt index dd3923472e..22c1bd53e1 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -6,6 +6,7 @@ namespace public abstract class AbstractDynamicTest { protected AbstractDynamicTest() { } + public int DynamicTestIndex { get; set; } public abstract .<.DiscoveryResult> GetTests(); } public abstract class AbstractDynamicTest : .AbstractDynamicTest @@ -351,7 +352,7 @@ namespace public static .ClassMetadata GetOrAdd(string name, <.ClassMetadata> factory) { } } [(.Class | .Method)] - public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute, .IAccessesInstanceData + public sealed class CombinedDataSourcesAttribute : .AsyncUntypedDataSourceGeneratorAttribute { public CombinedDataSourcesAttribute() { } [.(typeof(.CombinedDataSourcesAttribute.d__0))] @@ -438,6 +439,7 @@ namespace public DataSourceException(string dataSourceName, innerException) { } public DataSourceException(string dataSourceName, string message) { } public string DataSourceName { get; } + public static .DataSourceException FromNestedFailure(string message, innerException) { } } [(.Class | .Method | .Property | .Parameter, AllowMultiple=true)] public abstract class DataSourceGeneratorAttribute : .AsyncDataSourceGeneratorAttribute @@ -597,6 +599,7 @@ namespace public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } public string? DisplayName { get; set; } + public int DynamicTestIndex { get; set; } public string? ParentTestId { get; set; } public .? Properties { get; set; } public .? Relationship { get; set; } @@ -621,6 +624,16 @@ namespace { public static T Argument() { } } + public sealed class DynamicTestMetadata : .TestMetadata, .IDynamicTestMetadata + { + public DynamicTestMetadata(.DynamicDiscoveryResult dynamicResult) { } + public override <.ExecutableTestCreationContext, .TestMetadata, .AbstractExecutableTest> CreateExecutableTestFactory { get; } + public string? DisplayName { get; } + public int DynamicTestIndex { get; } + public string? ParentTestId { get; } + public .? Properties { get; } + public .? Relationship { get; } + } public class DynamicTest : .AbstractDynamicTest, .IDynamicTestCreatorLocation where T : class { @@ -628,6 +641,7 @@ namespace public .<> Attributes { get; set; } public string? CreatorFilePath { get; set; } public int? CreatorLineNumber { get; set; } + public string? DisplayName { get; set; } public object?[]? TestClassArguments { get; set; } public .<>? TestMethod { get; set; } public object?[]? TestMethodArguments { get; set; } @@ -830,7 +844,11 @@ namespace string? CreatorFilePath { get; set; } int? CreatorLineNumber { get; set; } } - public interface IDynamicTestMetadata { } + public interface IDynamicTestMetadata + { + string? DisplayName { get; } + int DynamicTestIndex { get; } + } public interface IDynamicTestSource { .<.AbstractDynamicTest> CollectDynamicTests(string sessionId); @@ -858,6 +876,7 @@ namespace public class InstanceMethodDataSourceAttribute : .MethodDataSourceAttribute, .IAccessesInstanceData { public InstanceMethodDataSourceAttribute(string methodNameProvidingDataSource) { } + public InstanceMethodDataSourceAttribute( classProvidingDataSource, string methodNameProvidingDataSource) { } } public class InvalidTestMetadataException : .TestBuilderException { @@ -962,10 +981,6 @@ namespace public . NotInParallelConstraintKeys { get; init; } public int Order { get; set; } } - public static class ObjectInitializer - { - public static . InitializeAsync(object? obj, .CancellationToken cancellationToken = default) { } - } public class ParallelGroupAttribute : .TUnitAttribute, ., . { public ParallelGroupAttribute(string group) { } @@ -1620,6 +1635,22 @@ namespace .DataSources public static string FormatArguments(object?[] arguments, .<> formatters) { } } } +namespace .Discovery +{ + public sealed class InitializerPropertyInfo + { + public InitializerPropertyInfo() { } + public required GetValue { get; init; } + public required string PropertyName { get; init; } + public required PropertyType { get; init; } + } + public static class InitializerPropertyRegistry + { + public static .[]? GetProperties( type) { } + public static bool HasRegistration( type) { } + public static void Register( type, .[] properties) { } + } +} namespace .Enums { public enum DataGeneratorType @@ -1959,14 +1990,23 @@ namespace .Helpers public static bool IsConstructedGenericType( type) { } public static MakeGenericTypeSafe( genericTypeDefinition, params [] typeArguments) { } } + public static class ParallelTaskHelper + { + public static . ForEachAsync(. items, action) { } + public static . ForEachAsync(T[] items, action) { } + public static . ForEachAsync(. items, action, .CancellationToken cancellationToken) { } + public static . ForEachAsync(T[] items, action, .CancellationToken cancellationToken) { } + public static . ForEachWithIndexAsync(T[] items, action) { } + public static . ForEachWithIndexAsync(T[] items, action, .CancellationToken cancellationToken) { } + } public class ProcessorCountParallelLimit : . { public ProcessorCountParallelLimit() { } public int Limit { get; } } - public class ReferenceEqualityComparer : . + public sealed class ReferenceEqualityComparer : . { - public ReferenceEqualityComparer() { } + public static readonly . Instance; public bool Equals(object? x, object? y) { } public int GetHashCode(object obj) { } } @@ -2130,6 +2170,7 @@ namespace .Interfaces { .<> GenerateDataFactoriesAsync(.DataSourceContext context, .CancellationToken cancellationToken = default); } + public interface IAsyncDiscoveryInitializer : . { } public interface IAsyncInitializer { . InitializeAsync(); @@ -2159,6 +2200,10 @@ namespace .Interfaces { .<> GenerateDataFactories(.DataSourceContext context); } + public interface IDisposer + { + . DisposeAsync(object? obj); + } public interface IEventReceiver { int Order { get; } @@ -2352,6 +2397,7 @@ namespace .Interfaces .TextWriter StandardOutput { get; } .<.Timing> Timings { get; } void AttachArtifact(.Artifact artifact); + void AttachArtifact(string filePath, string? displayName = null, string? description = null); string GetErrorOutput(); string GetStandardOutput(); void RecordTiming(.Timing timing); @@ -2508,6 +2554,14 @@ namespace .Models public ? MethodInvoker { get; set; } } } +namespace .PropertyInjection +{ + public static class PropertyCacheKeyGenerator + { + public static string GetCacheKey(.PropertyInfo property) { } + public static string GetCacheKey(..PropertyInjectionMetadata metadata) { } + } +} namespace .Services { public class GenericTypeResolver : . diff --git a/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/TestProject.fsproj b/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/TestProject.fsproj index d2e029d0c6..e4161bea03 100644 --- a/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/TestProject.fsproj +++ b/TUnit.Templates/content/TUnit.AspNet.FSharp/TestProject/TestProject.fsproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj b/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj index 905a5dd8b7..c89c3b6687 100644 --- a/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj +++ b/TUnit.Templates/content/TUnit.AspNet/TestProject/TestProject.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/TUnit.Templates/content/TUnit.AspNet/WebApp/WebApp.csproj b/TUnit.Templates/content/TUnit.AspNet/WebApp/WebApp.csproj index d1fadfb3ce..810579733e 100644 --- a/TUnit.Templates/content/TUnit.AspNet/WebApp/WebApp.csproj +++ b/TUnit.Templates/content/TUnit.AspNet/WebApp/WebApp.csproj @@ -7,7 +7,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.ApiService/ExampleNamespace.ApiService.csproj b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.ApiService/ExampleNamespace.ApiService.csproj index e3c1bc99c1..ed704c50b0 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.ApiService/ExampleNamespace.ApiService.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.ApiService/ExampleNamespace.ApiService.csproj @@ -11,7 +11,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.AppHost/ExampleNamespace.AppHost.csproj b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.AppHost/ExampleNamespace.AppHost.csproj index 44bd877374..4e51d34400 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.AppHost/ExampleNamespace.AppHost.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.AppHost/ExampleNamespace.AppHost.csproj @@ -1,6 +1,6 @@  - + Exe @@ -12,8 +12,8 @@ - - + + diff --git a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.ServiceDefaults/ExampleNamespace.ServiceDefaults.csproj b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.ServiceDefaults/ExampleNamespace.ServiceDefaults.csproj index 6e350de0e8..eeb71e38f4 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.ServiceDefaults/ExampleNamespace.ServiceDefaults.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.ServiceDefaults/ExampleNamespace.ServiceDefaults.csproj @@ -10,13 +10,13 @@ - - + + - - - + + + diff --git a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj index 107780bda0..5d4f5e21a4 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.TestProject/ExampleNamespace.TestProject.csproj @@ -10,8 +10,8 @@ - - + + diff --git a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.WebApp/ExampleNamespace.WebApp.csproj b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.WebApp/ExampleNamespace.WebApp.csproj index 3d403305bb..79f6c61e8a 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.WebApp/ExampleNamespace.WebApp.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Starter/ExampleNamespace.WebApp/ExampleNamespace.WebApp.csproj @@ -7,7 +7,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj b/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj index bbe3ede435..2316a2b3be 100644 --- a/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj +++ b/TUnit.Templates/content/TUnit.Aspire.Test/ExampleNamespace.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/TUnit.Templates/content/TUnit.FSharp/TestProject.fsproj b/TUnit.Templates/content/TUnit.FSharp/TestProject.fsproj index 1751db5e8c..251f36eda5 100644 --- a/TUnit.Templates/content/TUnit.FSharp/TestProject.fsproj +++ b/TUnit.Templates/content/TUnit.FSharp/TestProject.fsproj @@ -9,9 +9,9 @@ - - - + + + diff --git a/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj b/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj index ee11fb3565..a41db1f439 100644 --- a/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj +++ b/TUnit.Templates/content/TUnit.Playwright/TestProject.csproj @@ -8,7 +8,7 @@ - + diff --git a/TUnit.Templates/content/TUnit.VB/TestProject.vbproj b/TUnit.Templates/content/TUnit.VB/TestProject.vbproj index 548b07f51c..97dd3f0631 100644 --- a/TUnit.Templates/content/TUnit.VB/TestProject.vbproj +++ b/TUnit.Templates/content/TUnit.VB/TestProject.vbproj @@ -8,6 +8,6 @@ - + diff --git a/TUnit.Templates/content/TUnit/TestProject.csproj b/TUnit.Templates/content/TUnit/TestProject.csproj index b49bdc3b89..ff36e3ba17 100644 --- a/TUnit.Templates/content/TUnit/TestProject.csproj +++ b/TUnit.Templates/content/TUnit/TestProject.csproj @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/TUnit.TestProject.FSharp/AsyncTests.fs b/TUnit.TestProject.FSharp/AsyncTests.fs new file mode 100644 index 0000000000..675fc756d0 --- /dev/null +++ b/TUnit.TestProject.FSharp/AsyncTests.fs @@ -0,0 +1,51 @@ +namespace TUnit.TestProject.FSharp + +open System.Threading.Tasks +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.Operations +open TUnit.Core + +/// Tests to verify F# Async<'T> return types are properly executed +type AsyncTests() = + + /// Static tracker to verify tests actually execute + static member val private ExecutionCount = 0 with get, set + + [] + member _.FSharpAsync_BasicExecution() : Async = async { + AsyncTests.ExecutionCount <- 1 + do! Async.Sleep 10 + } + + [] + [] + member _.VerifyFSharpAsyncExecuted() = + // This test depends on the previous one and verifies it actually ran + if AsyncTests.ExecutionCount = 0 then + failwith "F# Async test did not execute!" + Task.CompletedTask + + [] + member _.FSharpAsync_WithReturnValue() : Async = async { + do! Async.Sleep 10 + return 42 + } + + [] + member _.FSharpAsync_WithAsyncSleep() : Async = async { + // Verify async operations work correctly + do! Async.Sleep 50 + } + + [] + member _.FSharpAsync_CallingTask() : Async = async { + // F# Async calling Task-based API + do! Task.Delay(10) |> Async.AwaitTask + } + + [] + member _.FSharpAsync_WithAssertion() : Async = async { + let result = 1 + 1 + do! check (Assert.That(result).IsEqualTo(2)) + } diff --git a/TUnit.TestProject.FSharp/TUnit.TestProject.FSharp.fsproj b/TUnit.TestProject.FSharp/TUnit.TestProject.FSharp.fsproj index 5fce858d96..a2eb49b6c7 100644 --- a/TUnit.TestProject.FSharp/TUnit.TestProject.FSharp.fsproj +++ b/TUnit.TestProject.FSharp/TUnit.TestProject.FSharp.fsproj @@ -18,6 +18,8 @@ + + diff --git a/TUnit.TestProject.FSharp/TaskAssertTests.fs b/TUnit.TestProject.FSharp/TaskAssertTests.fs new file mode 100644 index 0000000000..b2597ec104 --- /dev/null +++ b/TUnit.TestProject.FSharp/TaskAssertTests.fs @@ -0,0 +1,70 @@ +namespace TUnit.TestProject + +open System.Threading.Tasks +open TUnit.Assertions +open TUnit.Assertions.Extensions +open TUnit.Assertions.FSharp.TaskAssert +open TUnit.Core +open TUnit.TestProject.Library.Models + +type TaskAssertTests() = + + [] + [)>] + member _.IsInitialized_With_1_ClassDataSource(class1: InitializableClass) : Task = + taskAssert { + do! Assert.That(class1.IsInitialized).IsTrue() + } + + [] + [, typeof)>] + member _.IsInitialized_With_2_ClassDataSources(class1: InitializableClass, class2: InitializableClass) : Task = + taskAssert { + do! Assert.That(class1.IsInitialized).IsTrue() + do! Assert.That(class2.IsInitialized).IsTrue() + } + + [] + [, typeof, typeof)>] + member _.IsInitialized_With_3_ClassDataSources(class1: InitializableClass, class2: InitializableClass, class3: InitializableClass) : Task = + taskAssert { + do! Assert.That(class1.IsInitialized).IsTrue() + do! Assert.That(class2.IsInitialized).IsTrue() + do! Assert.That(class3.IsInitialized).IsTrue() + } + + [] + [, typeof, typeof, typeof)>] + member _.IsInitialized_With_4_ClassDataSources(class1: InitializableClass, class2: InitializableClass, class3: InitializableClass, class4: InitializableClass) : Task = + taskAssert { + do! Assert.That(class1.IsInitialized).IsTrue() + do! Assert.That(class2.IsInitialized).IsTrue() + do! Assert.That(class3.IsInitialized).IsTrue() + do! Assert.That(class4.IsInitialized).IsTrue() + } + + [] + [, typeof, typeof, typeof, typeof)>] + member _.IsInitialized_With_5_ClassDataSources(class1: InitializableClass, class2: InitializableClass, class3: InitializableClass, class4: InitializableClass, class5: InitializableClass) : Task = + taskAssert { + do! Assert.That(class1.IsInitialized).IsTrue() + do! Assert.That(class2.IsInitialized).IsTrue() + do! Assert.That(class3.IsInitialized).IsTrue() + do! Assert.That(class4.IsInitialized).IsTrue() + do! Assert.That(class5.IsInitialized).IsTrue() + } + + [] + member _.FailingAssertion_ThrowsCorrectly() : Task = + task { + let mutable exceptionThrown = false + try + do! taskAssert { + do! Assert.That(1 + 1).IsEqualTo(3) + } + with + | _ -> exceptionThrown <- true + + if not exceptionThrown then + failwith "Expected an exception to be thrown but none was thrown" + } diff --git a/TUnit.TestProject/Bugs/3266/Tests.cs b/TUnit.TestProject/Bugs/3266/Tests.cs index f18ef81aed..25dadb37fa 100644 --- a/TUnit.TestProject/Bugs/3266/Tests.cs +++ b/TUnit.TestProject/Bugs/3266/Tests.cs @@ -1,17 +1,42 @@ +using System.Collections.Concurrent; using TUnit.Core; using TUnit.Core.Interfaces; using TUnit.TestProject.Attributes; namespace TUnit.TestProject.Bugs.Bug3266; +// Global tracker to verify initialization order across all components +public static class InitializationOrderTracker +{ + private static int _counter; + private static readonly ConcurrentDictionary InitOrder = new(); + + public static int RecordInitialization(string componentName) + { + var order = Interlocked.Increment(ref _counter); + InitOrder[componentName] = order; + return order; + } + + public static int GetOrder(string componentName) => InitOrder.TryGetValue(componentName, out var order) ? order : -1; + + public static void Reset() + { + _counter = 0; + InitOrder.Clear(); + } +} + // Mock test container - shared per test session public class PulsarTestContainer : IAsyncInitializer, IAsyncDisposable { public bool IsInitialized { get; private set; } public bool IsDisposed { get; private set; } + public int InitializationOrder { get; private set; } public Task InitializeAsync() { + InitializationOrder = InitializationOrderTracker.RecordInitialization(nameof(PulsarTestContainer)); IsInitialized = true; return Task.CompletedTask; } @@ -31,6 +56,7 @@ public class PulsarConnection : IAsyncInitializer, IAsyncDisposable public bool IsInitialized { get; private set; } public bool IsDisposed { get; private set; } + public int InitializationOrder { get; private set; } public Task InitializeAsync() { @@ -41,6 +67,7 @@ public Task InitializeAsync() "PulsarConnection.InitializeAsync() called before nested Container property was initialized!"); } + InitializationOrder = InitializationOrderTracker.RecordInitialization(nameof(PulsarConnection)); IsInitialized = true; return Task.CompletedTask; } @@ -60,6 +87,7 @@ public class WebAppFactory : IAsyncInitializer, IAsyncDisposable public bool IsInitialized { get; private set; } public bool IsDisposed { get; private set; } + public int InitializationOrder { get; private set; } public Task InitializeAsync() { @@ -70,6 +98,7 @@ public Task InitializeAsync() "WebAppFactory.InitializeAsync() called before nested Container property was initialized!"); } + InitializationOrder = InitializationOrderTracker.RecordInitialization(nameof(WebAppFactory)); IsInitialized = true; return Task.CompletedTask; } @@ -130,4 +159,280 @@ await Assert.That(Connection.Container) .IsSameReferenceAs(WebApp.Container) .Because("Both properties should share the same PulsarTestContainer instance"); } + + [Test] + public async Task InitializationOrderShouldBeDependencyFirst() + { + // PulsarTestContainer must be initialized before both PulsarConnection and WebAppFactory + await Assert.That(Connection.Container.InitializationOrder) + .IsLessThan(Connection.InitializationOrder) + .Because("Container should be initialized before PulsarConnection"); + + await Assert.That(WebApp.Container.InitializationOrder) + .IsLessThan(WebApp.InitializationOrder) + .Because("Container should be initialized before WebAppFactory"); + } +} + +#region Deep Nested Dependency Chain Tests + +// Level 1: Deepest shared dependency +public class DeepDependencyLevel1 : IAsyncInitializer, IAsyncDisposable +{ + public bool IsInitialized { get; private set; } + public int InitializationOrder { get; private set; } + + public Task InitializeAsync() + { + InitializationOrder = InitializationOrderTracker.RecordInitialization($"{nameof(DeepDependencyLevel1)}_{GetHashCode()}"); + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Level 2: Depends on Level 1 +public class DeepDependencyLevel2 : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required DeepDependencyLevel1 Level1 { get; init; } + + public bool IsInitialized { get; private set; } + public int InitializationOrder { get; private set; } + + public Task InitializeAsync() + { + if (!Level1.IsInitialized) + { + throw new InvalidOperationException("Level2 initialized before Level1!"); + } + InitializationOrder = InitializationOrderTracker.RecordInitialization($"{nameof(DeepDependencyLevel2)}_{GetHashCode()}"); + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Level 3: Depends on Level 2 +public class DeepDependencyLevel3 : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required DeepDependencyLevel2 Level2 { get; init; } + + public bool IsInitialized { get; private set; } + public int InitializationOrder { get; private set; } + + public Task InitializeAsync() + { + if (!Level2.IsInitialized) + { + throw new InvalidOperationException("Level3 initialized before Level2!"); + } + if (!Level2.Level1.IsInitialized) + { + throw new InvalidOperationException("Level3 initialized before Level1!"); + } + InitializationOrder = InitializationOrderTracker.RecordInitialization($"{nameof(DeepDependencyLevel3)}_{GetHashCode()}"); + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Level 4: Depends on Level 3 (deepest level in the chain) +public class DeepDependencyLevel4 : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required DeepDependencyLevel3 Level3 { get; init; } + + public bool IsInitialized { get; private set; } + public int InitializationOrder { get; private set; } + + public Task InitializeAsync() + { + if (!Level3.IsInitialized) + { + throw new InvalidOperationException("Level4 initialized before Level3!"); + } + InitializationOrder = InitializationOrderTracker.RecordInitialization($"{nameof(DeepDependencyLevel4)}_{GetHashCode()}"); + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Abstract base class in deep chain +public abstract class DeepAbstractBase +{ + [ClassDataSource(Shared = SharedType.None)] + public required DeepDependencyLevel4 DeepDependency { get; init; } +} + +// Concrete test for 4-level deep dependency chain +[EngineTest(ExpectedResult.Pass)] +[NotInParallel] +public class DeepDependencyChainTest : DeepAbstractBase +{ + [Test] + public async Task FourLevelDeepChainShouldInitializeInCorrectOrder() + { + // Verify all levels are initialized + await Assert.That(DeepDependency.Level3.Level2.Level1.IsInitialized).IsTrue(); + await Assert.That(DeepDependency.Level3.Level2.IsInitialized).IsTrue(); + await Assert.That(DeepDependency.Level3.IsInitialized).IsTrue(); + await Assert.That(DeepDependency.IsInitialized).IsTrue(); + + // Verify initialization order: Level1 < Level2 < Level3 < Level4 + await Assert.That(DeepDependency.Level3.Level2.Level1.InitializationOrder) + .IsLessThan(DeepDependency.Level3.Level2.InitializationOrder); + + await Assert.That(DeepDependency.Level3.Level2.InitializationOrder) + .IsLessThan(DeepDependency.Level3.InitializationOrder); + + await Assert.That(DeepDependency.Level3.InitializationOrder) + .IsLessThan(DeepDependency.InitializationOrder); + } +} + +#endregion + +#region MethodDataSource with Property Injection Tests + +// A fixture that uses property injection and provides data via MethodDataSource +public class FixtureWithMethodDataSource : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required PulsarTestContainer Container { get; init; } + + public bool IsInitialized { get; private set; } + + public Task InitializeAsync() + { + if (!Container.IsInitialized) + { + throw new InvalidOperationException("FixtureWithMethodDataSource initialized before its Container!"); + } + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; + + // This method provides test data and requires the fixture to be initialized + public IEnumerable> GetTestData() + { + if (!IsInitialized) + { + throw new InvalidOperationException("GetTestData called before fixture was initialized!"); + } + yield return () => "TestValue1"; + yield return () => "TestValue2"; + } +} + +// Test that combines MethodDataSource with ClassDataSource property injection +[EngineTest(ExpectedResult.Pass)] +[NotInParallel] +public class MethodDataSourceWithPropertyInjectionTest +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required FixtureWithMethodDataSource Fixture { get; init; } + + public IEnumerable> TestData => Fixture.GetTestData(); + + [Test] + [MethodDataSource(nameof(TestData))] + public async Task TestWithMethodDataFromInjectedFixture(string testValue) + { + // If we reach here, the initialization order was correct + await Assert.That(Fixture.IsInitialized).IsTrue(); + await Assert.That(Fixture.Container.IsInitialized).IsTrue(); + await Assert.That(testValue).IsNotNullOrEmpty(); + } +} + +#endregion + +#region Multiple Abstract Base Classes with Different Sharing Modes + +// Shared per class container +public class PerClassContainer : IAsyncInitializer, IAsyncDisposable +{ + public bool IsInitialized { get; private set; } + public string InstanceId { get; } = Guid.NewGuid().ToString(); + + public Task InitializeAsync() + { + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// Dependency that uses PerClass shared container +public class PerClassDependency : IAsyncInitializer, IAsyncDisposable +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required PerClassContainer Container { get; init; } + + public bool IsInitialized { get; private set; } + + public Task InitializeAsync() + { + if (!Container.IsInitialized) + { + throw new InvalidOperationException("PerClassDependency initialized before Container!"); + } + IsInitialized = true; + return Task.CompletedTask; + } + + public ValueTask DisposeAsync() => default; +} + +// First abstract base with PerClass dependency +public abstract class AbstractWithPerClassDependency +{ + [ClassDataSource(Shared = SharedType.None)] + public required PerClassDependency PerClassDep { get; init; } +} + +// Second abstract layer with additional PerTestSession dependency +public abstract class AbstractWithMixedSharing : AbstractWithPerClassDependency +{ + [ClassDataSource(Shared = SharedType.None)] + public required PulsarConnection SessionDep { get; init; } +} + +// Test for mixed sharing modes in abstract hierarchy +[EngineTest(ExpectedResult.Pass)] +[NotInParallel] +public class MixedSharingModesInAbstractHierarchyTest : AbstractWithMixedSharing +{ + [Test] + public async Task AllDependenciesShouldBeProperlyInitialized() + { + // Verify PerClass chain + await Assert.That(PerClassDep.IsInitialized).IsTrue(); + await Assert.That(PerClassDep.Container.IsInitialized).IsTrue(); + + // Verify PerTestSession chain + await Assert.That(SessionDep.IsInitialized).IsTrue(); + await Assert.That(SessionDep.Container.IsInitialized).IsTrue(); + } + + [Test] + public async Task ContainersWithSameSharingModeAndTypeShouldBeShared() + { + // Both PulsarConnection and WebAppFactory should share the same PulsarTestContainer + // because they both use SharedType.PerTestSession for PulsarTestContainer + await Assert.That(SessionDep.Container).IsNotNull(); + } } + +#endregion diff --git a/TUnit.TestProject/Bugs/3939/Tests.cs b/TUnit.TestProject/Bugs/3939/Tests.cs new file mode 100644 index 0000000000..215a5be18c --- /dev/null +++ b/TUnit.TestProject/Bugs/3939/Tests.cs @@ -0,0 +1,53 @@ +using System.Diagnostics.CodeAnalysis; +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3939; + +/// +/// Regression test for issue #3939: IClassConstructor and ITestEndEventReceiver +/// should use the same instance so that state can be shared (e.g., DI scope disposal). +/// +public sealed class ScopedClassConstructor : IClassConstructor, ITestEndEventReceiver +{ + private object? _scope; + + public Task Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, ClassConstructorMetadata classConstructorMetadata) + { + // Simulate creating a DI scope - this should be available in OnTestEnd + _scope = new object(); + return Task.FromResult(new Tests()); + } + + public ValueTask OnTestEnd(TestContext context) + { + // This should be called on the SAME instance that Create() was called on + // If _scope is null, it means a different instance was used - this is the bug! + if (_scope == null) + { + throw new InvalidOperationException( + "Issue #3939: _scope was null in OnTestEnd(), indicating a different IClassConstructor " + + "instance was used than the one where Create() was called. " + + "IClassConstructor and ITestEndEventReceiver must use the same instance."); + } + + // Simulate disposing the scope + _scope = null; + return default; + } + + public int Order => 0; +} + +[EngineTest(ExpectedResult.Pass)] +[ClassConstructor] +public sealed class Tests +{ + [Test] + public Task TestMethod() + { + // The actual test - just verifies the test runs + // The real assertion is in OnTestEnd - it will throw if different instances are used + return Task.CompletedTask; + } +} diff --git a/TUnit.TestProject/Bugs/3951/Tests.cs b/TUnit.TestProject/Bugs/3951/Tests.cs new file mode 100644 index 0000000000..7bcb4c6e14 --- /dev/null +++ b/TUnit.TestProject/Bugs/3951/Tests.cs @@ -0,0 +1,50 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3951; + + +public sealed class MyTestGeneratorAttribute : DataSourceGeneratorAttribute where T : MyType, new() +{ + protected override IEnumerable> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) => [() => new T()]; +} + +public class MyType; + +public class ErrContext: IAsyncInitializer, IAsyncDisposable +{ + public ValueTask DisposeAsync() => default; + public Task InitializeAsync() => Task.CompletedTask; +} + +public class ErrFixture : IAsyncDisposable, IAsyncInitializer +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required ErrContext Fixture { get; set; } + public ValueTask DisposeAsync() => default; + public Task InitializeAsync() => Task.CompletedTask; +} + +[EngineTest(ExpectedResult.Pass)] +public class ErrTest +{ + [ClassDataSource>(Shared = SharedType.PerClass)] + public required ErrFixture Fixture { get; init; } + + public IEnumerable> TestExecutions => [() => Fixture.Fixture]; + + [MethodDataSource("TestExecutions")] + + [Test] + public async Task MyTest(ErrContext context) + { + await Assert.That(context.GetType()).IsNotAssignableTo(); + } + + [MyTestGeneratorAttribute] + [Test] + public async Task MyTest2(MyType t) + { + await Assert.That(t).IsAssignableTo(); + } +} diff --git a/TUnit.TestProject/Bugs/3990/ClassLevelCombinedDataSourcesTests.cs b/TUnit.TestProject/Bugs/3990/ClassLevelCombinedDataSourcesTests.cs new file mode 100644 index 0000000000..81bb4a7f5d --- /dev/null +++ b/TUnit.TestProject/Bugs/3990/ClassLevelCombinedDataSourcesTests.cs @@ -0,0 +1,153 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3990; + +/// +/// Tests for GitHub Issue #3990: Support CombinedDataSources on Class level +/// + +#region Test 1: Class-level CombinedDataSources with static Arguments + +[EngineTest(ExpectedResult.Pass)] +[CombinedDataSources] +public class ClassLevelCombinedDataSources_WithStaticArguments( + [Arguments(1, 2, 3)] int x, + [Arguments("a", "b")] string y) +{ + [Test] + public async Task Test_ShouldReceiveConstructorParameters() + { + // Should create 3 x 2 = 6 test cases + await Assert.That(x).IsIn([1, 2, 3]); + await Assert.That(y).IsIn(["a", "b"]); + } +} + +#endregion + +#region Test 2: Class-level CombinedDataSources with static MethodDataSource + +public class ClassLevelDataProviders +{ + public static IEnumerable GetNumbers() + { + yield return 10; + yield return 20; + } + + public static IEnumerable GetStrings() + { + yield return "Hello"; + yield return "World"; + } + + public static IEnumerable GetColors() + { + yield return "Red"; + yield return "Blue"; + yield return "Green"; + } +} + +[EngineTest(ExpectedResult.Pass)] +[CombinedDataSources] +public class ClassLevelCombinedDataSources_WithStaticMethodDataSource( + [MethodDataSource(nameof(ClassLevelDataProviders.GetNumbers))] int number, + [MethodDataSource(nameof(ClassLevelDataProviders.GetStrings))] string text) +{ + [Test] + public async Task Test_ShouldReceiveDataFromStaticMethods() + { + // Should create 2 x 2 = 4 test cases + await Assert.That(number).IsIn([10, 20]); + await Assert.That(text).IsIn(["Hello", "World"]); + } +} + +#endregion + +#region Test 3: Class-level CombinedDataSources mixed with method-level data sources + +[EngineTest(ExpectedResult.Pass)] +[CombinedDataSources] +public class ClassLevelCombinedDataSources_MixedWithMethodLevel( + [Arguments(1, 2)] int classArg, + [Arguments("A", "B")] string classText) +{ + [Test] + [CombinedDataSources] + public async Task Test_ShouldCombineClassAndMethodData( + [Arguments(100, 200)] int methodArg, + [Arguments("X", "Y")] string methodText) + { + // Class: 2 x 2 = 4 combinations + // Method: 2 x 2 = 4 combinations + // Total: 4 x 4 = 16 test cases + await Assert.That(classArg).IsIn([1, 2]); + await Assert.That(classText).IsIn(["A", "B"]); + await Assert.That(methodArg).IsIn([100, 200]); + await Assert.That(methodText).IsIn(["X", "Y"]); + } +} + +#endregion + +#region Test 4: Class-level CombinedDataSources with mixed data source types + +[EngineTest(ExpectedResult.Pass)] +[CombinedDataSources] +public class ClassLevelCombinedDataSources_MixedDataSourceTypes( + [Arguments(1, 2)] int number, + [MethodDataSource(nameof(ClassLevelDataProviders.GetColors))] string color) +{ + [Test] + public async Task Test_ShouldMixArgumentsAndMethodDataSource() + { + // Should create 2 x 3 = 6 test cases + await Assert.That(number).IsIn([1, 2]); + await Assert.That(color).IsIn(["Red", "Blue", "Green"]); + } +} + +#endregion + +#region Test 5: Class-level CombinedDataSources with three constructor parameters + +[EngineTest(ExpectedResult.Pass)] +[CombinedDataSources] +public class ClassLevelCombinedDataSources_ThreeParameters( + [Arguments(1, 2)] int x, + [Arguments("a", "b")] string y, + [Arguments(true, false)] bool z) +{ + [Test] + public async Task Test_ShouldHandleThreeParameters() + { + // Should create 2 x 2 x 2 = 8 test cases + await Assert.That(x).IsIn([1, 2]); + await Assert.That(y).IsIn(["a", "b"]); + await Assert.That(z).IsIn([true, false]); + } +} + +#endregion + +#region Edge Case: Instance data source at class level + +/// +/// Edge case documentation: When using CombinedDataSources at the class level with +/// an instance-requiring data source (like MethodDataSource pointing to an instance method), +/// the runtime check in CombinedDataSourcesAttribute.GetParameterValues() (lines 156-165) +/// will throw an InvalidOperationException with a clear, diagnostic error message: +/// +/// "Cannot use instance-based data source '{AttributeName}' on parameter '{name}' in class '{ClassName}'. +/// When [CombinedDataSources] is applied at the class level (constructor parameters), all data sources +/// must be static because no instance exists yet. Use static [MethodDataSource] or [Arguments] instead, +/// or move [CombinedDataSources] to the method level if you need instance-based data sources." +/// +/// This provides proper error handling with actionable guidance, replacing the confusing +/// "circular dependency" message that was previously shown due to the blanket IAccessesInstanceData check. +/// +public class EdgeCaseDocumentation; + +#endregion diff --git a/TUnit.TestProject/Bugs/3992/IAsyncInitializerDiscoveryTests.cs b/TUnit.TestProject/Bugs/3992/IAsyncInitializerDiscoveryTests.cs new file mode 100644 index 0000000000..b0c579b0ac --- /dev/null +++ b/TUnit.TestProject/Bugs/3992/IAsyncInitializerDiscoveryTests.cs @@ -0,0 +1,41 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3992; + +/// +/// Regression test for issue #3992: IAsyncInitializer should not run during test discovery. +/// Verifies that IAsyncInitializer.InitializeAsync() is only called during test execution, +/// not during the discovery phase. +/// +[EngineTest(ExpectedResult.Pass)] +public class IAsyncInitializerDiscoveryTests +{ + private static int _initializationCount = 0; + + public class DataSourceWithAsyncInit : IAsyncInitializer + { + public bool IsInitialized { get; private set; } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializationCount); + IsInitialized = true; + Console.WriteLine($"InitializeAsync called (count: {_initializationCount})"); + return Task.CompletedTask; + } + } + + [Test] + [ClassDataSource] + public async Task DataSource_InitializeAsync_OnlyCalledDuringExecution(DataSourceWithAsyncInit dataSource) + { + // Verify that the data source was initialized + await Assert.That(dataSource.IsInitialized).IsTrue(); + + // Verify that InitializeAsync was called (at least once during execution) + await Assert.That(_initializationCount).IsGreaterThan(0); + + Console.WriteLine($"Test execution confirmed: InitializeAsync was called {_initializationCount} time(s)"); + } +} diff --git a/TUnit.TestProject/Bugs/3992/InstanceMethodDataSourceWithAsyncInitializerTests.cs b/TUnit.TestProject/Bugs/3992/InstanceMethodDataSourceWithAsyncInitializerTests.cs new file mode 100644 index 0000000000..733b3b6b72 --- /dev/null +++ b/TUnit.TestProject/Bugs/3992/InstanceMethodDataSourceWithAsyncInitializerTests.cs @@ -0,0 +1,132 @@ +using System.Collections.Concurrent; +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3992; + +/// +/// Regression test for issue #3992: IAsyncInitializer should not run during test discovery +/// when using InstanceMethodDataSource with ClassDataSource. +/// +/// This test replicates the user's scenario where: +/// 1. A ClassDataSource fixture implements IAsyncInitializer (e.g., starts Docker containers) +/// 2. An InstanceMethodDataSource returns predefined test case identifiers +/// 3. The fixture should NOT be initialized during discovery - only during execution +/// +/// The key insight is that test case IDENTIFIERS are known ahead of time (predefined), +/// but the actual fixture initialization (Docker containers, DB connections, etc.) +/// should only happen when tests actually execute. +/// +/// The bug caused Docker containers to start during test discovery (e.g., in IDE or --list-tests), +/// which was unexpected and resource-intensive. +/// +[EngineTest(ExpectedResult.Pass)] +public class InstanceMethodDataSourceWithAsyncInitializerTests +{ + private static int _initializationCount; + private static int _testExecutionCount; + private static readonly ConcurrentBag _observedInstanceIds = []; + + /// + /// Simulates a fixture like ClientServiceFixture that starts Docker containers. + /// Implements IAsyncInitializer (NOT IAsyncDiscoveryInitializer) because the user + /// does not want initialization during discovery. + /// + public class SimulatedContainerFixture : IAsyncInitializer + { + /// + /// Test case identifiers are PREDEFINED - they don't depend on initialization. + /// This allows discovery to enumerate test cases without initializing the fixture. + /// + private static readonly string[] PredefinedTestCases = ["TestCase1", "TestCase2", "TestCase3"]; + + /// + /// Unique identifier for this instance to verify sharing behavior. + /// + public Guid InstanceId { get; } = Guid.NewGuid(); + + public bool IsInitialized { get; private set; } + + /// + /// Returns predefined test case identifiers. These are available during discovery + /// WITHOUT requiring initialization. + /// + public IEnumerable GetTestCases() => PredefinedTestCases; + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializationCount); + Console.WriteLine($"[SimulatedContainerFixture] InitializeAsync called on instance {InstanceId} (count: {_initializationCount})"); + + // Simulate expensive container startup - this should NOT happen during discovery + IsInitialized = true; + + return Task.CompletedTask; + } + } + + [ClassDataSource(Shared = SharedType.PerClass)] + public required SimulatedContainerFixture Fixture { get; init; } + + /// + /// This property is accessed by InstanceMethodDataSource during discovery. + /// It returns predefined test case identifiers that don't require initialization. + /// The bug was that accessing this would trigger InitializeAsync() during discovery. + /// After the fix, InitializeAsync() should only be called during test execution. + /// + public IEnumerable TestExecutions => Fixture.GetTestCases(); + + [Test] + [InstanceMethodDataSource(nameof(TestExecutions))] + public async Task Test_WithInstanceMethodDataSource_DoesNotInitializeDuringDiscovery(string testCase) + { + Interlocked.Increment(ref _testExecutionCount); + + // Track this instance to verify sharing + _observedInstanceIds.Add(Fixture.InstanceId); + + // The fixture should be initialized by the time the test runs + await Assert.That(Fixture.IsInitialized) + .IsTrue() + .Because("the fixture should be initialized before test execution"); + + await Assert.That(testCase) + .IsNotNullOrEmpty() + .Because("the test case data should be available"); + + Console.WriteLine($"[Test] Executed with testCase='{testCase}', instanceId={Fixture.InstanceId}, " + + $"initCount={_initializationCount}, execCount={_testExecutionCount}"); + } + + [After(Class)] + public static async Task VerifyInitializationAndSharing() + { + // With SharedType.PerClass, the fixture should be initialized exactly ONCE + // during test execution, NOT during discovery. + // + // Before the fix: _initializationCount would be 2+ (discovery + execution) + // After the fix: _initializationCount should be exactly 1 (execution only) + + Console.WriteLine($"[After(Class)] Final counts - init: {_initializationCount}, exec: {_testExecutionCount}"); + Console.WriteLine($"[After(Class)] Unique instance IDs observed: {_observedInstanceIds.Distinct().Count()}"); + + await Assert.That(_initializationCount) + .IsEqualTo(1) + .Because("IAsyncInitializer should only be called once during execution, not during discovery"); + + await Assert.That(_testExecutionCount) + .IsEqualTo(3) + .Because("there should be 3 test executions (one per test case)"); + + // Verify that all tests used the SAME fixture instance (SharedType.PerClass) + var uniqueInstanceIds = _observedInstanceIds.Distinct().ToList(); + await Assert.That(uniqueInstanceIds) + .HasCount().EqualTo(1) + .Because("with SharedType.PerClass, all tests should share the same fixture instance"); + + // Reset for next run + _initializationCount = 0; + _testExecutionCount = 0; + _observedInstanceIds.Clear(); + } +} diff --git a/TUnit.TestProject/Bugs/3992/RuntimeInitializeTests.cs b/TUnit.TestProject/Bugs/3992/RuntimeInitializeTests.cs new file mode 100644 index 0000000000..1d86987a8a --- /dev/null +++ b/TUnit.TestProject/Bugs/3992/RuntimeInitializeTests.cs @@ -0,0 +1,56 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3992; + +/// +/// Once this is discovered during test discovery, containers spin up +/// +[EngineTest(ExpectedResult.Pass)] +public sealed class RuntimeInitializeTests +{ + //Docker container + [ClassDataSource(Shared = SharedType.PerClass)] + public required DummyContainer Container { get; init; } + + [Before(Class)] + public static Task BeforeClass(ClassHookContext context) => NotInitialised(context.Tests); + + [After(TestDiscovery)] + public static Task AfterDiscovery(TestDiscoveryContext context) => NotInitialised(context.AllTests); + + public static async Task NotInitialised(IEnumerable tests) + { + var bugRecreations = tests.Select(x => x.Metadata.TestDetails.ClassInstance).OfType(); + + foreach (var bugRecreation in bugRecreations) + { + await Assert.That(bugRecreation.Container).IsNotNull(); + await Assert.That(DummyContainer.NumberOfInits).IsEqualTo(0); + } + } + + [Test, Arguments(1)] + public async Task Test(int value, CancellationToken token) + { + await Assert.That(value).IsNotDefault(); + await Assert.That(DummyContainer.NumberOfInits).IsEqualTo(1); + } + + public class DummyContainer : IAsyncInitializer, IAsyncDisposable + { + public Task InitializeAsync() + { + NumberOfInits++; + return Task.CompletedTask; + } + + public static int NumberOfInits { get; private set; } + + public ValueTask DisposeAsync() + { + return default; + } + } + +} diff --git a/TUnit.TestProject/Bugs/3992/RuntimeInitializeTests2.cs b/TUnit.TestProject/Bugs/3992/RuntimeInitializeTests2.cs new file mode 100644 index 0000000000..4edc097c47 --- /dev/null +++ b/TUnit.TestProject/Bugs/3992/RuntimeInitializeTests2.cs @@ -0,0 +1,56 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3992; + +/// +/// Once this is discovered during test discovery, containers spin up +/// +[EngineTest(ExpectedResult.Pass)] +public sealed class DiscoveryInitializeTests +{ + //Docker container + [ClassDataSource(Shared = SharedType.PerClass)] + public required DummyContainer Container { get; init; } + + [Before(Class)] + public static Task BeforeClass(ClassHookContext context) => Initialised(context.Tests); + + [After(TestDiscovery)] + public static Task AfterDiscovery(TestDiscoveryContext context) => Initialised(context.AllTests); + + public static async Task Initialised(IEnumerable tests) + { + var bugRecreations = tests.Select(x => x.Metadata.TestDetails.ClassInstance).OfType(); + + foreach (var bugRecreation in bugRecreations) + { + await Assert.That(bugRecreation.Container).IsNotNull(); + await Assert.That(DummyContainer.NumberOfInits).IsEqualTo(1); + } + } + + [Test, Arguments(1)] + public async Task Test(int value, CancellationToken token) + { + await Assert.That(value).IsNotDefault(); + await Assert.That(DummyContainer.NumberOfInits).IsEqualTo(1); + } + + public class DummyContainer : IAsyncDiscoveryInitializer, IAsyncDisposable + { + public Task InitializeAsync() + { + NumberOfInits++; + return Task.CompletedTask; + } + + public static int NumberOfInits { get; private set; } + + public ValueTask DisposeAsync() + { + return default; + } + } + +} diff --git a/TUnit.TestProject/Bugs/3993/IAsyncInitializerTests.cs b/TUnit.TestProject/Bugs/3993/IAsyncInitializerTests.cs new file mode 100644 index 0000000000..58b07b362a --- /dev/null +++ b/TUnit.TestProject/Bugs/3993/IAsyncInitializerTests.cs @@ -0,0 +1,62 @@ +using System.Collections.Concurrent; +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3993; + +/// +/// Regression test for issue #3993: IAsyncInitializer called 4 times instead of 3 +/// when using ClassDataSource with MethodDataSource that accesses instance properties. +/// +[EngineTest(ExpectedResult.Pass)] +public class IAsyncInitializerTests +{ + [ClassDataSource(Shared = SharedType.None)] + public required PerTestCaseSource PerTestCaseSource { get; init; } + + public IEnumerable PerTestCaseStrings() => PerTestCaseSource.MyString; + + [Test] + [MethodDataSource(nameof(PerTestCaseStrings))] + public async Task ForNTestCases_ShouldInitNTimes(string s) + { + await Assert.That(s).IsNotNull(); + + // Each of the 3 test cases should have its own isolated PerTestCaseSource instance + // Since Shared = SharedType.None, each test gets a new instance + // Therefore, InitializeAsync should be called exactly 3 times (once per test) + // NOT 4 times (which would include the discovery instance) + await Assert + .That(PerTestCaseSource.SetUps) + .IsEqualTo(3) + .Because("each of the 3 test cases should be wrapped in its own isolated test class, " + + "causing a call to async init, but no more than those three calls should be needed. " + + "The discovery-time instance should NOT be initialized."); + } + + [After(Class)] + public static void ResetCounter() + { + // Reset for next run + PerTestCaseSource.Reset(); + } +} + +public class PerTestCaseSource : IAsyncInitializer +{ + public string[] MyString = []; + private static int _setUps = 0; + public static int SetUps => _setUps; + + public Task InitializeAsync() + { + Interlocked.Increment(ref _setUps); + MyString = ["123", "321", "9210"]; + return Task.CompletedTask; + } + + public static void Reset() + { + _setUps = 0; + } +} diff --git a/TUnit.TestProject/Bugs/3997/Tests.cs b/TUnit.TestProject/Bugs/3997/Tests.cs new file mode 100644 index 0000000000..9fe62ef189 --- /dev/null +++ b/TUnit.TestProject/Bugs/3997/Tests.cs @@ -0,0 +1,58 @@ +using TUnit.Core; +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._3997; + +/// +/// Simulates a data source (like a Docker container) that needs initialization during test discovery +/// so that InstanceMethodDataSource can access its data when generating test cases. +/// +public class SimulatedContainer : IAsyncDiscoveryInitializer +{ + private readonly List _data = []; + public bool IsInitialized { get; private set; } + + public IReadOnlyList Data => _data; + + public Task InitializeAsync() + { + if (IsInitialized) + { + throw new InvalidOperationException("Container already initialized! InitializeAsync should only be called once."); + } + + // Simulate container startup and data population + _data.AddRange(["TestCase1", "TestCase2", "TestCase3"]); + IsInitialized = true; + return Task.CompletedTask; + } +} + +/// +/// Tests that IAsyncDiscoveryInitializer is called during test discovery, +/// allowing InstanceMethodDataSource to access initialized data. +/// +[EngineTest(ExpectedResult.Pass)] +public class DiscoveryInitializerTests +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required SimulatedContainer Container { get; init; } + + /// + /// This property provides test data from the initialized container. + /// The container MUST be initialized during discovery before this is evaluated. + /// + public IEnumerable TestCases => Container.Data; + + [Test] + [InstanceMethodDataSource(nameof(TestCases))] + public async Task TestWithContainerData(string testCase) + { + // Container should be initialized + await Assert.That(Container.IsInitialized).IsTrue(); + + // testCase should be one of the container's data items + await Assert.That(Container.Data).Contains(testCase); + } +} diff --git a/TUnit.TestProject/Bugs/4032/NestedAsyncInitializerTests.cs b/TUnit.TestProject/Bugs/4032/NestedAsyncInitializerTests.cs new file mode 100644 index 0000000000..f04d018ba2 --- /dev/null +++ b/TUnit.TestProject/Bugs/4032/NestedAsyncInitializerTests.cs @@ -0,0 +1,417 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._4032; + +/// +/// Regression tests for issue #4032: IAsyncInitializer lifecycle change broke complex test infrastructure orchestration. +/// The problem was that nested IAsyncInitializer dependencies weren't initialized before the parent's InitializeAsync was called. +/// This broke patterns like WebApplicationFactory + Testcontainers where the container must be initialized +/// before the WebApplicationFactory tries to access the container's connection string. +/// + +#region Mock Classes + +/// +/// Simulates a Testcontainers PostgreSQL container. +/// Must be initialized before any parent object tries to access GetConnectionString(). +/// +public class MockDatabaseContainer : IAsyncInitializer +{ + private static int _instanceCount; + private static int _initializeCount; + private static readonly List _initializationOrder = []; + + public static int InstanceCount => _instanceCount; + public static int InitializeCount => _initializeCount; + public static IReadOnlyList InitializationOrder => _initializationOrder; + + public int InstanceId { get; } + public bool IsInitialized { get; private set; } + public string? ConnectionString { get; private set; } + + public MockDatabaseContainer() + { + InstanceId = Interlocked.Increment(ref _instanceCount); + } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializeCount); + IsInitialized = true; + ConnectionString = $"Host=localhost;Database=test_{InstanceId};User=test;Password=test"; + + lock (_initializationOrder) + { + _initializationOrder.Add(InstanceId); + } + + Console.WriteLine($"[MockDatabaseContainer #{InstanceId}] InitializeAsync called - ConnectionString: {ConnectionString}"); + return Task.CompletedTask; + } + + public string GetConnectionString() + { + if (!IsInitialized || ConnectionString == null) + { + throw new InvalidOperationException( + $"MockDatabaseContainer #{InstanceId} is not initialized! " + + "This indicates a bug in the IAsyncInitializer lifecycle ordering. " + + "Nested dependencies must be initialized before parent objects."); + } + + return ConnectionString; + } + + public static void Reset() + { + _instanceCount = 0; + _initializeCount = 0; + lock (_initializationOrder) + { + _initializationOrder.Clear(); + } + } +} + +/// +/// Simulates a WebApplicationFactory that depends on a database container. +/// The container MUST be initialized before this class's InitializeAsync is called, +/// because InitializeAsync accesses the container's connection string. +/// +public class MockWebApplicationFactory : IAsyncInitializer +{ + private static int _instanceCount; + private static int _initializeCount; + + public static int InstanceCount => _instanceCount; + public static int InitializeCount => _initializeCount; + + public int InstanceId { get; } + public bool IsInitialized { get; private set; } + public string? ConfiguredConnectionString { get; private set; } + + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required MockDatabaseContainer Database { get; init; } + + public MockWebApplicationFactory() + { + InstanceId = Interlocked.Increment(ref _instanceCount); + } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializeCount); + + // This is the critical part: we need to access the container's connection string + // during initialization. If the container isn't initialized yet, this will throw. + ConfiguredConnectionString = Database.GetConnectionString(); + IsInitialized = true; + + Console.WriteLine($"[MockWebApplicationFactory #{InstanceId}] InitializeAsync called - Using connection: {ConfiguredConnectionString}"); + return Task.CompletedTask; + } + + public static void Reset() + { + _instanceCount = 0; + _initializeCount = 0; + } +} + +#endregion + +#region Tests + +/// +/// Tests that nested IAsyncInitializer dependencies are initialized before the parent's InitializeAsync. +/// This is the exact pattern from issue #4032. +/// +[NotInParallel] +[EngineTest(ExpectedResult.Pass)] +public class NestedAsyncInitializerOrderTests +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required MockWebApplicationFactory Factory { get; init; } + + [Before(Class)] + public static void ResetCounters() + { + MockDatabaseContainer.Reset(); + MockWebApplicationFactory.Reset(); + } + + [Test] + public async Task Factory_ShouldBeInitialized() + { + // The factory should be initialized (which means the database must have been initialized first) + await Assert.That(Factory.IsInitialized).IsTrue() + .Because("the factory's InitializeAsync should have been called"); + } + + [Test] + public async Task Factory_ShouldHaveConfiguredConnectionString() + { + // The factory should have captured the connection string during initialization + await Assert.That(Factory.ConfiguredConnectionString).IsNotNull() + .Because("the factory should have accessed the database connection string during InitializeAsync"); + } + + [Test] + public async Task Database_ShouldBeInitializedBeforeFactory() + { + // The database should be initialized + await Assert.That(Factory.Database.IsInitialized).IsTrue() + .Because("the database container should be initialized before the factory accesses it"); + } + + [Test] + public async Task Database_InitializeCount_ShouldBeOne() + { + // The database should only be initialized once (shared per test session) + await Assert.That(MockDatabaseContainer.InitializeCount).IsEqualTo(1) + .Because("the database container is shared PerTestSession and should only be initialized once"); + } + + [Test] + public async Task Factory_InitializeCount_ShouldBeOne() + { + // The factory should only be initialized once (shared per test session) + await Assert.That(MockWebApplicationFactory.InitializeCount).IsEqualTo(1) + .Because("the factory is shared PerTestSession and should only be initialized once"); + } +} + +/// +/// Tests deep nesting (3 levels) of IAsyncInitializer dependencies. +/// Uses a shared static list to track initialization order reliably. +/// +public static class DeepNestingInitializationTracker +{ + private static readonly List _initializationOrder = []; + private static readonly object _lock = new(); + + public static void RecordInitialization(string name) + { + lock (_lock) + { + _initializationOrder.Add(name); + } + } + + public static IReadOnlyList GetOrder() + { + lock (_lock) + { + return _initializationOrder.ToList(); + } + } + + public static void Reset() + { + lock (_lock) + { + _initializationOrder.Clear(); + } + } +} + +public class DeepNestedDependency : IAsyncInitializer +{ + private static int _instanceCount; + private static int _initializeCount; + + public static int InstanceCount => _instanceCount; + public static int InitializeCount => _initializeCount; + + public int InstanceId { get; } + public bool IsInitialized { get; private set; } + public string? Value { get; private set; } + + public DeepNestedDependency() + { + InstanceId = Interlocked.Increment(ref _instanceCount); + } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializeCount); + IsInitialized = true; + Value = $"DeepValue_{InstanceId}"; + DeepNestingInitializationTracker.RecordInitialization("Deep"); + Console.WriteLine($"[DeepNestedDependency #{InstanceId}] InitializeAsync called"); + return Task.CompletedTask; + } + + public string GetValue() + { + if (!IsInitialized || Value == null) + { + throw new InvalidOperationException($"DeepNestedDependency #{InstanceId} is not initialized!"); + } + + return Value; + } + + public static void Reset() + { + _instanceCount = 0; + _initializeCount = 0; + } +} + +public class MiddleDependency : IAsyncInitializer +{ + private static int _instanceCount; + private static int _initializeCount; + + public static int InstanceCount => _instanceCount; + public static int InitializeCount => _initializeCount; + + public int InstanceId { get; } + public bool IsInitialized { get; private set; } + public string? CombinedValue { get; private set; } + + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required DeepNestedDependency DeepDependency { get; init; } + + public MiddleDependency() + { + InstanceId = Interlocked.Increment(ref _instanceCount); + } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializeCount); + + // Access the deep dependency - must be initialized first + CombinedValue = $"Middle_{InstanceId}_{DeepDependency.GetValue()}"; + IsInitialized = true; + DeepNestingInitializationTracker.RecordInitialization("Middle"); + Console.WriteLine($"[MiddleDependency #{InstanceId}] InitializeAsync called - CombinedValue: {CombinedValue}"); + return Task.CompletedTask; + } + + public string GetCombinedValue() + { + if (!IsInitialized || CombinedValue == null) + { + throw new InvalidOperationException($"MiddleDependency #{InstanceId} is not initialized!"); + } + + return CombinedValue; + } + + public static void Reset() + { + _instanceCount = 0; + _initializeCount = 0; + } +} + +public class TopLevelDependency : IAsyncInitializer +{ + private static int _instanceCount; + private static int _initializeCount; + + public static int InstanceCount => _instanceCount; + public static int InitializeCount => _initializeCount; + + public int InstanceId { get; } + public bool IsInitialized { get; private set; } + public string? FinalValue { get; private set; } + + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required MiddleDependency MiddleDependency { get; init; } + + public TopLevelDependency() + { + InstanceId = Interlocked.Increment(ref _instanceCount); + } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _initializeCount); + + // Access the middle dependency - must be initialized first + FinalValue = $"Top_{InstanceId}_{MiddleDependency.GetCombinedValue()}"; + IsInitialized = true; + DeepNestingInitializationTracker.RecordInitialization("Top"); + Console.WriteLine($"[TopLevelDependency #{InstanceId}] InitializeAsync called - FinalValue: {FinalValue}"); + return Task.CompletedTask; + } + + public static void Reset() + { + _instanceCount = 0; + _initializeCount = 0; + } +} + +/// +/// Tests 3-level deep nesting of IAsyncInitializer dependencies. +/// Order must be: DeepNestedDependency -> MiddleDependency -> TopLevelDependency +/// +[NotInParallel] +[EngineTest(ExpectedResult.Pass)] +public class DeepNestedAsyncInitializerOrderTests +{ + [ClassDataSource(Shared = SharedType.PerTestSession)] + public required TopLevelDependency TopLevel { get; init; } + + [Before(Class)] + public static void ResetCounters() + { + DeepNestedDependency.Reset(); + MiddleDependency.Reset(); + TopLevelDependency.Reset(); + DeepNestingInitializationTracker.Reset(); + } + + [Test] + public async Task AllLevels_ShouldBeInitialized() + { + await Assert.That(TopLevel.IsInitialized).IsTrue(); + await Assert.That(TopLevel.MiddleDependency.IsInitialized).IsTrue(); + await Assert.That(TopLevel.MiddleDependency.DeepDependency.IsInitialized).IsTrue(); + } + + [Test] + public async Task InitializationOrder_ShouldBeDeepestFirst() + { + // Get the recorded initialization order + var order = DeepNestingInitializationTracker.GetOrder(); + + // Deep should be initialized first, then Middle, then Top + await Assert.That(order).HasCount().EqualTo(3) + .Because("there should be exactly 3 initializations"); + + await Assert.That(order[0]).IsEqualTo("Deep") + .Because("the deepest dependency should be initialized first"); + + await Assert.That(order[1]).IsEqualTo("Middle") + .Because("the middle dependency should be initialized second"); + + await Assert.That(order[2]).IsEqualTo("Top") + .Because("the top-level dependency should be initialized last"); + } + + [Test] + public async Task FinalValue_ShouldContainAllLevels() + { + // The final value should contain data from all three levels + await Assert.That(TopLevel.FinalValue).Contains("Top_"); + await Assert.That(TopLevel.FinalValue).Contains("Middle_"); + await Assert.That(TopLevel.FinalValue).Contains("DeepValue_"); + } + + [Test] + public async Task EachLevel_ShouldBeInitializedOnce() + { + await Assert.That(DeepNestedDependency.InitializeCount).IsEqualTo(1); + await Assert.That(MiddleDependency.InitializeCount).IsEqualTo(1); + await Assert.That(TopLevelDependency.InitializeCount).IsEqualTo(1); + } +} + +#endregion diff --git a/TUnit.TestProject/Bugs/4049/InitializerExceptionPropagationTests.cs b/TUnit.TestProject/Bugs/4049/InitializerExceptionPropagationTests.cs new file mode 100644 index 0000000000..c2b0bb4cbf --- /dev/null +++ b/TUnit.TestProject/Bugs/4049/InitializerExceptionPropagationTests.cs @@ -0,0 +1,31 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._4049; + +/// +/// Tests that exceptions thrown during IAsyncInitializer.InitializeAsync +/// are properly propagated and cause tests to fail. +/// See: https://github.com/thomhurst/TUnit/issues/4049 +/// +[EngineTest(ExpectedResult.Failure)] +[ClassDataSource(Shared = SharedType.None)] +public class InitializerExceptionPropagationTests( + InitializerExceptionPropagationTests.FailingInitializerFactory factory) +{ + [Test] + public void Test_Should_Fail_Due_To_Initializer_Exception() + { + // This test should never run - it should fail during initialization + // because the IAsyncInitializer.InitializeAsync throws + throw new InvalidOperationException("This test should not have executed - initializer should have thrown"); + } + + public class FailingInitializerFactory : IAsyncInitializer + { + public Task InitializeAsync() + { + throw new InvalidOperationException("Simulated initialization failure (e.g., Docker container failed to start)"); + } + } +} diff --git a/TUnit.TestProject/Bugs/4049/NestedInitializerExceptionPropagationTests.cs b/TUnit.TestProject/Bugs/4049/NestedInitializerExceptionPropagationTests.cs new file mode 100644 index 0000000000..2bc3947a74 --- /dev/null +++ b/TUnit.TestProject/Bugs/4049/NestedInitializerExceptionPropagationTests.cs @@ -0,0 +1,43 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._4049; + +/// +/// Tests that exceptions thrown during nested IAsyncInitializer property access +/// are properly propagated and cause tests to fail rather than running with null properties. +/// See: https://github.com/thomhurst/TUnit/issues/4049 +/// +[EngineTest(ExpectedResult.Failure)] +[ClassDataSource(Shared = SharedType.None)] +public class NestedInitializerExceptionPropagationTests( + NestedInitializerExceptionPropagationTests.FailingNestedInitializerFactory factory) +{ + [Test] + public void Test_Should_Fail_Due_To_Nested_Initializer_Exception() + { + // This test should never run - it should fail during discovery/initialization + // because the nested IAsyncInitializer throws during property access + throw new InvalidOperationException("This test should not have executed - nested initializer should have thrown"); + } + + public class FailingNestedInitializerFactory : IAsyncInitializer + { + // This property throws when accessed, simulating a container that fails to start + public FailingNestedInitializer NestedInitializer => + throw new InvalidOperationException("Simulated container startup failure"); + + public Task InitializeAsync() + { + return Task.CompletedTask; + } + } + + public class FailingNestedInitializer : IAsyncInitializer + { + public Task InitializeAsync() + { + return Task.CompletedTask; + } + } +} diff --git a/TUnit.TestProject/Bugs/4049/PropertyGetterSideEffectTests.cs b/TUnit.TestProject/Bugs/4049/PropertyGetterSideEffectTests.cs new file mode 100644 index 0000000000..35c206fee4 --- /dev/null +++ b/TUnit.TestProject/Bugs/4049/PropertyGetterSideEffectTests.cs @@ -0,0 +1,132 @@ +using TUnit.Core.Interfaces; +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._4049; + +/// +/// Regression test for issue #4049: Property getters should not be called during discovery +/// for properties that don't return IAsyncInitializer. +/// +/// This simulates the WebApplicationFactory.Server scenario where accessing a property +/// causes side effects (like starting a test server) before the object is configured. +/// +[EngineTest(ExpectedResult.Pass)] +public class PropertyGetterSideEffectTests +{ + private static int _sideEffectCount = 0; + + /// + /// Simulates a class like WebApplicationFactory where accessing certain properties + /// triggers initialization (like building the test host). + /// + public class FixtureWithSideEffects : IAsyncInitializer + { + private object? _server; + + /// + /// Simulates WebApplicationFactory.Server - accessing this property has side effects. + /// This property type is 'object', NOT IAsyncInitializer, so TUnit should NOT access it. + /// + public object Server + { + get + { + Interlocked.Increment(ref _sideEffectCount); + Console.WriteLine($"Server getter called! Side effect count: {_sideEffectCount}"); + _server ??= new object(); // Side effect: lazy initialization + return _server; + } + } + + public bool IsInitialized { get; private set; } + + public Task InitializeAsync() + { + IsInitialized = true; + Console.WriteLine("FixtureWithSideEffects.InitializeAsync called"); + return Task.CompletedTask; + } + } + + [Test] + [ClassDataSource] + public async Task PropertyGetters_WithSideEffects_ShouldNotBeCalledDuringDiscovery(FixtureWithSideEffects fixture) + { + // Verify the fixture was properly initialized + await Assert.That(fixture.IsInitialized).IsTrue(); + + // The Server property getter should NOT have been called during discovery. + // It should only be called if the test explicitly accesses it. + // At this point, we haven't accessed Server, so count should be 0. + await Assert.That(_sideEffectCount).IsEqualTo(0) + .Because("TUnit should not access non-IAsyncInitializer property getters during discovery"); + + // Now let's explicitly access the Server to verify it works + var server = fixture.Server; + await Assert.That(server).IsNotNull(); + await Assert.That(_sideEffectCount).IsEqualTo(1); + + Console.WriteLine($"Test passed: Server getter was only called when explicitly accessed (count: {_sideEffectCount})"); + } +} + +/// +/// Test to verify that nested IAsyncInitializer properties ARE still discovered +/// when they are properly typed. +/// +[EngineTest(ExpectedResult.Pass)] +public class NestedAsyncInitializerDiscoveryTests +{ + private static int _parentInitCount = 0; + private static int _childInitCount = 0; + + public class ChildInitializer : IAsyncInitializer + { + public bool IsInitialized { get; private set; } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _childInitCount); + IsInitialized = true; + Console.WriteLine($"ChildInitializer.InitializeAsync called (count: {_childInitCount})"); + return Task.CompletedTask; + } + } + + public class ParentWithNestedInitializer : IAsyncInitializer + { + /// + /// This property IS typed as IAsyncInitializer, so TUnit SHOULD access it + /// and discover the nested initializer for proper lifecycle management. + /// + public IAsyncInitializer NestedInitializer { get; } = new ChildInitializer(); + + public bool IsInitialized { get; private set; } + + public Task InitializeAsync() + { + Interlocked.Increment(ref _parentInitCount); + IsInitialized = true; + Console.WriteLine($"ParentWithNestedInitializer.InitializeAsync called (count: {_parentInitCount})"); + return Task.CompletedTask; + } + } + + [Test] + [ClassDataSource] + public async Task NestedInitializers_WhenProperlyTyped_ShouldBeDiscoveredAndInitialized(ParentWithNestedInitializer parent) + { + // Verify parent was initialized + await Assert.That(parent.IsInitialized).IsTrue(); + await Assert.That(_parentInitCount).IsGreaterThan(0); + + // Verify child was discovered and initialized + var child = parent.NestedInitializer as ChildInitializer; + await Assert.That(child).IsNotNull(); + await Assert.That(child!.IsInitialized).IsTrue() + .Because("TUnit should discover and initialize properly-typed IAsyncInitializer properties"); + await Assert.That(_childInitCount).IsGreaterThan(0); + + Console.WriteLine($"Test passed: Both parent ({_parentInitCount}) and child ({_childInitCount}) were initialized"); + } +} diff --git a/TUnit.TestProject/Bugs/4065/BugRepro4065.cs b/TUnit.TestProject/Bugs/4065/BugRepro4065.cs new file mode 100644 index 0000000000..8d64cf14bc --- /dev/null +++ b/TUnit.TestProject/Bugs/4065/BugRepro4065.cs @@ -0,0 +1,38 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.Bugs._4065; + +public enum MyEnum +{ + One, + Two, + Three +} + +[EngineTest(ExpectedResult.Pass)] +public class BugRepro4065 +{ + [Test] + [Arguments((MyEnum[])[MyEnum.One, MyEnum.Two])] + public async Task EnumArrayWithCollectionExpression(MyEnum[] values) + { + await Assert.That(values.Length).IsEqualTo(2); + await Assert.That(values[0]).IsEqualTo(MyEnum.One); + await Assert.That(values[1]).IsEqualTo(MyEnum.Two); + } + + [Test] + [Arguments((MyEnum[])[MyEnum.Three])] + public async Task EnumArraySingleElement(MyEnum[] values) + { + await Assert.That(values.Length).IsEqualTo(1); + await Assert.That(values[0]).IsEqualTo(MyEnum.Three); + } + + [Test] + [Arguments(new MyEnum[] { MyEnum.One, MyEnum.Two })] // Old syntax (should still work) + public async Task EnumArrayWithNewSyntax(MyEnum[] values) + { + await Assert.That(values.Length).IsEqualTo(2); + } +} diff --git a/TUnit.TestProject/Bugs/_3882/CancellationAfterHooksTests.cs b/TUnit.TestProject/Bugs/_3882/CancellationAfterHooksTests.cs new file mode 100644 index 0000000000..1f1b19030b --- /dev/null +++ b/TUnit.TestProject/Bugs/_3882/CancellationAfterHooksTests.cs @@ -0,0 +1,174 @@ +using System.Diagnostics; + +namespace TUnit.TestProject.Bugs._3882; + +/// +/// Tests for issue #3882: After Test hook is not run when test is cancelled +/// https://github.com/thomhurst/TUnit/issues/3882 +/// +/// This test demonstrates that After hooks now execute even when tests are cancelled. +/// The Before hook starts a process, the test delays, and the After hook cleans up the process. +/// When cancelled via Test Explorer or timeout, the After hook should still execute. +/// +public class CancellationAfterHooksTests +{ + private static readonly string MarkerFileDirectory = Path.Combine(Path.GetTempPath(), "TUnit_3882_Tests"); + + [Before(Test)] + public async Task StartProcess(TestContext context) + { + // Create marker directory + Directory.CreateDirectory(MarkerFileDirectory); + + // Write marker to prove Before hook ran + var beforeMarker = Path.Combine(MarkerFileDirectory, $"before_{context.Metadata.TestName}.txt"); + await File.WriteAllTextAsync(beforeMarker, $"Before hook executed at {DateTime.Now:O}"); + } + + [Test] + [Timeout(2000)] // 2 second timeout to force cancellation + public async Task Test_ThatGets_Cancelled(CancellationToken cancellationToken) + { + // This test delays longer than the timeout, causing cancellation + await Task.Delay(10000, cancellationToken); + } + + [After(Test)] + public async Task StopProcess(TestContext context) + { + try + { + // Write marker to prove After hook ran EVEN ON CANCELLATION + var afterMarker = Path.Combine(MarkerFileDirectory, $"after_{context.Metadata.TestName}.txt"); + await File.WriteAllTextAsync(afterMarker, $"After hook executed at {DateTime.Now:O} - Outcome: {context.Execution.Result?.State}"); + } + catch (Exception ex) + { + // Don't let marker file creation failure prevent process cleanup + Console.WriteLine($"[AfterTest] Failed to write marker file: {ex.Message}"); + } + } +} + +/// +/// Tests for Session-level After hooks with cancellation +/// +public class SessionLevelCancellationTests +{ + private static readonly string SessionMarkerFile = Path.Combine(Path.GetTempPath(), "TUnit_3882_Session_After.txt"); + + [Before(TestSession)] + public static async Task SessionSetup(TestSessionContext context) + { + await File.WriteAllTextAsync( + Path.Combine(Path.GetTempPath(), "TUnit_3882_Session_Before.txt"), + $"Session Before hook executed at {DateTime.Now:O}"); + } + + [After(TestSession)] + public static async Task SessionCleanup(TestSessionContext context) + { + // This should run even if tests are cancelled + try + { + await File.WriteAllTextAsync( + SessionMarkerFile, + $"Session After hook executed at {DateTime.Now:O}"); + Console.WriteLine($"[AfterTestSession] Session After hook completed successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[AfterTestSession] Failed to write marker file: {ex.Message}"); + throw; // Re-throw to signal failure, but after logging + } + } + + [Test] + [Timeout(1000)] + public async Task SessionTest_ThatGets_Cancelled(CancellationToken cancellationToken) + { + await Task.Delay(5000, cancellationToken); + } +} + +/// +/// Tests for Assembly-level After hooks with cancellation +/// +public class AssemblyLevelCancellationTests +{ + private static readonly string AssemblyMarkerFile = Path.Combine(Path.GetTempPath(), "TUnit_3882_Assembly_After.txt"); + + [Before(Assembly)] + public static async Task AssemblySetup(AssemblyHookContext context) + { + await File.WriteAllTextAsync( + Path.Combine(Path.GetTempPath(), "TUnit_3882_Assembly_Before.txt"), + $"Assembly Before hook executed at {DateTime.Now:O}"); + } + + [After(Assembly)] + public static async Task AssemblyCleanup(AssemblyHookContext context) + { + // This should run even if tests are cancelled + try + { + await File.WriteAllTextAsync( + AssemblyMarkerFile, + $"Assembly After hook executed at {DateTime.Now:O}"); + Console.WriteLine($"[AfterAssembly] Assembly After hook completed successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[AfterAssembly] Failed to write marker file: {ex.Message}"); + throw; // Re-throw to signal failure, but after logging + } + } + + [Test] + [Timeout(1000)] + public async Task AssemblyTest_ThatGets_Cancelled(CancellationToken cancellationToken) + { + await Task.Delay(5000, cancellationToken); + } +} + +/// +/// Tests for Class-level After hooks with cancellation +/// +public class ClassLevelCancellationTests +{ + private static readonly string ClassMarkerFile = Path.Combine(Path.GetTempPath(), "TUnit_3882_Class_After.txt"); + + [Before(Class)] + public static async Task ClassSetup(ClassHookContext context) + { + await File.WriteAllTextAsync( + Path.Combine(Path.GetTempPath(), "TUnit_3882_Class_Before.txt"), + $"Class Before hook executed at {DateTime.Now:O}"); + } + + [After(Class)] + public static async Task ClassCleanup(ClassHookContext context) + { + // This should run even if tests are cancelled + try + { + await File.WriteAllTextAsync( + ClassMarkerFile, + $"Class After hook executed at {DateTime.Now:O}"); + Console.WriteLine($"[AfterClass] Class After hook completed successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[AfterClass] Failed to write marker file: {ex.Message}"); + throw; // Re-throw to signal failure, but after logging + } + } + + [Test] + [Timeout(1000)] + public async Task ClassTest_ThatGets_Cancelled(CancellationToken cancellationToken) + { + await Task.Delay(5000, cancellationToken); + } +} diff --git a/TUnit.TestProject/Bugs/_3882/ExternalCancellationTests.cs b/TUnit.TestProject/Bugs/_3882/ExternalCancellationTests.cs new file mode 100644 index 0000000000..509a8326aa --- /dev/null +++ b/TUnit.TestProject/Bugs/_3882/ExternalCancellationTests.cs @@ -0,0 +1,167 @@ +using System.Diagnostics; + +namespace TUnit.TestProject.Bugs._3882; + +[Timeout(300_000)] // Overall timeout for the test class to prevent indefinite hangs +public class ExternalCancellationTests +{ + private static readonly string MarkerFileDirectory = Path.Combine(Path.GetTempPath(), "TUnit_3882_External"); + + [Before(Test)] + public async Task StartProcess(TestContext context, CancellationToken cancellationToken) + { + // Create marker directory + Directory.CreateDirectory(MarkerFileDirectory); + + // Write marker to prove Before hook ran + var beforeMarker = Path.Combine(MarkerFileDirectory, $"before_{context.Metadata.TestName}.txt"); + await File.WriteAllTextAsync(beforeMarker, $"Before hook executed at {DateTime.Now:O}"); + } + + [Test] + // NO [Timeout] attribute - test runs indefinitely until cancelled externally + public async Task Test_ThatGets_Cancelled_Externally(CancellationToken cancellationToken) + { + // This test delays indefinitely, only stops via external cancellation + await Task.Delay(TimeSpan.FromHours(1), cancellationToken); + } + + [After(Test)] + public async Task StopProcess(TestContext context, CancellationToken cancellationToken) + { + try + { + // Write marker to prove After hook ran EVEN ON EXTERNAL CANCELLATION + var afterMarker = Path.Combine(MarkerFileDirectory, $"after_{context.Metadata.TestName}.txt"); + await File.WriteAllTextAsync(afterMarker, $"After hook executed at {DateTime.Now:O} - Outcome: {context.Execution.Result?.State}"); + } + catch (Exception ex) + { + // Don't let marker file creation failure prevent process cleanup + Console.WriteLine($"[AfterTest] Failed to write marker file: {ex.Message}"); + } + } +} + +/// +/// Tests for Session-level After hooks with external cancellation +/// +public class ExternalSessionLevelCancellationTests +{ + private static readonly string SessionMarkerFile = Path.Combine(Path.GetTempPath(), "TUnit_3882_External_Session_After.txt"); + + [Before(TestSession)] + public static async Task SessionSetup(TestSessionContext context) + { + await File.WriteAllTextAsync( + Path.Combine(Path.GetTempPath(), "TUnit_3882_External_Session_Before.txt"), + $"Session Before hook executed at {DateTime.Now:O}"); + } + + [After(TestSession)] + public static async Task SessionCleanup(TestSessionContext context) + { + // This should run even if tests are cancelled externally + try + { + await File.WriteAllTextAsync( + SessionMarkerFile, + $"Session After hook executed at {DateTime.Now:O}"); + Console.WriteLine($"[AfterTestSession] Session After hook completed successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[AfterTestSession] Failed to write marker file: {ex.Message}"); + throw; // Re-throw to signal failure, but after logging + } + } + + [Test] + // NO [Timeout] attribute - test runs indefinitely until cancelled externally + public async Task SessionTest_ThatGets_Cancelled_Externally(CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromHours(1), cancellationToken); + } +} + +/// +/// Tests for Assembly-level After hooks with external cancellation +/// +public class ExternalAssemblyLevelCancellationTests +{ + private static readonly string AssemblyMarkerFile = Path.Combine(Path.GetTempPath(), "TUnit_3882_External_Assembly_After.txt"); + + [Before(Assembly)] + public static async Task AssemblySetup(AssemblyHookContext context) + { + await File.WriteAllTextAsync( + Path.Combine(Path.GetTempPath(), "TUnit_3882_External_Assembly_Before.txt"), + $"Assembly Before hook executed at {DateTime.Now:O}"); + } + + [After(Assembly)] + public static async Task AssemblyCleanup(AssemblyHookContext context) + { + // This should run even if tests are cancelled externally + try + { + await File.WriteAllTextAsync( + AssemblyMarkerFile, + $"Assembly After hook executed at {DateTime.Now:O}"); + Console.WriteLine($"[AfterAssembly] Assembly After hook completed successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[AfterAssembly] Failed to write marker file: {ex.Message}"); + throw; // Re-throw to signal failure, but after logging + } + } + + [Test] + // NO [Timeout] attribute - test runs indefinitely until cancelled externally + public async Task AssemblyTest_ThatGets_Cancelled_Externally(CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromHours(1), cancellationToken); + } +} + +/// +/// Tests for Class-level After hooks with external cancellation +/// +public class ExternalClassLevelCancellationTests +{ + private static readonly string ClassMarkerFile = Path.Combine(Path.GetTempPath(), "TUnit_3882_External_Class_After.txt"); + + [Before(Class)] + public static async Task ClassSetup(ClassHookContext context) + { + await File.WriteAllTextAsync( + Path.Combine(Path.GetTempPath(), "TUnit_3882_External_Class_Before.txt"), + $"Class Before hook executed at {DateTime.Now:O}"); + } + + [After(Class)] + public static async Task ClassCleanup(ClassHookContext context) + { + // This should run even if tests are cancelled externally + try + { + await File.WriteAllTextAsync( + ClassMarkerFile, + $"Class After hook executed at {DateTime.Now:O}"); + Console.WriteLine($"[AfterClass] Class After hook completed successfully"); + } + catch (Exception ex) + { + Console.WriteLine($"[AfterClass] Failed to write marker file: {ex.Message}"); + throw; // Re-throw to signal failure, but after logging + } + } + + [Test] + // NO [Timeout] attribute - test runs indefinitely until cancelled externally + public async Task ClassTest_ThatGets_Cancelled_Externally(CancellationToken cancellationToken) + { + await Task.Delay(TimeSpan.FromHours(1), cancellationToken); + } +} diff --git a/TUnit.TestProject/DynamicTests/DynamicTestDisplayNameTests.cs b/TUnit.TestProject/DynamicTests/DynamicTestDisplayNameTests.cs new file mode 100644 index 0000000000..627e4f1fe8 --- /dev/null +++ b/TUnit.TestProject/DynamicTests/DynamicTestDisplayNameTests.cs @@ -0,0 +1,62 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.DynamicTests; + +/// +/// Tests that validate custom DisplayName is correctly applied to dynamic tests. +/// +[EngineTest(ExpectedResult.Pass)] +public class DynamicTestDisplayNameTests +{ + public async Task TestWithCustomDisplayName() + { + await Assert.That(TestContext.Current!.Metadata.DisplayName).IsEqualTo("My Custom Dynamic Test Name"); + } + + public async Task TestWithParameterizedDisplayName(int value) + { + await Assert.That(TestContext.Current!.Metadata.DisplayName).IsEqualTo($"Dynamic Test with value {value}"); + } + + public async Task TestWithDefaultDisplayName() + { + // When DisplayName is not set, it should fall back to the default generated name + await Assert.That(TestContext.Current!.Metadata.DisplayName).Contains("TestWithDefaultDisplayName"); + } + +#pragma warning disable TUnitWIP0001 + [DynamicTestBuilder] +#pragma warning restore TUnitWIP0001 + public void BuildTests(DynamicTestBuilderContext context) + { + // Test with a custom display name + context.AddTest(new DynamicTest + { + TestMethod = c => c.TestWithCustomDisplayName(), + DisplayName = "My Custom Dynamic Test Name" + }); + + // Test with parameterized display name + context.AddTest(new DynamicTest + { + TestMethod = c => c.TestWithParameterizedDisplayName(42), + TestMethodArguments = [42], + DisplayName = "Dynamic Test with value 42" + }); + + // Another parameterized test with different value + context.AddTest(new DynamicTest + { + TestMethod = c => c.TestWithParameterizedDisplayName(100), + TestMethodArguments = [100], + DisplayName = "Dynamic Test with value 100" + }); + + // Test without custom display name (should use default) + context.AddTest(new DynamicTest + { + TestMethod = c => c.TestWithDefaultDisplayName() + // DisplayName intentionally not set + }); + } +} diff --git a/TUnit.TestProject/DynamicTests/DynamicTestIndexTests.cs b/TUnit.TestProject/DynamicTests/DynamicTestIndexTests.cs new file mode 100644 index 0000000000..32999d816c --- /dev/null +++ b/TUnit.TestProject/DynamicTests/DynamicTestIndexTests.cs @@ -0,0 +1,36 @@ +using TUnit.TestProject.Attributes; + +namespace TUnit.TestProject.DynamicTests; + +/// +/// Tests that validate DynamicTestIndex generates unique test IDs +/// when multiple dynamic tests target the same method. +/// +[EngineTest(ExpectedResult.Pass)] +public class DynamicTestIndexTests +{ + public void TestMethod(int value) + { + Console.WriteLine($"TestMethod called with value: {value}"); + } + +#pragma warning disable TUnitWIP0001 + [DynamicTestBuilder] +#pragma warning restore TUnitWIP0001 + public void BuildTests(DynamicTestBuilderContext context) + { + // Add 5 dynamic tests all targeting the SAME method with different arguments. + // Before the DynamicTestIndex fix, these would generate duplicate test IDs + // and only one would execute. + for (var i = 1; i <= 5; i++) + { + var value = i; + context.AddTest(new DynamicTest + { + TestMethod = c => c.TestMethod(value), + TestMethodArguments = [value], + Attributes = [] + }); + } + } +} diff --git a/TUnit.TestProject/TestArtifactTests.cs b/TUnit.TestProject/TestArtifactTests.cs index 7e0fbecfad..9e11e408d9 100644 --- a/TUnit.TestProject/TestArtifactTests.cs +++ b/TUnit.TestProject/TestArtifactTests.cs @@ -14,4 +14,22 @@ public void Artifact_Test() DisplayName = "Blah!" }); } + + [Test] + public void Artifact_Test_Simple_Overload() + { + // Simple overload - file name is used as display name + TestContext.Current!.Output.AttachArtifact("Data/Zip.zip"); + } + + [Test] + public void Artifact_Test_Simple_Overload_With_Name() + { + // Simple overload with custom display name and description + TestContext.Current!.Output.AttachArtifact( + "Data/Zip.zip", + displayName: "Test Zip File", + description: "A sample zip file for testing" + ); + } } diff --git a/TUnit.TestProject/TestBuildContextOutputCaptureTests.cs b/TUnit.TestProject/TestBuildContextOutputCaptureTests.cs index cb11a76453..246e622391 100644 --- a/TUnit.TestProject/TestBuildContextOutputCaptureTests.cs +++ b/TUnit.TestProject/TestBuildContextOutputCaptureTests.cs @@ -30,9 +30,12 @@ public DataSourceWithConstructorOutput() } /// - /// Data source that writes to console in async initializer + /// Data source that writes to console in async initializer. + /// Uses IAsyncDiscoveryInitializer so it initializes during test discovery/building, + /// allowing the output to be captured in the test's build context. + /// Note: Regular IAsyncInitializer only runs during test execution (per issue #3992 fix). /// - public class DataSourceWithAsyncInitOutput : IAsyncInitializer + public class DataSourceWithAsyncInitOutput : IAsyncDiscoveryInitializer { public string Value { get; private set; } = "Uninitialized"; @@ -88,8 +91,9 @@ public async Task Test_CapturesConstructorOutput_InTestResults(DataSourceWithCon [ClassDataSource] public async Task Test_CapturesAsyncInitializerOutput_InTestResults(DataSourceWithAsyncInitOutput data) { - // The InitializeAsync output should be captured during test building - // and included in the test's output + // The InitializeAsync output should be captured during test building. + // Note: This uses IAsyncDiscoveryInitializer which runs during discovery. + // Regular IAsyncInitializer runs during execution only (per issue #3992 fix). // Get the test output var output = TestContext.Current!.GetStandardOutput(); diff --git a/TUnit.UnitTests/PropertyDataSourceInjectionTests.cs b/TUnit.UnitTests/PropertyDataSourceInjectionTests.cs index 75dffd84e0..4bb4388a3c 100644 --- a/TUnit.UnitTests/PropertyDataSourceInjectionTests.cs +++ b/TUnit.UnitTests/PropertyDataSourceInjectionTests.cs @@ -227,3 +227,49 @@ public string GetDeepData() } } +// Regression test for Issue #3991: TestContext.Current is null during property injection +public class TestContextAvailabilityDuringPropertyInjectionTests +{ + [TestContextDataSource] + public required TestContextCapture? ContextCapture { get; set; } + + [Test] + public async Task PropertyInjection_CanAccessTestContext_DuringDiscoveryPhase() + { + // This test verifies that TestContext.Current is available during property injection + // in the discovery/registration phase (Issue #3991) + await Assert.That(ContextCapture).IsNotNull(); + await Assert.That(ContextCapture!.TestContextWasAvailable).IsTrue(); + await Assert.That(ContextCapture.CapturedTestContextId).IsNotNull(); + } +} + +// Data source that accesses TestContext.Current during initialization +// This reproduces the bug from Issue #3991 +public class TestContextDataSourceAttribute : AsyncDataSourceGeneratorAttribute +{ + protected override async IAsyncEnumerable>> GenerateDataSourcesAsync(DataGeneratorMetadata dataGeneratorMetadata) + { + yield return async () => + { + // This is where the bug occurs: TestContext.Current is null during discovery + var testContext = TestContext.Current; + + return new TestContextCapture + { + TestContextWasAvailable = testContext != null, + CapturedTestContextId = testContext?.Id, + CapturedCancellationToken = testContext?.CancellationToken ?? default + }; + }; + await Task.CompletedTask; + } +} + +public class TestContextCapture +{ + public bool TestContextWasAvailable { get; set; } + public string? CapturedTestContextId { get; set; } + public CancellationToken CapturedCancellationToken { get; set; } +} + diff --git a/docs/docs/advanced/extension-points.md b/docs/docs/advanced/extension-points.md index b4c9e0a1d3..d853d5431c 100644 --- a/docs/docs/advanced/extension-points.md +++ b/docs/docs/advanced/extension-points.md @@ -430,6 +430,68 @@ public class DatabaseTests : IAsyncInitializer } ``` +## IAsyncDiscoveryInitializer + +For scenarios requiring initialization during test discovery rather than execution, implement `IAsyncDiscoveryInitializer`: + +```csharp +namespace TUnit.Core.Interfaces; + +/// +/// Initializes during test discovery phase, before test enumeration. +/// Use when data sources need access to initialized data during discovery. +/// +public interface IAsyncDiscoveryInitializer : IAsyncInitializer; +``` + +**When to use:** +- `InstanceMethodDataSource` accessing dynamically loaded data +- Test case enumeration depends on async-loaded fixtures +- Discovery-time data generation + +**Performance consideration:** Discovery runs frequently (IDE reloads, `--list-tests`, CI enumeration), so avoid expensive operations when possible. Prefer predefined data over discovery-time initialization when feasible. + +Example: + +```csharp +// Fixture that loads test cases during discovery +public class TestCaseFixture : IAsyncDiscoveryInitializer, IAsyncDisposable +{ + private List _testCases = []; + + public async Task InitializeAsync() + { + // This runs during DISCOVERY, not just execution + _testCases = await LoadTestCasesFromDatabaseAsync(); + } + + public IEnumerable GetTestCases() => _testCases; + + public async ValueTask DisposeAsync() + { + _testCases.Clear(); + } +} + +public class MyTests +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required TestCaseFixture Fixture { get; init; } + + public IEnumerable TestCases => Fixture.GetTestCases(); + + [Test] + [InstanceMethodDataSource(nameof(TestCases))] + public async Task MyTest(string testCase) + { + // Tests are generated during discovery with initialized data + await Assert.That(testCase).IsNotNullOrEmpty(); + } +} +``` + +See [Property Injection - Discovery Phase Initialization](../test-lifecycle/property-injection.md#discovery-phase-initialization) for detailed guidance and best practices. + ## Best Practices 1. **Keep Extensions Focused**: Each extension should have a single, clear responsibility. diff --git a/docs/docs/advanced/performance-best-practices.md b/docs/docs/advanced/performance-best-practices.md index 65587e29db..15034990fd 100644 --- a/docs/docs/advanced/performance-best-practices.md +++ b/docs/docs/advanced/performance-best-practices.md @@ -99,11 +99,26 @@ public class LazyDataProvider #### Configure Appropriate Parallelism ```csharp -// Set maximum parallel test execution -[assembly: MaxParallelTests(Environment.ProcessorCount)] +using TUnit.Core; +using TUnit.Core.Interfaces; -// Or use command line -// dotnet test --maximum-parallel-tests 8 +// Set maximum parallel test execution using an assembly-level parallel limiter +[assembly: ParallelLimiter] + +public class ProcessorCountLimit : IParallelLimit +{ + public int Limit => Environment.ProcessorCount; +} +``` + +Alternatively, use the command line flag: +```bash +dotnet test -- --maximum-parallel-tests 8 +``` + +Or set an environment variable: +```bash +export TUNIT_MAX_PARALLEL_TESTS=8 ``` #### Group Related Tests @@ -207,8 +222,8 @@ public class PerformantTests ```csharp // ❌ Bad: Expensive operation in assertion await Assert.That(await GetAllUsersFromDatabase()) - .HasCount() - .EqualTo(1000); + .Count() + .IsEqualTo(1000); // ✅ Good: Use efficient queries var userCount = await GetUserCountFromDatabase(); @@ -231,7 +246,7 @@ public async Task EfficientValidation() } // More expensive validations only if needed - await Assert.That(result.Items).HasCount().GreaterThan(0); + await Assert.That(result.Items).Count().IsGreaterThan(0); } ``` @@ -467,15 +482,20 @@ public class InMemoryDatabaseTests ```bash # Run fast unit tests first -dotnet test --filter "Category=Unit" --no-build +dotnet test --no-build -- --treenode-filter "/*/*/*/*[Category=Unit]" # Run slower integration tests separately -dotnet test --filter "Category=Integration" --no-build +dotnet test --no-build -- --treenode-filter "/*/*/*/*[Category=Integration]" # Run expensive E2E tests last -dotnet test --filter "Category=E2E" --no-build +dotnet test --no-build -- --treenode-filter "/*/*/*/*[Category=E2E]" ``` +> **Note**: With .NET 10 SDK or newer, you can use the simpler syntax: +> ```bash +> dotnet test --no-build --treenode-filter "/**[Category=Unit]" +> ``` + ### Use Test Result Caching ```xml diff --git a/docs/docs/assertions/awaiting.md b/docs/docs/assertions/awaiting.md index 2185db045c..540a19102c 100644 --- a/docs/docs/assertions/awaiting.md +++ b/docs/docs/assertions/awaiting.md @@ -90,7 +90,7 @@ public async Task ComplexCollectionAssertions() // Assert multiple conditions on a collection await Assert.That(orders) - .HasCount().IsGreaterThan(0) + .Count().IsGreaterThan(0) .And.Contains(o => o.Status == OrderStatus.Completed) .And.DoesNotContain(o => o.Total < 0) .And.HasDistinctItems(); @@ -146,7 +146,7 @@ public async Task DetailedExceptionAssertions() var exception = await Assert.That(() => ParallelOperationAsync()) .Throws(); - await Assert.That(exception.InnerExceptions).HasCount(3); + await Assert.That(exception.InnerExceptions).Count().IsEqualTo(3); await Assert.That(exception.InnerExceptions).All(e => e is TaskCanceledException); } ``` @@ -169,7 +169,7 @@ public async Task CustomAssertionConditions() // Combine built-in and custom assertions await Assert.That(measurements) - .HasCount().GreaterThan(100) + .Count().IsGreaterThan(100) .And.All(m => m > 0) .And.Satisfies(IsNormallyDistributed, "Data should be normally distributed"); } @@ -242,7 +242,7 @@ public async Task StringPatternAssertions() .StartsWith("Report Generated:") .And.Contains("Total Items:") .And.DoesNotContain("null") - .And.HasLength().IsBetween(1000, 5000); + .And.Length().IsBetween(1000, 5000); } ``` @@ -299,7 +299,7 @@ public async Task PerformanceAssertions() await Assert.That(results.Max()) .IsLessThan(500); // No operation over 500ms - await Assert.That(results.Where(r => r > 200).HasCount()) + await Assert.That(results.Where(r => r > 200).Count()) .IsLessThan(5); // Less than 5% over 200ms } ``` diff --git a/docs/docs/assertions/collections.md b/docs/docs/assertions/collections.md index 52f72d5d27..fe6e4b6f18 100644 --- a/docs/docs/assertions/collections.md +++ b/docs/docs/assertions/collections.md @@ -98,7 +98,7 @@ public async Task Collection_Does_Not_Contain_Matching() ## Count Assertions -### HasCount +### Count Tests that a collection has an exact count: @@ -108,7 +108,7 @@ public async Task Collection_Has_Count() { var numbers = new[] { 1, 2, 3, 4, 5 }; - await Assert.That(numbers).HasCount(5); + await Assert.That(numbers).Count().IsEqualTo(5); } ``` @@ -123,29 +123,74 @@ public async Task Count_With_Comparison() var numbers = new[] { 1, 2, 3, 4, 5 }; await Assert.That(numbers) - .HasCount().EqualTo(5); + .Count().IsEqualTo(5); await Assert.That(numbers) - .HasCount().GreaterThan(3) - .And.HasCount().LessThan(10); + .Count().IsGreaterThan(3) + .And.Count().IsLessThan(10); } ``` -### Count with Predicate +### Count with Inner Assertion -Count items matching a predicate: +Count items that satisfy an assertion, allowing you to reuse existing assertion methods: ```csharp [Test] -public async Task Count_With_Predicate() +public async Task Count_With_Inner_Assertion() { var numbers = new[] { 1, 2, 3, 4, 5, 6 }; - // Count even numbers - var evenCount = await Assert.That(numbers) - .Count(n => n % 2 == 0); + // Count numbers greater than 3 using assertion builder + await Assert.That(numbers) + .Count(item => item.IsGreaterThan(3)) + .IsEqualTo(3); + + // Count numbers between 2 and 5 + await Assert.That(numbers) + .Count(item => item.IsBetween(2, 5)) + .IsEqualTo(4); +} - await Assert.That(evenCount).IsEqualTo(3); +[Test] +public async Task Count_Strings_With_Inner_Assertion() +{ + var names = new[] { "Alice", "Bob", "Andrew", "Charlie" }; + + // Count names starting with "A" + await Assert.That(names) + .Count(item => item.StartsWith("A")) + .IsEqualTo(2); +} +``` + +### Count with Collection Chaining + +Count assertions preserve the collection type, allowing you to chain additional collection assertions: + +```csharp +[Test] +public async Task Count_With_Chaining() +{ + var numbers = new[] { 1, 2, 3, 4, 5 }; + + // Assert count and then chain with other collection assertions + await Assert.That(numbers) + .Count().IsEqualTo(5) + .And.Contains(3) + .And.IsInOrder(); + + // Count with item filter assertion, then chain + await Assert.That(numbers) + .Count(item => item.IsGreaterThan(2)).IsEqualTo(3) + .And.Contains(5) + .And.All(x => x > 0); + + // For non-int collections, you can also use inline count assertions + var names = new[] { "Alice", "Bob", "Charlie" }; + await Assert.That(names) + .Count(c => c.IsEqualTo(3)) + .And.Contains("Bob"); } ``` @@ -160,7 +205,7 @@ public async Task Collection_Is_Empty() var empty = new List(); await Assert.That(empty).IsEmpty(); - await Assert.That(empty).HasCount(0); + await Assert.That(empty).Count().IsEqualTo(0); } ``` @@ -490,7 +535,7 @@ The default behavior (ignoring order) is ideal for: public async Task Database_Query_Results() { var results = await database.GetActiveUsersAsync(); - + // Order doesn't matter for this assertion await Assert.That(results) .IsEquivalentTo(new[] { user1, user2, user3 }); @@ -510,7 +555,7 @@ Use order-sensitive comparison when: public async Task Sorted_Query_Results() { var results = await database.GetUsersSortedByNameAsync(); - + // Order matters here await Assert.That(results) .IsEquivalentTo( @@ -530,7 +575,7 @@ public async Task Multiple_Order_Sensitive_Checks() { var list1 = GetSortedList1(); var list2 = GetSortedList2(); - + // Be explicit about ordering requirements await Assert.That(list1).IsEquivalentTo(expected1, CollectionOrdering.Matching); await Assert.That(list2).IsEquivalentTo(expected2, CollectionOrdering.Matching); @@ -544,7 +589,7 @@ For ordered comparisons, you can also use `IsInOrder()`: public async Task Verify_Ordering_Separately() { var actual = new[] { 1, 2, 3 }; - + // Check both equivalency and ordering await Assert.That(actual).IsEquivalentTo(new[] { 1, 2, 3 }); await Assert.That(actual).IsInOrder(); @@ -638,7 +683,7 @@ public async Task Filter_And_Assert() var evens = numbers.Where(n => n % 2 == 0).ToArray(); await Assert.That(evens) - .HasCount(5) + .Count().IsEqualTo(5) .And.All(n => n % 2 == 0); } ``` @@ -659,7 +704,7 @@ public async Task LINQ_Query_Results() var adults = users.Where(u => u.Age >= 18).ToArray(); await Assert.That(adults) - .HasCount(3) + .Count().IsEqualTo(3) .And.All(u => u.Age >= 18); } ``` @@ -708,7 +753,7 @@ public async Task Map_And_Verify() var names = users.Select(u => u.Name).ToArray(); await Assert.That(names) - .HasCount(2) + .Count().IsEqualTo(2) .And.Contains("Alice") .And.Contains("Bob") .And.All(name => !string.IsNullOrEmpty(name)); @@ -745,12 +790,12 @@ public async Task Nested_Collections() new[] { 7, 8, 9 } }; - await Assert.That(matrix).HasCount(3); + await Assert.That(matrix).Count().IsEqualTo(3); await Assert.That(matrix).All(row => row.Length == 3); // Flatten and assert var flattened = matrix.SelectMany(x => x).ToArray(); - await Assert.That(flattened).HasCount(9); + await Assert.That(flattened).Count().IsEqualTo(9); } ``` @@ -768,7 +813,7 @@ public async Task Collection_Of_Collections() }; await Assert.That(groups) - .HasCount(3) + .Count().IsEqualTo(3) .And.All(group => group.Count > 0); } ``` @@ -783,7 +828,7 @@ public async Task Chained_Collection_Assertions() await Assert.That(numbers) .IsNotEmpty() - .And.HasCount(5) + .And.Count().IsEqualTo(5) .And.Contains(3) .And.DoesNotContain(10) .And.IsInOrder() @@ -807,7 +852,7 @@ public async Task Materialize_Before_Multiple_Assertions() // Materialize once to avoid re-execution var materialized = query.ToArray(); - await Assert.That(materialized).HasCount().GreaterThan(1000); + await Assert.That(materialized).Count().IsGreaterThan(1000); await Assert.That(materialized).Contains(100); await Assert.That(materialized).All(n => n % 2 == 0); } @@ -822,7 +867,7 @@ public async Task HashSet_Assertions() var set = new HashSet { 1, 2, 3, 4, 5 }; await Assert.That(set) - .HasCount(5) + .Count().IsEqualTo(5) .And.Contains(3) .And.HasDistinctItems(); } diff --git a/docs/docs/assertions/dictionaries.md b/docs/docs/assertions/dictionaries.md index 938a723f6e..24320dffa8 100644 --- a/docs/docs/assertions/dictionaries.md +++ b/docs/docs/assertions/dictionaries.md @@ -104,7 +104,7 @@ public async Task Dictionary_Count() ["c"] = 3 }; - await Assert.That(dict).HasCount(3); + await Assert.That(dict).Count().IsEqualTo(3); } ``` @@ -223,7 +223,7 @@ public async Task Lookup_Table() }; await Assert.That(statusCodes) - .HasCount(3) + .Count().IsEqualTo(3) .And.ContainsKey(200) .And.ContainsValue("OK"); } @@ -243,7 +243,7 @@ public async Task Cache_Contains_Entry() await Assert.That(cache) .ContainsKey("user:123") - .And.HasCount(2) + .And.Count().IsEqualTo(2) .And.IsNotEmpty(); } ``` @@ -306,7 +306,7 @@ public async Task Dictionary_Keys() var keys = dict.Keys; await Assert.That(keys) - .HasCount(3) + .Count().IsEqualTo(3) .And.Contains("a") .And.Contains("b") .And.Contains("c"); @@ -329,7 +329,7 @@ public async Task Dictionary_Values() var values = dict.Values; await Assert.That(values) - .HasCount(3) + .Count().IsEqualTo(3) .And.Contains(1) .And.Contains(2) .And.All(v => v > 0); @@ -376,7 +376,7 @@ public async Task Chained_Dictionary_Assertions() await Assert.That(dict) .IsNotEmpty() - .And.HasCount(3) + .And.Count().IsEqualTo(3) .And.ContainsKey("apple") .And.ContainsKey("banana") .And.ContainsValue(2) @@ -397,7 +397,7 @@ public async Task Concurrent_Dictionary() concurrent.TryAdd("b", 2); await Assert.That(concurrent) - .HasCount(2) + .Count().IsEqualTo(2) .And.ContainsKey("a"); } ``` @@ -412,7 +412,7 @@ public async Task ReadOnly_Dictionary() var readOnly = new ReadOnlyDictionary(dict); await Assert.That(readOnly) - .HasCount(1) + .Count().IsEqualTo(1) .And.ContainsKey("a"); } ``` diff --git a/docs/docs/assertions/equality-and-comparison.md b/docs/docs/assertions/equality-and-comparison.md index f20912b79f..f2b14bc868 100644 --- a/docs/docs/assertions/equality-and-comparison.md +++ b/docs/docs/assertions/equality-and-comparison.md @@ -54,7 +54,7 @@ public async Task Using_EqualTo_Alias() var numbers = new[] { 1, 2, 3 }; await Assert.That(numbers) - .HasCount().EqualTo(3) + .Count().IsEqualTo(3) .And.Contains(2); } ``` diff --git a/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md b/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md index befc48a581..59e748ad0f 100644 --- a/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md +++ b/docs/docs/assertions/extensibility/extensibility-chaining-and-converting.md @@ -185,4 +185,4 @@ public class HasDetailAssertion : Assertion TUnit includes several built-in examples of type conversion assertions: - `WhenParsedInto()` - Converts a string to a parsed type (e.g., `await Assert.That("123").WhenParsedInto().IsEqualTo(123)`) -- `IsTypeOf()` - Converts to a specific type (e.g., `await Assert.That(obj).IsTypeOf().HasLength(5)`) +- `IsTypeOf()` - Converts to a specific type (e.g., `await Assert.That(obj).IsTypeOf().Length().IsEqualTo(5)`) diff --git a/docs/docs/assertions/fsharp.md b/docs/docs/assertions/fsharp.md index 5b10b7ce84..6709cc0f10 100644 --- a/docs/docs/assertions/fsharp.md +++ b/docs/docs/assertions/fsharp.md @@ -14,9 +14,9 @@ So a test could look like: ```fsharp member this.CheckPositive() = async { - let result = 1 + 1 - do! check (Assert.That(result).IsPositive()) - } + let result = 1 + 1 + do! check (Assert.That(result).IsPositive()) +} ``` F# is a lot more strict with type resolution when it comes to extension methods and method overloads. Because of that you may need to annotate the type for the assertions. @@ -24,23 +24,42 @@ F# is a lot more strict with type resolution when it comes to extension methods For example, ```fsharp - [] - [] - member _.Test3() = async { - let value = "1" - do! check (Assert.That(value).IsEqualTo("1")) - } - - [] - [] - member _.Throws1() = async { - do! check (Assert.That(fun () -> task { return new string([||]) }).ThrowsException()) - } - - [] - [] - member _.Throws4() = async { - do! check (Assert.That(fun () -> Task.FromResult(true)).ThrowsNothing()) - } - -``` \ No newline at end of file +[] +[] +member _.Test3() = async { + let value = "1" + do! check (Assert.That(value).IsEqualTo("1")) +} + +[] +[] +member _.Throws1() = async { + do! check (Assert.That(fun () -> task { return new string([||]) }).ThrowsException()) +} + +[] +[] +member _.Throws4() = async { + do! check (Assert.That(fun () -> Task.FromResult(true)).ThrowsNothing()) +} +``` + +## taskAssert expression + +For Task-based tests you can also use the taskAssert computation expression to avoid wrapping every assertion in check. + +To use it, open the namespace: + +```fsharp +open TUnit.Assertions.FSharp.TaskAssert +``` + +Inside a taskAssert block you can directly do! the assertion: +```fsharp +[] +[] +member _.Test3() = taskAssert { + let value = "1" + do! Assert.That(value).IsEqualTo("1") +} +``` diff --git a/docs/docs/assertions/getting-started.md b/docs/docs/assertions/getting-started.md index 67c466360b..a078a9af9b 100644 --- a/docs/docs/assertions/getting-started.md +++ b/docs/docs/assertions/getting-started.md @@ -63,7 +63,7 @@ await Assert.That(input).IsNotEmpty(); ```csharp await Assert.That(numbers).Contains(42); -await Assert.That(items).HasCount(5); +await Assert.That(items).Count().IsEqualTo(5); await Assert.That(list).IsNotEmpty(); await Assert.That(values).All(x => x > 0); ``` @@ -99,8 +99,8 @@ Combine multiple assertions on the same value using `.And`: await Assert.That(username) .IsNotNull() .And.IsNotEmpty() - .And.HasLength().GreaterThan(3) - .And.HasLength().LessThan(20); + .And.Length().IsGreaterThan(3) + .And.Length().IsLessThan(20); ``` Use `.Or` when any condition can be true: @@ -153,7 +153,7 @@ Collections have rich assertion support: var numbers = new[] { 1, 2, 3, 4, 5 }; // Count and emptiness -await Assert.That(numbers).HasCount(5); +await Assert.That(numbers).Count().IsEqualTo(5); await Assert.That(numbers).IsNotEmpty(); // Membership @@ -397,7 +397,7 @@ Now that you understand the basics, explore specific assertion types: | Null/Default | `IsNull()`, `IsNotNull()`, `IsDefault()` | | Boolean | `IsTrue()`, `IsFalse()` | | Strings | `Contains()`, `StartsWith()`, `Matches()` | -| Collections | `Contains()`, `HasCount()`, `All()`, `Any()` | +| Collections | `Contains()`, `Count()`, `All()`, `Any()` | | Exceptions | `Throws()`, `ThrowsNothing()` | | Types | `IsTypeOf()`, `IsAssignableTo()` | | Async | `CompletesWithin()`, async exception testing | diff --git a/docs/docs/assertions/member-assertions.md b/docs/docs/assertions/member-assertions.md index ea0b7e4e4d..c0d8d7cec4 100644 --- a/docs/docs/assertions/member-assertions.md +++ b/docs/docs/assertions/member-assertions.md @@ -72,7 +72,7 @@ public async Task ComplexMemberAssertions() await Assert.That(team) .Member(t => t.Name, name => name.StartsWith("Team")) .And.Member(t => t.Members, members => members - .HasCount().IsGreaterThan(0) + .Count().IsGreaterThan(0) .And.All(m => m.IsActive) .And.Any(m => m.Role == "Lead")) .And.Member(t => t.CreatedDate, date => date @@ -134,7 +134,7 @@ public async Task NestedObjectAssertions() .And.Member(c => c.Address.City, city => city.IsEqualTo("Seattle")) .And.Member(c => c.Address.ZipCode, zip => zip.Matches(@"^\d{5}$")) .And.Member(c => c.Employees, employees => employees - .HasCount().IsBetween(100, 500) + .Count().IsBetween(100, 500) .And.All(e => e.Email.EndsWith("@techcorp.com"))); } ``` diff --git a/docs/docs/assertions/null-and-default.md b/docs/docs/assertions/null-and-default.md index c0570f6fbd..b2a241aa3a 100644 --- a/docs/docs/assertions/null-and-default.md +++ b/docs/docs/assertions/null-and-default.md @@ -69,7 +69,7 @@ public async Task Chained_After_Null_Check() await Assert.That(input) .IsNotNull() .And.IsNotEmpty() // Compiler knows input is not null - .And.HasLength().GreaterThan(5); + .And.Length().IsGreaterThan(5); } ``` diff --git a/docs/docs/assertions/regex-assertions.md b/docs/docs/assertions/regex-assertions.md index 6b2d9b1261..048aefc022 100644 --- a/docs/docs/assertions/regex-assertions.md +++ b/docs/docs/assertions/regex-assertions.md @@ -108,13 +108,13 @@ public async Task PositionAndLengthAssertions() // Assert that match has specific length await Assert.That(text) .Matches(pattern) - .HasLength(3); + .Length().IsEqualTo(3); // Combine with group assertions await Assert.That(text) .Matches(pattern) .AtIndex(12) - .And.HasLength(3); + .And.Length().IsEqualTo(3); } ``` @@ -149,7 +149,7 @@ public async Task ProductCodeValidation() .Matches(pattern) .And.Group("code", code => code.StartsWith("ABC")) .And.Group("price", price => price.Contains(".99")) - .And.Group("stock", stock => stock.HasLength(2)); + .And.Group("stock", stock => stock.Length().IsEqualTo(2)); } ``` @@ -259,9 +259,9 @@ public async Task CompleteEmailValidation() .And.Group("local", local => local.StartsWith("john")) .And.Group("subdomain", sub => sub.IsEqualTo("mail")) .And.Group("domain", domain => domain.IsEqualTo("example")) - .And.Group("tld", tld => tld.HasLength(3)) + .And.Group("tld", tld => tld.Length().IsEqualTo(3)) .And.AtIndex(0) - .And.HasLength(email.Length); + .And.Length().IsEqualTo(email.Length); } ``` diff --git a/docs/docs/assertions/string.md b/docs/docs/assertions/string.md index 7ffadc6bd6..6d9336f4a0 100644 --- a/docs/docs/assertions/string.md +++ b/docs/docs/assertions/string.md @@ -291,7 +291,7 @@ public async Task String_Is_Not_Empty() } ``` -### HasLength +### Length Tests that a string has a specific length: @@ -301,7 +301,7 @@ public async Task String_Has_Length() { var code = "ABC123"; - await Assert.That(code).HasLength(6); + await Assert.That(code).Length().IsEqualTo(6); } ``` @@ -314,12 +314,12 @@ public async Task Length_With_Comparison() var username = "alice"; await Assert.That(username) - .HasLength().GreaterThan(3) - .And.HasLength().LessThan(20); + .Length().IsGreaterThan(3) + .And.Length().IsLessThan(20); } ``` -Or more concisely: +Using `IsBetween`: ```csharp [Test] @@ -327,7 +327,7 @@ public async Task Length_Range() { var username = "alice"; - await Assert.That(username.Length).IsBetween(3, 20); + await Assert.That(username).Length().IsBetween(3, 20); } ``` @@ -459,8 +459,8 @@ public async Task Validate_Username() var username = "alice_123"; await Assert.That(username) - .HasLength().GreaterThanOrEqualTo(3) - .And.HasLength().LessThanOrEqualTo(20) + .Length().IsGreaterThanOrEqualTo(3) + .And.Length().IsLessThanOrEqualTo(20) .And.Matches(@"^[a-zA-Z0-9_]+$") .And.DoesNotContain(" "); } @@ -475,7 +475,7 @@ public async Task Validate_Password() var password = "SecureP@ss123"; await Assert.That(password) - .HasLength().GreaterThanOrEqualTo(8) + .Length().IsGreaterThanOrEqualTo(8) .And.Matches(@"[A-Z]") // Has uppercase .And.Matches(@"[a-z]") // Has lowercase .And.Matches(@"\d") // Has digit @@ -600,7 +600,7 @@ public async Task Chained_String_Assertions() .And.Contains("World") .And.StartsWith("Hello") .And.EndsWith("!") - .And.HasLength(13); + .And.Length().IsEqualTo(13); } ``` diff --git a/docs/docs/benchmarks/AsyncTests.md b/docs/docs/benchmarks/AsyncTests.md index d3f9769eb3..8832cf6009 100644 --- a/docs/docs/benchmarks/AsyncTests.md +++ b/docs/docs/benchmarks/AsyncTests.md @@ -7,20 +7,20 @@ sidebar_position: 2 # AsyncTests Benchmark :::info Last Updated -This benchmark was automatically generated on **2025-11-18** from the latest CI run. +This benchmark was automatically generated on **2025-12-19** from the latest CI run. -**Environment:** Ubuntu Latest • .NET SDK 10.0.100 +**Environment:** Ubuntu Latest • .NET SDK 10.0.101 ::: ## 📊 Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.2.3 | 556.5 ms | 556.3 ms | 2.03 ms | -| NUnit | 4.4.0 | 663.3 ms | 662.2 ms | 9.02 ms | -| MSTest | 4.0.2 | 635.0 ms | 632.0 ms | 11.80 ms | -| xUnit3 | 3.2.0 | 715.8 ms | 716.7 ms | 8.28 ms | -| **TUnit (AOT)** | 1.2.3 | 124.5 ms | 124.5 ms | 0.26 ms | +| **TUnit** | 1.5.70 | 584.8 ms | 585.8 ms | 6.49 ms | +| NUnit | 4.4.0 | 771.5 ms | 770.5 ms | 8.25 ms | +| MSTest | 4.0.2 | 647.1 ms | 646.7 ms | 7.31 ms | +| xUnit3 | 3.2.1 | 725.9 ms | 723.1 ms | 10.05 ms | +| **TUnit (AOT)** | 1.5.70 | 124.6 ms | 124.7 ms | 0.47 ms | ## 📈 Visual Comparison @@ -58,8 +58,8 @@ This benchmark was automatically generated on **2025-11-18** from the latest CI xychart-beta title "AsyncTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT"] - y-axis "Time (ms)" 0 --> 859 - bar [556.5, 663.3, 635, 715.8, 124.5] + y-axis "Time (ms)" 0 --> 926 + bar [584.8, 771.5, 647.1, 725.9, 124.6] ``` ## 🎯 Key Insights @@ -72,4 +72,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2025-11-18T00:28:14.218Z* +*Last generated: 2025-12-19T00:29:51.613Z* diff --git a/docs/docs/benchmarks/BuildTime.md b/docs/docs/benchmarks/BuildTime.md index d06fcc5888..0c6daa5110 100644 --- a/docs/docs/benchmarks/BuildTime.md +++ b/docs/docs/benchmarks/BuildTime.md @@ -7,9 +7,9 @@ sidebar_position: 8 # Build Performance Benchmark :::info Last Updated -This benchmark was automatically generated on **2025-11-18** from the latest CI run. +This benchmark was automatically generated on **2025-12-19** from the latest CI run. -**Environment:** Ubuntu Latest • .NET SDK 10.0.100 +**Environment:** Ubuntu Latest • .NET SDK 10.0.101 ::: ## 📊 Results @@ -18,10 +18,10 @@ Compilation time comparison across frameworks: | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.2.3 | 1.947 s | 1.953 s | 0.0381 s | -| Build_NUnit | 4.4.0 | 1.556 s | 1.557 s | 0.0171 s | -| Build_MSTest | 4.0.2 | 1.621 s | 1.624 s | 0.0156 s | -| Build_xUnit3 | 3.2.0 | 1.523 s | 1.527 s | 0.0162 s | +| **TUnit** | 1.5.70 | 2.051 s | 2.049 s | 0.0315 s | +| Build_NUnit | 4.4.0 | 1.649 s | 1.645 s | 0.0239 s | +| Build_MSTest | 4.0.2 | 1.745 s | 1.732 s | 0.0350 s | +| Build_xUnit3 | 3.2.1 | 1.623 s | 1.624 s | 0.0216 s | ## 📈 Visual Comparison @@ -60,7 +60,7 @@ xychart-beta title "Build Time Comparison" x-axis ["Build_TUnit", "Build_NUnit", "Build_MSTest", "Build_xUnit3"] y-axis "Time (s)" 0 --> 3 - bar [1.947, 1.556, 1.621, 1.523] + bar [2.051, 1.649, 1.745, 1.623] ``` --- @@ -69,4 +69,4 @@ xychart-beta View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2025-11-18T00:28:14.220Z* +*Last generated: 2025-12-19T00:29:51.615Z* diff --git a/docs/docs/benchmarks/DataDrivenTests.md b/docs/docs/benchmarks/DataDrivenTests.md index 06066bfb23..860d9d5f74 100644 --- a/docs/docs/benchmarks/DataDrivenTests.md +++ b/docs/docs/benchmarks/DataDrivenTests.md @@ -7,20 +7,20 @@ sidebar_position: 3 # DataDrivenTests Benchmark :::info Last Updated -This benchmark was automatically generated on **2025-11-18** from the latest CI run. +This benchmark was automatically generated on **2025-12-19** from the latest CI run. -**Environment:** Ubuntu Latest • .NET SDK 10.0.100 +**Environment:** Ubuntu Latest • .NET SDK 10.0.101 ::: ## 📊 Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.2.3 | 493.26 ms | 492.16 ms | 3.175 ms | -| NUnit | 4.4.0 | 601.85 ms | 601.22 ms | 13.698 ms | -| MSTest | 4.0.2 | 617.89 ms | 622.69 ms | 12.013 ms | -| xUnit3 | 3.2.0 | 610.52 ms | 609.06 ms | 11.016 ms | -| **TUnit (AOT)** | 1.2.3 | 24.33 ms | 24.31 ms | 0.233 ms | +| **TUnit** | 1.5.70 | 510.15 ms | 510.45 ms | 5.350 ms | +| NUnit | 4.4.0 | 664.02 ms | 664.38 ms | 6.350 ms | +| MSTest | 4.0.2 | 624.22 ms | 621.89 ms | 8.050 ms | +| xUnit3 | 3.2.1 | 625.91 ms | 622.72 ms | 13.715 ms | +| **TUnit (AOT)** | 1.5.70 | 25.72 ms | 25.63 ms | 0.282 ms | ## 📈 Visual Comparison @@ -58,8 +58,8 @@ This benchmark was automatically generated on **2025-11-18** from the latest CI xychart-beta title "DataDrivenTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT"] - y-axis "Time (ms)" 0 --> 742 - bar [493.26, 601.85, 617.89, 610.52, 24.33] + y-axis "Time (ms)" 0 --> 797 + bar [510.15, 664.02, 624.22, 625.91, 25.72] ``` ## 🎯 Key Insights @@ -72,4 +72,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2025-11-18T00:28:14.218Z* +*Last generated: 2025-12-19T00:29:51.614Z* diff --git a/docs/docs/benchmarks/MassiveParallelTests.md b/docs/docs/benchmarks/MassiveParallelTests.md index 5808198b81..0982f8c418 100644 --- a/docs/docs/benchmarks/MassiveParallelTests.md +++ b/docs/docs/benchmarks/MassiveParallelTests.md @@ -7,20 +7,20 @@ sidebar_position: 4 # MassiveParallelTests Benchmark :::info Last Updated -This benchmark was automatically generated on **2025-11-18** from the latest CI run. +This benchmark was automatically generated on **2025-12-19** from the latest CI run. -**Environment:** Ubuntu Latest • .NET SDK 10.0.100 +**Environment:** Ubuntu Latest • .NET SDK 10.0.101 ::: ## 📊 Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.2.3 | 595.5 ms | 595.7 ms | 2.63 ms | -| NUnit | 4.4.0 | 1,164.1 ms | 1,163.4 ms | 6.01 ms | -| MSTest | 4.0.2 | 2,947.1 ms | 2,948.2 ms | 6.02 ms | -| xUnit3 | 3.2.0 | 3,048.5 ms | 3,046.6 ms | 14.96 ms | -| **TUnit (AOT)** | 1.2.3 | 130.9 ms | 130.9 ms | 0.41 ms | +| **TUnit** | 1.5.70 | 599.4 ms | 599.3 ms | 3.19 ms | +| NUnit | 4.4.0 | 1,287.1 ms | 1,285.7 ms | 9.81 ms | +| MSTest | 4.0.2 | 2,971.9 ms | 2,972.6 ms | 6.34 ms | +| xUnit3 | 3.2.1 | 3,072.8 ms | 3,069.3 ms | 7.78 ms | +| **TUnit (AOT)** | 1.5.70 | 131.6 ms | 131.8 ms | 0.58 ms | ## 📈 Visual Comparison @@ -58,8 +58,8 @@ This benchmark was automatically generated on **2025-11-18** from the latest CI xychart-beta title "MassiveParallelTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT"] - y-axis "Time (ms)" 0 --> 3659 - bar [595.5, 1164.1, 2947.1, 3048.5, 130.9] + y-axis "Time (ms)" 0 --> 3688 + bar [599.4, 1287.1, 2971.9, 3072.8, 131.6] ``` ## 🎯 Key Insights @@ -72,4 +72,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2025-11-18T00:28:14.219Z* +*Last generated: 2025-12-19T00:29:51.614Z* diff --git a/docs/docs/benchmarks/MatrixTests.md b/docs/docs/benchmarks/MatrixTests.md index 7807cc0d66..ac569051f1 100644 --- a/docs/docs/benchmarks/MatrixTests.md +++ b/docs/docs/benchmarks/MatrixTests.md @@ -7,20 +7,20 @@ sidebar_position: 5 # MatrixTests Benchmark :::info Last Updated -This benchmark was automatically generated on **2025-11-18** from the latest CI run. +This benchmark was automatically generated on **2025-12-19** from the latest CI run. -**Environment:** Ubuntu Latest • .NET SDK 10.0.100 +**Environment:** Ubuntu Latest • .NET SDK 10.0.101 ::: ## 📊 Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.2.3 | 561.43 ms | 562.61 ms | 4.718 ms | -| NUnit | 4.4.0 | 1,556.36 ms | 1,557.95 ms | 7.815 ms | -| MSTest | 4.0.2 | 1,517.35 ms | 1,517.64 ms | 12.824 ms | -| xUnit3 | 3.2.0 | 1,602.60 ms | 1,603.72 ms | 11.920 ms | -| **TUnit (AOT)** | 1.2.3 | 79.00 ms | 79.03 ms | 0.223 ms | +| **TUnit** | 1.5.70 | 558.07 ms | 558.31 ms | 3.185 ms | +| NUnit | 4.4.0 | 1,651.26 ms | 1,651.55 ms | 8.074 ms | +| MSTest | 4.0.2 | 1,516.31 ms | 1,516.34 ms | 4.841 ms | +| xUnit3 | 3.2.1 | 1,606.91 ms | 1,604.13 ms | 7.677 ms | +| **TUnit (AOT)** | 1.5.70 | 78.72 ms | 78.66 ms | 0.411 ms | ## 📈 Visual Comparison @@ -58,8 +58,8 @@ This benchmark was automatically generated on **2025-11-18** from the latest CI xychart-beta title "MatrixTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT"] - y-axis "Time (ms)" 0 --> 1924 - bar [561.43, 1556.36, 1517.35, 1602.6, 79] + y-axis "Time (ms)" 0 --> 1982 + bar [558.07, 1651.26, 1516.31, 1606.91, 78.72] ``` ## 🎯 Key Insights @@ -72,4 +72,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2025-11-18T00:28:14.219Z* +*Last generated: 2025-12-19T00:29:51.614Z* diff --git a/docs/docs/benchmarks/ScaleTests.md b/docs/docs/benchmarks/ScaleTests.md index 368e584fb2..31f420757a 100644 --- a/docs/docs/benchmarks/ScaleTests.md +++ b/docs/docs/benchmarks/ScaleTests.md @@ -7,20 +7,20 @@ sidebar_position: 6 # ScaleTests Benchmark :::info Last Updated -This benchmark was automatically generated on **2025-11-18** from the latest CI run. +This benchmark was automatically generated on **2025-12-19** from the latest CI run. -**Environment:** Ubuntu Latest • .NET SDK 10.0.100 +**Environment:** Ubuntu Latest • .NET SDK 10.0.101 ::: ## 📊 Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.2.3 | 541.57 ms | 540.90 ms | 3.848 ms | -| NUnit | 4.4.0 | 591.49 ms | 591.89 ms | 9.280 ms | -| MSTest | 4.0.2 | 512.41 ms | 507.96 ms | 11.331 ms | -| xUnit3 | 3.2.0 | 596.95 ms | 593.96 ms | 9.270 ms | -| **TUnit (AOT)** | 1.2.3 | 43.84 ms | 43.96 ms | 3.376 ms | +| **TUnit** | 1.5.70 | 528.92 ms | 529.16 ms | 3.466 ms | +| NUnit | 4.4.0 | 692.00 ms | 692.40 ms | 9.554 ms | +| MSTest | 4.0.2 | 583.69 ms | 584.50 ms | 18.412 ms | +| xUnit3 | 3.2.1 | 601.26 ms | 602.87 ms | 9.034 ms | +| **TUnit (AOT)** | 1.5.70 | 38.89 ms | 38.74 ms | 2.465 ms | ## 📈 Visual Comparison @@ -58,8 +58,8 @@ This benchmark was automatically generated on **2025-11-18** from the latest CI xychart-beta title "ScaleTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT"] - y-axis "Time (ms)" 0 --> 717 - bar [541.57, 591.49, 512.41, 596.95, 43.84] + y-axis "Time (ms)" 0 --> 831 + bar [528.92, 692, 583.69, 601.26, 38.89] ``` ## 🎯 Key Insights @@ -72,4 +72,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2025-11-18T00:28:14.219Z* +*Last generated: 2025-12-19T00:29:51.615Z* diff --git a/docs/docs/benchmarks/SetupTeardownTests.md b/docs/docs/benchmarks/SetupTeardownTests.md index fa7067085b..97381e6510 100644 --- a/docs/docs/benchmarks/SetupTeardownTests.md +++ b/docs/docs/benchmarks/SetupTeardownTests.md @@ -7,20 +7,20 @@ sidebar_position: 7 # SetupTeardownTests Benchmark :::info Last Updated -This benchmark was automatically generated on **2025-11-18** from the latest CI run. +This benchmark was automatically generated on **2025-12-19** from the latest CI run. -**Environment:** Ubuntu Latest • .NET SDK 10.0.100 +**Environment:** Ubuntu Latest • .NET SDK 10.0.101 ::: ## 📊 Results | Framework | Version | Mean | Median | StdDev | |-----------|---------|------|--------|--------| -| **TUnit** | 1.2.3 | 578.7 ms | 578.1 ms | 6.18 ms | -| NUnit | 4.4.0 | 1,182.7 ms | 1,182.8 ms | 8.39 ms | -| MSTest | 4.0.2 | 1,152.5 ms | 1,151.9 ms | 6.97 ms | -| xUnit3 | 3.2.0 | 1,229.3 ms | 1,225.5 ms | 9.31 ms | -| **TUnit (AOT)** | 1.2.3 | NA | NA | NA | +| **TUnit** | 1.5.70 | 602.4 ms | 598.3 ms | 11.72 ms | +| NUnit | 4.4.0 | 1,338.1 ms | 1,338.7 ms | 12.67 ms | +| MSTest | 4.0.2 | 1,180.3 ms | 1,176.7 ms | 10.05 ms | +| xUnit3 | 3.2.1 | 1,273.1 ms | 1,269.2 ms | 15.82 ms | +| **TUnit (AOT)** | 1.5.70 | NA | NA | NA | ## 📈 Visual Comparison @@ -58,8 +58,8 @@ This benchmark was automatically generated on **2025-11-18** from the latest CI xychart-beta title "SetupTeardownTests Performance Comparison" x-axis ["TUnit", "NUnit", "MSTest", "xUnit3", "TUnit_AOT"] - y-axis "Time (ms)" 0 --> 1476 - bar [578.7, 1182.7, 1152.5, 1229.3, 0] + y-axis "Time (ms)" 0 --> 1606 + bar [602.4, 1338.1, 1180.3, 1273.1, 0] ``` ## 🎯 Key Insights @@ -72,4 +72,4 @@ This benchmark compares TUnit's performance against NUnit, MSTest, xUnit3 using View the [benchmarks overview](/docs/benchmarks) for methodology details and environment information. ::: -*Last generated: 2025-11-18T00:28:14.220Z* +*Last generated: 2025-12-19T00:29:51.615Z* diff --git a/docs/docs/benchmarks/index.md b/docs/docs/benchmarks/index.md index 5dcade5491..0608586fad 100644 --- a/docs/docs/benchmarks/index.md +++ b/docs/docs/benchmarks/index.md @@ -7,9 +7,9 @@ sidebar_position: 1 # Performance Benchmarks :::info Last Updated -These benchmarks were automatically generated on **2025-11-18** from the latest CI run. +These benchmarks were automatically generated on **2025-12-19** from the latest CI run. -**Environment:** Ubuntu Latest • .NET SDK 10.0.100 +**Environment:** Ubuntu Latest • .NET SDK 10.0.101 ::: ## 🚀 Runtime Benchmarks @@ -37,8 +37,8 @@ These benchmarks compare TUnit against the most popular .NET testing frameworks: | Framework | Version Tested | |-----------|----------------| -| **TUnit** | 1.2.3 | -| **xUnit v3** | 3.2.0 | +| **TUnit** | 1.5.70 | +| **xUnit v3** | 3.2.1 | | **NUnit** | 4.4.0 | | **MSTest** | 4.0.2 | @@ -56,10 +56,10 @@ The benchmarks measure real-world testing patterns: ### Environment - **OS**: Ubuntu Latest (GitHub Actions) -- **Runtime**: .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 -- **SDK**: .NET SDK 10.0.100 +- **Runtime**: .NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3 +- **SDK**: .NET SDK 10.0.101 - **Hardware**: GitHub Actions Standard Runner (Ubuntu) -- **Tool**: BenchmarkDotNet v0.15.7, Linux Ubuntu 24.04.3 LTS (Noble Numbat) +- **Tool**: BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat) ### Why These Numbers Matter @@ -80,4 +80,4 @@ These benchmarks run automatically daily via [GitHub Actions](https://github.com Each benchmark runs multiple iterations with statistical analysis to ensure accuracy. Results may vary based on hardware and test characteristics. ::: -*Last generated: 2025-11-18T00:28:14.220Z* +*Last generated: 2025-12-19T00:29:51.616Z* diff --git a/docs/docs/examples/tunit-ci-pipeline.md b/docs/docs/examples/tunit-ci-pipeline.md index 07ea89d282..3d2bff9aab 100644 --- a/docs/docs/examples/tunit-ci-pipeline.md +++ b/docs/docs/examples/tunit-ci-pipeline.md @@ -331,7 +331,7 @@ test:integration: - build script: - dotnet test --configuration $BUILD_CONFIGURATION --no-build - --filter "Category=Integration" + -- --treenode-filter "/*/*/*/*[Category=Integration]" --report-trx --results-directory ./TestResults artifacts: when: always @@ -590,13 +590,18 @@ Run different test categories in separate jobs: ```yaml # Unit tests (fast) - name: Unit Tests - run: dotnet test --filter "Category=Unit" + run: dotnet test -- --treenode-filter "/*/*/*/*[Category=Unit]" # Integration tests (slower) - name: Integration Tests - run: dotnet test --filter "Category=Integration" + run: dotnet test -- --treenode-filter "/*/*/*/*[Category=Integration]" ``` +> **Note**: With .NET 10 SDK or newer, you can use the simpler syntax without the `--` separator: +> ```yaml +> run: dotnet test --treenode-filter "/**[Category=Unit]" +> ``` + ### Fail Fast in PRs Use fail-fast mode for quick feedback in pull requests: diff --git a/docs/docs/extensions/extensions.md b/docs/docs/extensions/extensions.md index fd78fc7d22..f8869d3589 100644 --- a/docs/docs/extensions/extensions.md +++ b/docs/docs/extensions/extensions.md @@ -108,7 +108,7 @@ Run your tests with the `--report-trx` flag: dotnet run --configuration Release --report-trx # Specify output location -dotnet run --configuration Release --report-trx --report-trx-filename ./reports/testresults.trx +dotnet run --configuration Release --results-directory ./reports --report-trx --report-trx-filename testresults.trx ``` **📚 More Resources:** diff --git a/docs/docs/getting-started/writing-your-first-test.md b/docs/docs/getting-started/writing-your-first-test.md index ef503ae7d3..346ce02d08 100644 --- a/docs/docs/getting-started/writing-your-first-test.md +++ b/docs/docs/getting-started/writing-your-first-test.md @@ -188,7 +188,7 @@ public class StringTests // Assert await Assert.That(result).IsEqualTo("HELLO"); - await Assert.That(result).HasLength(5); + await Assert.That(result).Length().IsEqualTo(5); await Assert.That(result.StartsWith("HE")).IsTrue(); } } diff --git a/docs/docs/guides/best-practices.md b/docs/docs/guides/best-practices.md index 6bc1304297..b67eec7ed8 100644 --- a/docs/docs/guides/best-practices.md +++ b/docs/docs/guides/best-practices.md @@ -665,7 +665,7 @@ TUnit is designed for performance at scale. Follow these guidelines to keep your - Let tests run in parallel (it's fast!) - Only use `[NotInParallel]` when absolutely necessary -- Configure parallelism based on your CPU: `[assembly: MaxParallelTests(Environment.ProcessorCount)]` +- Configure parallelism using the `--maximum-parallel-tests` CLI flag or `[assembly: ParallelLimiter]` attribute - Avoid expensive setup in `[Before(Test)]` hooks - use class or assembly-level hooks for shared resources ### Avoid Slow Operations in Tests diff --git a/docs/docs/guides/cookbook.md b/docs/docs/guides/cookbook.md index 95b0eaad8d..6721c8df26 100644 --- a/docs/docs/guides/cookbook.md +++ b/docs/docs/guides/cookbook.md @@ -89,7 +89,7 @@ public class OrderServiceTests }); await Assert.That(order.Id).IsGreaterThan(0); - await Assert.That(order.Items).HasCount().EqualTo(1); + await Assert.That(order.Items).Count().IsEqualTo(1); } } ``` @@ -616,7 +616,7 @@ public class OrderRepositoryIntegrationTests var loaded = await repository.GetWithItemsAsync(order.Id); await Assert.That(loaded).IsNotNull(); - await Assert.That(loaded!.Items).HasCount().EqualTo(2); + await Assert.That(loaded!.Items).Count().IsEqualTo(2); } } ``` diff --git a/docs/docs/migration/mstest.md b/docs/docs/migration/mstest.md index 05ef1b9e45..5559f6433f 100644 --- a/docs/docs/migration/mstest.md +++ b/docs/docs/migration/mstest.md @@ -300,6 +300,39 @@ public class MyTests } ``` +### Test Attachments + +```csharp +// MSTest +[TestMethod] +public void TestWithAttachment() +{ + // Test logic + var logPath = "test-log.txt"; + File.WriteAllText(logPath, "test logs"); + + TestContext.AddResultFile(logPath); +} + +// TUnit +[Test] +public async Task TestWithAttachment() +{ + // Test logic + var logPath = "test-log.txt"; + await File.WriteAllTextAsync(logPath, "test logs"); + + TestContext.Current!.Output.AttachArtifact(new Artifact + { + File = new FileInfo(logPath), + DisplayName = "Test Log", + Description = "Logs captured during test execution" // Optional + }); +} +``` + +For more information about working with test artifacts, including session-level artifacts and best practices, see the [Test Artifacts guide](../test-lifecycle/artifacts.md). + ### Assert.Fail ```csharp diff --git a/docs/docs/migration/nunit.md b/docs/docs/migration/nunit.md index ad278b1fcd..89c484d31f 100644 --- a/docs/docs/migration/nunit.md +++ b/docs/docs/migration/nunit.md @@ -25,6 +25,7 @@ Migrating from NUnit to TUnit can significantly improve test execution speed. Be | `Assert.That(actual, Is.EqualTo(expected))` | `await Assert.That(actual).IsEqualTo(expected)` | | `Assert.Throws(() => ...)` | `await Assert.ThrowsAsync(() => ...)` | | `TestContext.WriteLine(...)` | `TestContext` parameter with `context.OutputWriter.WriteLine(...)` | +| `TestContext.AddTestAttachment(path, name)` | `TestContext.Current!.Output.AttachArtifact(new Artifact { File = new FileInfo(path), DisplayName = name })` | | `CollectionAssert.AreEqual(expected, actual)` | `await Assert.That(actual).IsEquivalentTo(expected)` | | `StringAssert.Contains(substring, text)` | `await Assert.That(text).Contains(substring)` | @@ -137,7 +138,7 @@ await Assert.That(actual).IsEqualTo(expected); await Assert.That(value).IsTrue(); await Assert.That(value).IsNull(); await Assert.That(text).Contains("substring"); -await Assert.That(collection).HasCount().EqualTo(5); +await Assert.That(collection).Count().IsEqualTo(5); ``` ### Collection Assertions @@ -247,6 +248,39 @@ public async Task MyTest(TestContext context) } ``` +### Test Attachments + +```csharp +// NUnit +[Test] +public void TestWithAttachment() +{ + // Test logic + var logPath = "test-log.txt"; + File.WriteAllText(logPath, "test logs"); + + TestContext.AddTestAttachment(logPath, "Test Log"); +} + +// TUnit +[Test] +public async Task TestWithAttachment() +{ + // Test logic + var logPath = "test-log.txt"; + await File.WriteAllTextAsync(logPath, "test logs"); + + TestContext.Current!.Output.AttachArtifact(new Artifact + { + File = new FileInfo(logPath), + DisplayName = "Test Log", + Description = "Logs captured during test execution" // Optional + }); +} +``` + +For more information about working with test artifacts, including session-level artifacts and best practices, see the [Test Artifacts guide](../test-lifecycle/artifacts.md). + ### Combinatorial Testing #### Values and Combinatorial → Matrix @@ -610,7 +644,7 @@ public async Task ComplexAssertions() await Assert.That(text).Matches(@"^Hello"); // Collection assertions - await Assert.That(list).HasCount().EqualTo(5); + await Assert.That(list).Count().IsEqualTo(5); await Assert.That(list).Contains(3); await Assert.That(list).AllSatisfy(x => x > 0); await Assert.That(list).IsInAscendingOrder(); diff --git a/docs/docs/migration/xunit.md b/docs/docs/migration/xunit.md index 97a96bb507..c2e64b36a6 100644 --- a/docs/docs/migration/xunit.md +++ b/docs/docs/migration/xunit.md @@ -690,6 +690,57 @@ public class LoggingTests - Access output via `context.OutputWriter.WriteLine()` - TestContext provides additional test metadata +#### Test Attachments + +xUnit v3 introduced test attachments. TUnit also supports this capability: + +**xUnit v3 Code:** +```csharp +public class TestWithAttachments +{ + private readonly ITestContextAccessor _testContextAccessor; + + public TestWithAttachments(ITestContextAccessor testContextAccessor) + { + _testContextAccessor = testContextAccessor; + } + + [Fact] + public async Task Test_WithAttachment() + { + // Test logic + var logPath = "test-log.txt"; + await File.WriteAllTextAsync(logPath, "test logs"); + + _testContextAccessor.Current!.Attachments.Add( + new FileAttachment(logPath, "Test Log")); + } +} +``` + +**TUnit Equivalent:** +```csharp +public class TestWithAttachments +{ + [Test] + public async Task Test_WithAttachment() + { + // Test logic + var logPath = "test-log.txt"; + await File.WriteAllTextAsync(logPath, "test logs"); + + TestContext.Current!.Output.AttachArtifact(new Artifact + { + File = new FileInfo(logPath), + DisplayName = "Test Log", + Description = "Logs captured during test execution" // Optional + }); + } +} +``` + +For more information about working with test artifacts, including session-level artifacts and best practices, see the [Test Artifacts guide](../test-lifecycle/artifacts.md). + ### Traits and Categories #### Trait → Property @@ -790,7 +841,7 @@ public async Task Collection_Assertions() await Assert.That(list).DoesNotContain(5); await Assert.That(Array.Empty()).IsEmpty(); await Assert.That(list).IsNotEmpty(); - await Assert.That(list).HasCount().EqualTo(3); + await Assert.That(list).Count().IsEqualTo(3); } ``` diff --git a/docs/docs/test-authoring/depends-on.md b/docs/docs/test-authoring/depends-on.md index 0a066bb90d..c192670fcb 100644 --- a/docs/docs/test-authoring/depends-on.md +++ b/docs/docs/test-authoring/depends-on.md @@ -23,7 +23,7 @@ This takes a test name, which you can easily reference by using the `nameof(Test :::info -If you have multiple tests with the same name, but different parameter types, then you you must include the types of parameters in the `[DependsOn]` attribute too so TUnit can locate the correct test. +If you have multiple tests with the same name, but different parameter types, then you must include the types of parameters in the `[DependsOn]` attribute too so TUnit can locate the correct test. ::: @@ -101,9 +101,9 @@ public async Task AssertItemsInDatabase() ``` ## Getting other tests -If your tests depends on another test, it's possible to retrieve that test's context. This allows you to do things like check its result, or retrieve objects from its object bag. +If your tests depends on another test, it's possible to retrieve that test's context. This allows you to do things like check its result, or retrieve objects from its state bag. -This is done by calling the `GetTests` method on a `TestContext` object. It takes the test's method name (so you can use `nameof(...)`) and optionally the parameter types for if there's multiple overloads. +This is done by calling the `GetTests` method on the `TestContext.Dependencies` property. It takes the test's method name (so you can use `nameof(...)`) and optionally the parameter types for if there's multiple overloads. You'll notice this returns an array - This is because tests may be data driven and be invoked multiple times - If this is the case you'll have to find the one you want yourself. @@ -119,15 +119,15 @@ Example: public async Task AddItemToBag() { var itemId = await AddToBag(); - TestContext.Current!.StateBag.Add("ItemId", itemId); + TestContext.Current!.StateBag.Items["ItemId"] = itemId; } [Test] [DependsOn(nameof(AddItemToBag))] public async Task DeleteItemFromBag() { - var addToBagTestContext = TestContext.Current!.GetTests(nameof(AddItemToBag)).First(); - var itemId = addToBagTestContext.StateBag["ItemId"]; + var addToBagTestContext = TestContext.Current!.Dependencies.GetTests(nameof(AddItemToBag)).First(); + var itemId = addToBagTestContext.StateBag.Items["ItemId"]; await DeleteFromBag(itemId); } ``` diff --git a/docs/docs/test-authoring/method-data-source.md b/docs/docs/test-authoring/method-data-source.md index 05d58dbb7f..e1db097ae8 100644 --- a/docs/docs/test-authoring/method-data-source.md +++ b/docs/docs/test-authoring/method-data-source.md @@ -243,4 +243,33 @@ public class AsyncDataSourceTests :::tip Performance Note Async data sources are generated with strongly-typed delegates in AOT mode, providing excellent performance while maintaining full async/await support and cancellation token handling. ::: + +## Instance Method Data Sources + +In addition to static `MethodDataSource`, TUnit supports `InstanceMethodDataSource` for accessing instance members: + +```csharp +public class MyTests +{ + private IEnumerable TestData => new[] { 1, 2, 3 }; + + [Test] + [InstanceMethodDataSource(nameof(TestData))] + public async Task MyTest(int value) + { + await Assert.That(value).IsGreaterThan(0); + } +} +``` + +### Discovery Phase Considerations + +`InstanceMethodDataSource` is evaluated during **test discovery** (before test execution). If the instance property depends on async initialization via `IAsyncInitializer`, that initialization has **not yet run** during discovery, potentially resulting in empty data and no tests being generated. + +**Solutions:** + +1. **Preferred:** Return predefined values that don't require initialization +2. **Alternative:** Use `IAsyncDiscoveryInitializer` if you need discovery-time initialization + +See [Property Injection - Discovery Phase Initialization](../test-lifecycle/property-injection.md#discovery-phase-initialization) for detailed guidance and examples. EOF < /dev/null diff --git a/docs/docs/test-authoring/skip.md b/docs/docs/test-authoring/skip.md index 58d9ef2aa1..02d8387d03 100644 --- a/docs/docs/test-authoring/skip.md +++ b/docs/docs/test-authoring/skip.md @@ -64,3 +64,111 @@ public class MyTestClass { } ``` + +## Dynamic Skipping at Runtime + +Sometimes you need to determine whether to skip a test at runtime based on conditions that aren't known until the test executes. For this, you can use the static `Skip.Test(reason)` method. + +### Skip.Test() + +The `Skip.Test(reason)` method allows you to dynamically skip a test from within the test method or hooks. When called, it throws a `SkipTestException` that the test framework catches and marks the test as skipped. + +```csharp +using TUnit.Core; + +namespace MyTestProject; + +public class MyTestClass +{ + [Test] + public async Task MyTest() + { + var apiAvailable = await CheckApiAvailability(); + + if (!apiAvailable) + { + Skip.Test("API is not available"); + } + + // Test continues only if API is available + await CallApi(); + } +} +``` + + +### Skipping from Hooks + +You can also use `Skip.Test()` in test hooks to skip tests based on setup conditions: + +```csharp +using TUnit.Core; + +namespace MyTestProject; + +public class MyTestClass +{ + [Before(Test)] + public void BeforeEachTest() + { + var databaseAvailable = CheckDatabaseConnection(); + + if (!databaseAvailable) + { + Skip.Test("Database is not available"); + } + } + + [Test] + public void Test1() + { + // This test will be skipped if database is unavailable + } + + [Test] + public void Test2() + { + // This test will also be skipped if database is unavailable + } +} +``` + +You can skip all tests in a class from a `Before(Class)` hook: + +```csharp +using TUnit.Core; + +namespace MyTestProject; + +public class MyTestClass +{ + [Before(Class)] + public static void BeforeAllTests() + { + var serviceAvailable = CheckExternalService(); + + if (!serviceAvailable) + { + Skip.Test("External service is not available"); + } + } + + [Test] + public void Test1() + { + // All tests in this class will be skipped if service is unavailable + } +} +``` + +### When to Use Dynamic Skipping + +Use `Skip.Test()` and its variants when: +- The skip condition depends on runtime state (external services, environment variables, etc.) +- You need to perform some logic or API calls to determine if a test should run +- The skip decision is based on test setup or initialization results + +Use `[Skip]` attribute when: +- The skip condition is known at compile time or discovery time +- You want to skip tests based on static configuration or platform checks +- You need custom skip logic in a reusable attribute diff --git a/docs/docs/test-lifecycle/artifacts.md b/docs/docs/test-lifecycle/artifacts.md new file mode 100644 index 0000000000..ddba738162 --- /dev/null +++ b/docs/docs/test-lifecycle/artifacts.md @@ -0,0 +1,450 @@ +# Test Artifacts + +Test artifacts are files (screenshots, logs, videos, JSON dumps, etc.) that you can attach to your tests. They are invaluable for debugging test failures, especially in integration tests and end-to-end tests. + +TUnit supports attaching artifacts at two levels: +- **Test-level artifacts**: Attached to individual tests +- **Session-level artifacts**: Attached to the entire test session + +## Test-Level Artifacts + +Attach files to individual tests using `TestContext.Current.Output.AttachArtifact()`. + +### Basic Usage + +The simplest way to attach an artifact is by providing just the file path: + +```csharp +[Test] +public async Task MyIntegrationTest() +{ + // Perform your test logic + var result = await PerformOperation(); + + // Attach an artifact using the simple overload + TestContext.Current!.Output.AttachArtifact("path/to/logfile.log"); + + // Or with a custom display name and description + TestContext.Current!.Output.AttachArtifact( + "path/to/logfile.log", + displayName: "Application Logs", + description: "Logs captured during test execution" + ); + + await Assert.That(result).IsEqualTo(expected); +} +``` + +For more control, you can create an `Artifact` object directly: + +```csharp +[Test] +public async Task MyIntegrationTest() +{ + // Attach an artifact using the full Artifact object + TestContext.Current!.Output.AttachArtifact(new Artifact + { + File = new FileInfo("path/to/logfile.log"), + DisplayName = "Application Logs", + Description = "Logs captured during test execution" + }); +} +``` + +### Attaching Screenshots on Failure + +A common pattern is to capture a screenshot when a test fails: + +```csharp +public class MyTests +{ + [After(HookType.Test)] + public async Task TakeScreenshotOnFailure() + { + var testContext = TestContext.Current; + + if (testContext?.Result?.State == TestState.Failed) + { + // Capture screenshot + var screenshotPath = await CaptureScreenshot(); + + testContext.Output.AttachArtifact(new Artifact + { + File = new FileInfo(screenshotPath), + DisplayName = "Failure Screenshot", + Description = $"Screenshot captured when test '{testContext.TestDetails.TestName}' failed" + }); + } + } + + private async Task CaptureScreenshot() + { + // Your screenshot capture logic + var path = $"screenshots/test-{Guid.NewGuid()}.png"; + // ... capture screenshot to path ... + return path; + } +} +``` + +### Attaching Multiple Artifacts + +You can attach multiple artifacts to a single test: + +```csharp +[Test] +public async Task ComplexIntegrationTest() +{ + // Test logic that generates multiple outputs + var httpLog = await ExecuteHttpRequests(); + var dbLog = await QueryDatabase(); + var traceLog = await CollectTraces(); + + // Attach all artifacts + TestContext.Current!.Output.AttachArtifact(new Artifact + { + File = new FileInfo(httpLog), + DisplayName = "HTTP Requests", + Description = "All HTTP requests and responses" + }); + + TestContext.Current.Output.AttachArtifact(new Artifact + { + File = new FileInfo(dbLog), + DisplayName = "Database Queries", + Description = "All database queries executed" + }); + + TestContext.Current.Output.AttachArtifact(new Artifact + { + File = new FileInfo(traceLog), + DisplayName = "Trace Logs", + Description = "Application trace logs" + }); +} +``` + +## Session-Level Artifacts + +Attach files to the entire test session using `TestSessionContext.Current.AddArtifact()`. This is useful for artifacts that span multiple tests or provide context for the entire test run. + +### Basic Usage + +```csharp +[Before(HookType.TestSession)] +public static void SetupTestSession() +{ + // Start capturing session-wide logs + var sessionLogPath = "test-session-log.txt"; + StartLogging(sessionLogPath); + + // This artifact is available to the entire test session + TestSessionContext.Current!.AddArtifact(new Artifact + { + File = new FileInfo(sessionLogPath), + DisplayName = "Test Session Log", + Description = "Log file for the entire test session" + }); +} +``` + +### Configuration Files + +Attach configuration files to document the test environment: + +```csharp +[Before(HookType.TestSession)] +public static void DocumentTestEnvironment() +{ + // Attach environment configuration + TestSessionContext.Current!.AddArtifact(new Artifact + { + File = new FileInfo("appsettings.test.json"), + DisplayName = "Test Configuration", + Description = "Application configuration used for this test run" + }); + + // Attach environment info + var envInfo = CollectEnvironmentInfo(); + File.WriteAllText("environment-info.json", envInfo); + + TestSessionContext.Current.AddArtifact(new Artifact + { + File = new FileInfo("environment-info.json"), + DisplayName = "Environment Information", + Description = "System and runtime environment details" + }); +} +``` + +### Performance Reports + +Generate and attach performance reports for the entire test session: + +```csharp +[After(HookType.TestSession)] +public static void GeneratePerformanceReport() +{ + // Generate performance report after all tests complete + var reportPath = "performance-report.html"; + GenerateReport(reportPath); + + TestSessionContext.Current!.AddArtifact(new Artifact + { + File = new FileInfo(reportPath), + DisplayName = "Performance Report", + Description = "Performance metrics for all tests in this session" + }); +} +``` + +## Artifact Class + +The `Artifact` class has the following properties: + +```csharp +public class Artifact +{ + public required FileInfo File { get; init; } // The file to attach + public required string DisplayName { get; init; } // Human-readable name + public string? Description { get; init; } // Optional description +} +``` + +- **File**: A `FileInfo` object pointing to the file. The file must exist at the time of attachment. +- **DisplayName**: A short, descriptive name for the artifact (e.g., "Screenshot", "Logs", "Configuration"). +- **Description**: An optional longer description providing more context about the artifact. + +## Best Practices + +### 1. Clean Up Artifacts + +Consider cleaning up temporary artifact files after test execution to avoid accumulating files: + +```csharp +[After(HookType.TestSession)] +public static void CleanupArtifacts() +{ + var artifactDir = "test-artifacts"; + if (Directory.Exists(artifactDir)) + { + Directory.Delete(artifactDir, recursive: true); + } +} +``` + +### 2. Organize Artifacts by Test + +Create a unique directory for each test's artifacts: + +```csharp +[Before(HookType.Test)] +public void SetupTestArtifactDirectory() +{ + var testName = TestContext.Current!.TestDetails.TestName; + var sanitizedName = string.Concat(testName.Split(Path.GetInvalidFileNameChars())); + var artifactDir = Path.Combine("test-artifacts", sanitizedName); + Directory.CreateDirectory(artifactDir); + + TestContext.Current.StateBag["ArtifactDir"] = artifactDir; +} + +[Test] +public void MyTest() +{ + var artifactDir = (string)TestContext.Current!.StateBag["ArtifactDir"]; + var logPath = Path.Combine(artifactDir, "test.log"); + + // ... test logic ... + + TestContext.Current.Output.AttachArtifact(new Artifact + { + File = new FileInfo(logPath), + DisplayName = "Test Log" + }); +} +``` + +### 3. Only Attach on Failure + +For large artifacts (videos, extensive logs), consider only attaching them when tests fail: + +```csharp +[After(HookType.Test)] +public async Task ConditionalArtifactAttachment() +{ + var testContext = TestContext.Current; + + if (testContext?.Result?.State is TestState.Failed or TestState.TimedOut) + { + // Only attach expensive artifacts on failure + var videoPath = await StopRecording(); + + testContext.Output.AttachArtifact(new Artifact + { + File = new FileInfo(videoPath), + DisplayName = "Test Recording", + Description = "Video recording of the failed test" + }); + } +} +``` + +### 4. Use Descriptive Names + +Provide clear, descriptive names and descriptions for your artifacts: + +```csharp +// ❌ Not descriptive +TestContext.Current!.Output.AttachArtifact(new Artifact +{ + File = new FileInfo("log.txt"), + DisplayName = "Log" +}); + +// ✅ Descriptive and helpful +TestContext.Current!.Output.AttachArtifact(new Artifact +{ + File = new FileInfo("http-trace.log"), + DisplayName = "HTTP Request Trace", + Description = "Complete trace of all HTTP requests including headers and response bodies" +}); +``` + +### 5. Verify Files Exist + +Always ensure the file exists before attaching: + +```csharp +var logPath = "path/to/logfile.log"; + +if (File.Exists(logPath)) +{ + TestContext.Current!.Output.AttachArtifact(new Artifact + { + File = new FileInfo(logPath), + DisplayName = "Application Log" + }); +} +else +{ + TestContext.Current!.Output.WriteLine($"Warning: Log file not found at {logPath}"); +} +``` + +## Common Use Cases + +### Browser Testing with Playwright + +```csharp +[After(HookType.Test)] +public async Task CapturePlaywrightArtifacts() +{ + var testContext = TestContext.Current; + + if (testContext?.Result?.State != TestState.Passed) + { + // Capture screenshot + var screenshotPath = $"artifacts/screenshot-{testContext.Id}.png"; + await _page.ScreenshotAsync(new() { Path = screenshotPath }); + + testContext.Output.AttachArtifact(new Artifact + { + File = new FileInfo(screenshotPath), + DisplayName = "Browser Screenshot" + }); + + // Capture video if enabled + if (_browserContext.Options?.RecordVideo != null) + { + await _page.CloseAsync(); + var videoPath = await _page.Video!.PathAsync(); + + testContext.Output.AttachArtifact(new Artifact + { + File = new FileInfo(videoPath), + DisplayName = "Browser Recording" + }); + } + } +} +``` + +### API Testing + +```csharp +[Test] +public async Task ApiIntegrationTest() +{ + var requestLog = new StringBuilder(); + var responseLog = new StringBuilder(); + + // Make API calls while logging + var response = await _httpClient.GetAsync("/api/endpoint"); + requestLog.AppendLine($"GET /api/endpoint"); + responseLog.AppendLine(await response.Content.ReadAsStringAsync()); + + // Save and attach logs + var requestPath = "api-request.txt"; + var responsePath = "api-response.txt"; + + await File.WriteAllTextAsync(requestPath, requestLog.ToString()); + await File.WriteAllTextAsync(responsePath, responseLog.ToString()); + + TestContext.Current!.Output.AttachArtifact(new Artifact + { + File = new FileInfo(requestPath), + DisplayName = "API Request" + }); + + TestContext.Current.Output.AttachArtifact(new Artifact + { + File = new FileInfo(responsePath), + DisplayName = "API Response" + }); +} +``` + +### Database Testing + +```csharp +[Test] +public async Task DatabaseIntegrationTest() +{ + var queryLog = new List(); + + // Execute queries while logging + foreach (var query in _queries) + { + await _connection.ExecuteAsync(query); + queryLog.Add(query); + } + + // Save query log + var logPath = "database-queries.sql"; + await File.WriteAllLinesAsync(logPath, queryLog); + + TestContext.Current!.Output.AttachArtifact(new Artifact + { + File = new FileInfo(logPath), + DisplayName = "Database Queries", + Description = "All SQL queries executed during test" + }); +} +``` + +## Integration with Test Runners + +Artifacts attached using TUnit are automatically forwarded to the underlying Microsoft.Testing.Platform infrastructure, which makes them available to: + +- Test result files (TRX, etc.) +- CI/CD systems (GitHub Actions, Azure DevOps, etc.) +- Test explorers in IDEs (Visual Studio, Rider, VS Code) + +The exact behavior depends on your test runner configuration and CI/CD platform. + +## See Also + +- [Test Context](./test-context.md) - Overview of TestContext +- [Test Lifecycle Hooks](./setup.md) - Using Before/After hooks +- [CI/CD Reporting](../execution/ci-cd-reporting.md) - Integrating with CI systems diff --git a/docs/docs/test-lifecycle/event-subscribing.md b/docs/docs/test-lifecycle/event-subscribing.md index 1602e1c7c1..4588f92671 100644 --- a/docs/docs/test-lifecycle/event-subscribing.md +++ b/docs/docs/test-lifecycle/event-subscribing.md @@ -66,7 +66,7 @@ public class DatabaseConnectionAttribute : Attribute, ITestStartEventReceiver await _connection.OpenAsync(); // Store connection in test context for use by hooks and test - context.GetOrAdd("DbConnection", () => _connection); + context.StateBag.GetOrAdd("DbConnection", _ => _connection); } } @@ -77,7 +77,7 @@ public class MyTests public async Task TestWithDatabase() { // Database connection is already open and available - var connection = TestContext.Current!.Get("DbConnection"); + TestContext.Current!.StateBag.TryGetValue("DbConnection", out var connection); // ... test logic } @@ -85,7 +85,7 @@ public class MyTests public void BeforeTest() { // Database connection is already available here - var connection = TestContext.Current!.Get("DbConnection"); + TestContext.Current!.StateBag.TryGetValue("DbConnection", out var connection); // ... setup that needs the database } } @@ -128,7 +128,7 @@ public class DependencyInjectionClassConstructor : IClassConstructor, ITestEndEv { private readonly IServiceProvider _serviceProvider = CreateServiceProvider(); private AsyncServiceScope _scope; - + public Task Create([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type type, ClassConstructorMetadata classConstructorMetadata) { _scope = _serviceProvider.CreateAsyncScope(); @@ -138,7 +138,7 @@ public class DependencyInjectionClassConstructor : IClassConstructor, ITestEndEv } public ValueTask OnTestEnd(TestContext testContext) - { + { return _scope.DisposeAsync(); } diff --git a/docs/docs/test-lifecycle/lifecycle-overview.md b/docs/docs/test-lifecycle/lifecycle-overview.md new file mode 100644 index 0000000000..dd5d09bf43 --- /dev/null +++ b/docs/docs/test-lifecycle/lifecycle-overview.md @@ -0,0 +1,439 @@ +# Test Lifecycle Overview + +Understanding TUnit's complete test lifecycle helps you write effective tests and place setup/cleanup logic in the right place. TUnit provides multiple mechanisms for hooking into the lifecycle: + +1. **Hook Attributes** (`[Before]`, `[After]`, etc.) - Method-based hooks +2. **Event Receivers** (interfaces like `ITestStartEventReceiver`) - Object-based event subscriptions +3. **Initialization Interfaces** (`IAsyncInitializer`, `IAsyncDiscoveryInitializer`) - Async object setup +4. **Disposal Interfaces** (`IDisposable`, `IAsyncDisposable`) - Resource cleanup + +This page provides a complete visual overview of when each mechanism executes. + +## Complete Lifecycle Diagram + +```mermaid +flowchart TB + subgraph Discovery["1. Discovery Phase"] + direction TB + D1["[Before(TestDiscovery)]"] + D2["Scan assemblies for [Test] methods"] + D3["Create data sources & resolve property values"] + D3a["IAsyncDiscoveryInitializer.InitializeAsync()"] + D4["[After(TestDiscovery)]"] + D5["ITestRegisteredEventReceiver.OnTestRegistered()"] + D1 --> D2 --> D3 --> D3a --> D4 --> D5 + end + + subgraph Session["2. Test Session Execution"] + direction TB + S1["[Before(TestSession)]"] + S1a["IFirstTestInTestSessionEventReceiver"] + + subgraph Assembly["Per Assembly"] + direction TB + A1["[BeforeEvery(Assembly)] / [Before(Assembly)]"] + A1a["IFirstTestInAssemblyEventReceiver"] + + subgraph Class["Per Class"] + direction TB + C1["[BeforeEvery(Class)] / [Before(Class)]"] + C1a["IFirstTestInClassEventReceiver"] + + subgraph Test["Per Test Execution"] + direction TB + T0["Create test class instance (constructor)"] + T0a["Set injected property values on instance"] + T0b["IAsyncInitializer.InitializeAsync()"] + T1["[BeforeEvery(Test)]"] + T1a["ITestStartEventReceiver (Early)"] + T2["[Before(Test)]"] + T2a["ITestStartEventReceiver (Late)"] + T3["Test Body"] + T3a["ITestEndEventReceiver (Early)"] + T4["[After(Test)]"] + T4a["ITestEndEventReceiver (Late)"] + T5["[AfterEvery(Test)]"] + T6["IDisposable / IAsyncDisposable"] + T7["Cleanup tracked objects"] + + T0 --> T0a --> T0b --> T1 + T1 --> T1a --> T2 --> T2a --> T3 + T3 --> T3a --> T4 --> T4a --> T5 --> T6 --> T7 + end + + C2a["ILastTestInClassEventReceiver"] + C2["[After(Class)] / [AfterEvery(Class)]"] + C1 --> C1a --> Test --> C2a --> C2 + end + + A2a["ILastTestInAssemblyEventReceiver"] + A2["[After(Assembly)] / [AfterEvery(Assembly)]"] + A1 --> A1a --> Class --> A2a --> A2 + end + + S2a["ILastTestInTestSessionEventReceiver"] + S2["[After(TestSession)]"] + S1 --> S1a --> Assembly --> S2a --> S2 + end + + Discovery --> Session + + style D2 fill:#e1f5fe + style D3a fill:#fff3e0 + style T0b fill:#fff3e0 + style T3 fill:#c8e6c9 + style T6 fill:#ffcdd2 +``` + +## Phase 1: Test Discovery + +Before any tests execute, TUnit discovers all tests and prepares data sources. + +```mermaid +sequenceDiagram + participant Engine as TUnit Engine + participant Hooks as Hook Attributes + participant Data as Data Sources + participant Events as Event Receivers + + Engine->>Hooks: [Before(TestDiscovery)] + Engine->>Engine: Scan assemblies for [Test] methods + + loop For each data source + Engine->>Data: Create data source instance + Engine->>Data: Inject properties (resolve values) + Engine->>Data: IAsyncDiscoveryInitializer.InitializeAsync() + end + + Engine->>Hooks: [After(TestDiscovery)] + + loop For each discovered test + Engine->>Events: ITestRegisteredEventReceiver.OnTestRegistered() + end +``` + +### Discovery Phase Details + +| Step | What Happens | +|------|-------------| +| `[Before(TestDiscovery)]` | Hook runs once before discovery begins | +| **Scan Assemblies** | Find all methods with `[Test]` attribute | +| **Create Data Sources** | Instantiate `ClassDataSource`, resolve `MethodDataSource`, etc. | +| **Property Injection** | Resolve and cache property values for data sources | +| `IAsyncDiscoveryInitializer` | Initialize objects that need to be ready during discovery | +| `[After(TestDiscovery)]` | Hook runs once after discovery completes | +| `OnTestRegistered` | Event fires for each test after registration | + +:::warning Discovery vs Execution +`IAsyncInitializer` does **NOT** run during discovery. Only `IAsyncDiscoveryInitializer` runs at discovery time. + +Use `IAsyncDiscoveryInitializer` when your data source needs async initialization to generate test cases (e.g., loading test data from a database). +::: + +## Phase 2: Test Execution + +### Per-Test Execution Flow + +```mermaid +sequenceDiagram + participant Engine as TUnit Engine + participant Instance as Test Instance + participant Init as Initializers + participant Hooks as Hook Attributes + participant Events as Event Receivers + participant Dispose as Disposal + + Note over Engine: After BeforeClass hooks... + + Engine->>Instance: Create test class instance (constructor) + Engine->>Instance: Set cached property values on instance + Engine->>Init: IAsyncInitializer.InitializeAsync() for all tracked objects + + Engine->>Hooks: [BeforeEvery(Test)] + Engine->>Events: ITestStartEventReceiver (Early) + Engine->>Hooks: [Before(Test)] + Engine->>Events: ITestStartEventReceiver (Late) + + Engine->>Instance: Execute Test Body + + Engine->>Events: ITestEndEventReceiver (Early) + Engine->>Hooks: [After(Test)] + Engine->>Events: ITestEndEventReceiver (Late) + Engine->>Hooks: [AfterEvery(Test)] + + Engine->>Dispose: IAsyncDisposable.DisposeAsync() / IDisposable.Dispose() + Engine->>Engine: Cleanup tracked objects (decrement ref counts, dispose if 0) +``` + +### Complete Test Execution Order + +Here's the exact order of operations for a single test: + +| Order | What Happens | Type | +|-------|-------------|------| +| 1 | `[Before(TestSession)]` | Hook (once per session) | +| 2 | `IFirstTestInTestSessionEventReceiver` | Event (once per session) | +| 3 | `[BeforeEvery(Assembly)]` / `[Before(Assembly)]` | Hooks (once per assembly) | +| 4 | `IFirstTestInAssemblyEventReceiver` | Event (once per assembly) | +| 5 | `[BeforeEvery(Class)]` / `[Before(Class)]` | Hooks (once per class) | +| 6 | `IFirstTestInClassEventReceiver` | Event (once per class) | +| 7 | **Create test class instance** | Constructor runs | +| 8 | **Set property values on instance** | Cached values applied | +| 9 | **`IAsyncInitializer.InitializeAsync()`** | All tracked objects initialized | +| 10 | `[BeforeEvery(Test)]` | Hook | +| 11 | `ITestStartEventReceiver` (Early) | Event | +| 12 | `[Before(Test)]` | Hook (instance method) | +| 13 | `ITestStartEventReceiver` (Late) | Event | +| 14 | **Test Body Execution** | Your test code runs | +| 15 | `ITestEndEventReceiver` (Early) | Event | +| 16 | `[After(Test)]` | Hook (instance method) | +| 17 | `ITestEndEventReceiver` (Late) | Event | +| 18 | `[AfterEvery(Test)]` | Hook | +| 19 | **`IAsyncDisposable` / `IDisposable`** | Test instance disposed | +| 20 | **Cleanup tracked objects** | Ref count decremented, dispose if 0 | +| 21 | `ILastTestInClassEventReceiver` | Event (after last test in class) | +| 22 | `[After(Class)]` / `[AfterEvery(Class)]` | Hooks (after last test in class) | +| 23 | `ILastTestInAssemblyEventReceiver` | Event (after last test in assembly) | +| 24 | `[After(Assembly)]` / `[AfterEvery(Assembly)]` | Hooks (after last test in assembly) | +| 25 | `ILastTestInTestSessionEventReceiver` | Event (after last test in session) | +| 26 | `[After(TestSession)]` | Hook (once per session) | + +## Initialization Interfaces + +### IAsyncInitializer vs IAsyncDiscoveryInitializer + +```mermaid +flowchart LR + subgraph Discovery["Discovery Phase"] + D1["IAsyncDiscoveryInitializer.InitializeAsync()"] + end + + subgraph Execution["Execution Phase (after BeforeClass)"] + E1["IAsyncInitializer.InitializeAsync()"] + end + + D1 -.->|"Test cases generated"| Execution +``` + +| Interface | When It Runs | Use Case | +|-----------|-------------|----------| +| `IAsyncDiscoveryInitializer` | During test discovery | Loading data for test case generation | +| `IAsyncInitializer` | During test execution (after `[Before(Class)]`) | Starting containers, DB connections | + +### Initialization Order + +Objects are initialized **depth-first** (deepest nested objects first): + +```mermaid +flowchart TB + subgraph Init["Initialization Order"] + direction TB + I1["1. Deepest nested properties first"] + I2["2. Then their parent objects"] + I3["3. Finally the test class itself"] + end +``` + +```csharp +// If TestClass has PropertyA, and PropertyA has PropertyB... +// Initialization order: PropertyB → PropertyA → TestClass +``` + +## Disposal Interfaces + +### When Disposal Happens + +```mermaid +flowchart LR + subgraph AfterTest["After Each Test"] + A1["[After(Test)]"] + A2["[AfterEvery(Test)]"] + A3["Test Instance: IAsyncDisposable / IDisposable"] + end + + subgraph AfterScope["After Scope Ends"] + B1["Tracked objects with ref count = 0"] + B2["SharedType.PerClass objects after last test in class"] + B3["SharedType.PerAssembly objects after last test in assembly"] + B4["SharedType.PerTestSession objects after session"] + end + + A1 --> A2 --> A3 --> B1 +``` + +### Disposal by Sharing Type + +| SharedType | When Disposed | +|------------|--------------| +| `None` (default) | After each test | +| `PerClass` | After last test in the class | +| `PerAssembly` | After last test in the assembly | +| `PerTestSession` | After test session ends | +| `Keyed` | When all tests using that key complete | + +## Property Injection Lifecycle + +```mermaid +sequenceDiagram + participant Discovery as Discovery Phase + participant Registration as Test Registration + participant Execution as Test Execution + + Discovery->>Registration: Resolve property values + Registration->>Registration: Cache property values in test metadata + Registration->>Registration: Track objects for lifecycle management + + Note over Execution: For each test execution... + + Execution->>Execution: Create new test class instance + Execution->>Execution: Set cached property values on instance + Execution->>Execution: IAsyncInitializer.InitializeAsync() + Execution->>Execution: Run test + Execution->>Execution: Decrement ref counts, dispose if needed +``` + +### Key Points + +1. **Property values are resolved once** during test registration +2. **Shared objects** (`PerClass`, `PerAssembly`, etc.) are created once and reused +3. **Each test gets a new instance** of the test class +4. **Cached values are set** on each new test instance +5. **`IAsyncInitializer`** runs after `[Before(Class)]` hooks + +## Event Receiver Interfaces + +### All Event Receiver Interfaces + +| Interface | When Fired | Context | +|-----------|------------|---------| +| `ITestRegisteredEventReceiver` | After test discovered | `TestRegisteredContext` | +| `IFirstTestInTestSessionEventReceiver` | Before first test in session | `TestSessionContext` | +| `IFirstTestInAssemblyEventReceiver` | Before first test in assembly | `AssemblyHookContext` | +| `IFirstTestInClassEventReceiver` | Before first test in class | `ClassHookContext` | +| `ITestStartEventReceiver` | When test begins | `TestContext` | +| `ITestEndEventReceiver` | When test completes | `TestContext` | +| `ITestSkippedEventReceiver` | When test is skipped | `TestContext` | +| `ILastTestInClassEventReceiver` | After last test in class | `ClassHookContext` | +| `ILastTestInAssemblyEventReceiver` | After last test in assembly | `AssemblyHookContext` | +| `ILastTestInTestSessionEventReceiver` | After last test in session | `TestSessionContext` | + +### Early vs Late Stage + +For `ITestStartEventReceiver` and `ITestEndEventReceiver`: + +```mermaid +flowchart LR + subgraph TestStart["Test Start"] + A1["[BeforeEvery(Test)]"] + A2["ITestStartEventReceiver (Early)"] + A3["[Before(Test)]"] + A4["ITestStartEventReceiver (Late)"] + end + + subgraph TestEnd["Test End"] + B1["ITestEndEventReceiver (Early)"] + B2["[After(Test)]"] + B3["ITestEndEventReceiver (Late)"] + B4["[AfterEvery(Test)]"] + end + + TestStart --> TestEnd +``` + +```csharp +public class MyAttribute : Attribute, ITestStartEventReceiver +{ + // Early = runs BEFORE [Before(Test)] + // Late (default) = runs AFTER [Before(Test)] + public EventReceiverStage Stage => EventReceiverStage.Early; + + public ValueTask OnTestStart(TestContext context) => ValueTask.CompletedTask; +} +``` + +## Hook Attributes Reference + +### All Hook Types + +| Level | Before | After | Method Type | +|-------|--------|-------|-------------| +| Test Discovery | `[Before(TestDiscovery)]` | `[After(TestDiscovery)]` | Static | +| Test Session | `[Before(TestSession)]` | `[After(TestSession)]` | Static | +| Assembly | `[Before(Assembly)]` | `[After(Assembly)]` | Static | +| Class | `[Before(Class)]` | `[After(Class)]` | Static | +| Test | `[Before(Test)]` | `[After(Test)]` | **Instance** | + +### Before vs BeforeEvery + +| Attribute | Scope | +|-----------|-------| +| `[Before(Class)]` | Once for **this class only** | +| `[BeforeEvery(Class)]` | Before **every class** in session | +| `[Before(Test)]` | Before **each test in this class** | +| `[BeforeEvery(Test)]` | Before **every test** in session | + +## Quick Reference + +``` +┌─ DISCOVERY ──────────────────────────────────────────────────────┐ +│ [Before(TestDiscovery)] │ +│ → Scan assemblies for [Test] methods │ +│ → Create data sources, inject properties │ +│ → IAsyncDiscoveryInitializer.InitializeAsync() │ +│ [After(TestDiscovery)] │ +│ → ITestRegisteredEventReceiver.OnTestRegistered (per test) │ +└──────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─ TEST SESSION ───────────────────────────────────────────────────┐ +│ [Before(TestSession)] → IFirstTestInTestSessionEventReceiver │ +│ │ │ +│ ├─ [Before(Assembly)] → IFirstTestInAssemblyEventReceiver │ +│ │ │ │ +│ │ ├─ [Before(Class)] → IFirstTestInClassEventReceiver │ +│ │ │ │ │ +│ │ │ │ ┌─ PER TEST ─────────────────────────────────────┐ │ +│ │ │ │ │ Create instance (constructor) │ │ +│ │ │ │ │ Set property values │ │ +│ │ │ │ │ IAsyncInitializer.InitializeAsync() │ │ +│ │ │ │ │ [BeforeEvery(Test)] │ │ +│ │ │ │ │ ITestStartEventReceiver (Early) │ │ +│ │ │ │ │ [Before(Test)] │ │ +│ │ │ │ │ ITestStartEventReceiver (Late) │ │ +│ │ │ │ │ ─────────── TEST BODY ─────────── │ │ +│ │ │ │ │ ITestEndEventReceiver (Early) │ │ +│ │ │ │ │ [After(Test)] │ │ +│ │ │ │ │ ITestEndEventReceiver (Late) │ │ +│ │ │ │ │ [AfterEvery(Test)] │ │ +│ │ │ │ │ IAsyncDisposable / IDisposable │ │ +│ │ │ │ │ Cleanup tracked objects │ │ +│ │ │ │ └─────────────────────────────────────────────────┘ │ +│ │ │ │ │ +│ │ │ ├─ ILastTestInClassEventReceiver → [After(Class)] │ +│ │ │ │ +│ │ ├─ ILastTestInAssemblyEventReceiver → [After(Assembly)] │ +│ │ │ +│ ├─ ILastTestInTestSessionEventReceiver → [After(TestSession)] │ +└───────────────────────────────────────────────────────────────────┘ +``` + +## Exception Handling + +:::tip Cleanup Always Runs +All `[After]` hooks, `ITestEndEventReceiver` events, and disposal methods run even if earlier ones fail. Exceptions are collected and thrown together. +::: + +| Phase | Behavior | +|-------|----------| +| Before hooks | Fail fast (exception stops execution) | +| After hooks | Run all, collect exceptions | +| Disposal | Always runs, exceptions collected | + +## Related Pages + +- [Test Set Ups](setup.md) - Detailed guide to `[Before]` hooks +- [Test Clean Ups](cleanup.md) - Detailed guide to `[After]` hooks +- [Event Subscribing](event-subscribing.md) - Event receiver interfaces +- [Property Injection](property-injection.md) - Property injection and `IAsyncInitializer` +- [Dependency Injection](dependency-injection.md) - DI integration +- [Test Context](test-context.md) - Accessing test information diff --git a/docs/docs/test-lifecycle/property-injection.md b/docs/docs/test-lifecycle/property-injection.md index e770ab1fc1..691af95f2f 100644 --- a/docs/docs/test-lifecycle/property-injection.md +++ b/docs/docs/test-lifecycle/property-injection.md @@ -20,6 +20,19 @@ The AOT system generates strongly-typed property setters at compile time, elimin Properties can implement `IAsyncInitializer` for complex setup scenarios with automatic lifecycle management: +:::warning Discovery Phase vs Execution Phase +`IAsyncInitializer` runs during **test execution**, not during **test discovery**. + +Test discovery happens when: +- Your IDE loads/reloads the project +- You run `dotnet run --list-tests` +- CI/CD systems enumerate tests before running them + +During discovery, `IAsyncInitializer` has **not yet run**, so properties will be uninitialized. If you use `InstanceMethodDataSource` or similar features that access instance properties during discovery, you may get empty data or no tests generated. + +**When you need discovery-time initialization**, use `IAsyncDiscoveryInitializer` instead (see [Discovery Phase Initialization](#discovery-phase-initialization) below). +::: + ```csharp using TUnit.Core; @@ -46,6 +59,164 @@ public class AsyncPropertyExample : IAsyncInitializer, IAsyncDisposable } ``` +## Discovery Phase Initialization + +### When Discovery Initialization is Needed + +Most tests don't need discovery-time initialization. Discovery-time initialization is only required when: + +1. You use `InstanceMethodDataSource` and the data source method returns **dynamically loaded data** (not predefined values) +2. The test case enumeration depends on async-loaded data (e.g., querying a database for test cases) + +**Performance Note:** Discovery happens frequently (IDE reloads, project switches, `--list-tests`), so discovery-time initialization runs more often than test execution. Avoid expensive operations in `IAsyncDiscoveryInitializer` when possible. + +### Using IAsyncDiscoveryInitializer + +When you need data available during discovery, implement `IAsyncDiscoveryInitializer` instead of `IAsyncInitializer`: + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +```csharp +// This fixture's data won't be available during discovery +public class TestDataFixture : IAsyncInitializer, IAsyncDisposable +{ + private List _testCases = []; + + public async Task InitializeAsync() + { + // This runs during EXECUTION, not DISCOVERY + _testCases = await LoadTestCasesFromDatabaseAsync(); + } + + // This will return empty list during discovery! + public IEnumerable GetTestCases() => _testCases; + + public async ValueTask DisposeAsync() + { + _testCases.Clear(); + } +} + +public class MyTests +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required TestDataFixture Fixture { get; init; } + + // During discovery, Fixture.GetTestCases() returns empty list + // Result: No tests are generated! + public IEnumerable TestCases => Fixture.GetTestCases(); + + [Test] + [InstanceMethodDataSource(nameof(TestCases))] + public async Task MyTest(string testCase) + { + // This test is never created because TestCases was empty during discovery + await Assert.That(testCase).IsNotNullOrEmpty(); + } +} +``` + + + + +```csharp +// This fixture's data IS available during discovery +public class TestDataFixture : IAsyncDiscoveryInitializer, IAsyncDisposable +{ + private List _testCases = []; + + public async Task InitializeAsync() + { + // This runs during DISCOVERY, before test enumeration + _testCases = await LoadTestCasesFromDatabaseAsync(); + } + + // This returns populated list during discovery! + public IEnumerable GetTestCases() => _testCases; + + public async ValueTask DisposeAsync() + { + _testCases.Clear(); + } +} + +public class MyTests +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required TestDataFixture Fixture { get; init; } + + // During discovery, Fixture is already initialized + // Result: Tests are generated successfully! + public IEnumerable TestCases => Fixture.GetTestCases(); + + [Test] + [InstanceMethodDataSource(nameof(TestCases))] + public async Task MyTest(string testCase) + { + // This test IS created with each test case from the fixture + await Assert.That(testCase).IsNotNullOrEmpty(); + } +} +``` + + + + +```csharp +// Best approach: Predefined test case IDs, expensive initialization during execution only +public class TestDataFixture : IAsyncInitializer, IAsyncDisposable +{ + // Test case IDs are predefined - no initialization needed for discovery + private static readonly string[] PredefinedTestCases = ["Case1", "Case2", "Case3"]; + + private DockerContainer? _container; + + public async Task InitializeAsync() + { + // Expensive initialization runs during EXECUTION only + _container = await StartDockerContainerAsync(); + } + + // Returns predefined IDs - works during discovery without initialization + public IEnumerable GetTestCaseIds() => PredefinedTestCases; + + public async ValueTask DisposeAsync() + { + if (_container != null) + await _container.DisposeAsync(); + } +} + +public class MyTests +{ + [ClassDataSource(Shared = SharedType.PerClass)] + public required TestDataFixture Fixture { get; init; } + + // Returns predefined IDs - no initialization required during discovery + public IEnumerable TestCases => Fixture.GetTestCaseIds(); + + [Test] + [InstanceMethodDataSource(nameof(TestCases))] + public async Task MyTest(string testCaseId) + { + // Fixture IS initialized by the time the test runs + // Can now use the expensive resources (Docker container, etc.) + await Assert.That(testCaseId).IsNotNullOrEmpty(); + } +} +``` + + + + +**Recommendation:** Prefer the "predefined data" approach when possible. This avoids expensive initialization during discovery, which happens frequently (IDE reloads, `--list-tests`, etc.). + +For more troubleshooting, see [Test Discovery Issues](../troubleshooting.md#test-discovery-issues). + ## Basic Property Injection Examples ```csharp @@ -94,12 +265,8 @@ public class PropertySetterTests Console.WriteLine($"Property7: {Property7}"); } - // Static data source method for Property2 - public static IEnumerable GetMethodData() - { - yield return "method_data_1"; - yield return "method_data_2"; - } + // Static data source method for Property2 - returns a single value for property injection + public static string GetMethodData() => "method_data_value"; } // Example model for ClassDataSource diff --git a/docs/docs/test-lifecycle/test-context.md b/docs/docs/test-lifecycle/test-context.md index 5292cce789..d73b8b7681 100644 --- a/docs/docs/test-lifecycle/test-context.md +++ b/docs/docs/test-lifecycle/test-context.md @@ -28,6 +28,35 @@ if (TestContext.Current?.Result?.State == TestState.Failed) } ``` +## Test Output and Artifacts + +The `TestContext` provides multiple ways to write output and attach artifacts: + +```csharp +// Write to standard output (modern interface-based approach) +TestContext.Current!.Output.WriteLine("Debug information"); + +// Alternative: Direct TextWriter access (also valid) +TestContext.Current!.OutputWriter.WriteLine("Debug information"); + +// Write to error output +TestContext.Current.Output.WriteError("Warning: something unexpected happened"); + +// Attach an artifact (file, screenshot, log, etc.) +TestContext.Current.Output.AttachArtifact(new Artifact +{ + File = new FileInfo("path/to/logfile.log"), + DisplayName = "Application Logs", + Description = "Logs captured during test execution" +}); +``` + +Both `Output.WriteLine()` and `OutputWriter.WriteLine()` are valid - the `Output` property provides a convenient interface-based API, while `OutputWriter` gives direct access to the underlying TextWriter. + +Artifacts are particularly useful for debugging test failures, especially in integration tests. You can attach screenshots, logs, videos, configuration files, or any other files that help diagnose issues. + +For complete information about working with test artifacts, including session-level artifacts, best practices, and common use cases, see the [Test Artifacts](./artifacts.md) guide. + ## Dependency Injection **Note**: `TestContext` does NOT provide direct access to dependency injection services. The internal service provider in `TestContext` is exclusively for TUnit framework services and is not meant for user-provided dependencies. diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index b0f44ddad1..97bc0074f6 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -230,6 +230,52 @@ public void MyTest() { } [MethodDataSource(nameof(DataClass.GetData))] ``` +### Tests Not Generated When Using InstanceMethodDataSource + +**Symptom:** When using `InstanceMethodDataSource` with a property from a `ClassDataSource` fixture, no tests appear in the test explorer or `--list-tests` output. + +**Cause:** The fixture implements `IAsyncInitializer`, which runs during test **execution**, not during test **discovery**. When TUnit enumerates test cases during discovery, the fixture hasn't been initialized yet, so the data source returns empty data. + +**Solution 1 (Recommended):** Return predefined test case identifiers that don't require initialization: + +```csharp +public class Fixture : IAsyncInitializer // ✅ Stays as IAsyncInitializer +{ + // Predefined IDs - available without initialization + private static readonly string[] TestCaseIds = ["Case1", "Case2", "Case3"]; + + public async Task InitializeAsync() + { + // Expensive setup runs during execution only + await StartDockerContainerAsync(); + } + + // Returns predefined IDs - works during discovery + public IEnumerable GetTestCaseIds() => TestCaseIds; +} +``` + +**Solution 2:** Use `IAsyncDiscoveryInitializer` if you truly need data loaded during discovery: + +```csharp +public class Fixture : IAsyncDiscoveryInitializer // Changed from IAsyncInitializer +{ + private List _testCases = []; + + public async Task InitializeAsync() + { + // Now runs during discovery + _testCases = await LoadTestCasesAsync(); + } + + public IEnumerable GetTestCases() => _testCases; +} +``` + +**Performance note:** Solution 1 is preferred because discovery happens frequently (IDE reloads, project switches, `--list-tests`), and you want to avoid expensive operations during discovery when possible. + +For detailed examples, see [Property Injection - Discovery Phase Initialization](test-lifecycle/property-injection.md#discovery-phase-initialization). + ## Test Execution Issues ### Tests Hanging or Deadlocking @@ -463,7 +509,7 @@ await Assert.That(actual).IsEquivalentTo(expected, CollectionOrdering.Matching); Or assert on elements individually: ```csharp -await Assert.That(actual).HasCount().EqualTo(expected.Length); +await Assert.That(actual).Count().IsEqualTo(expected.Length); for (int i = 0; i < expected.Length; i++) { await Assert.That(actual[i]).IsEqualTo(expected[i]); @@ -483,7 +529,7 @@ var expected = new[] await Assert.That(actual).IsEquivalentTo(expected); // More reliable - assert on properties -await Assert.That(actual).HasCount().EqualTo(2); +await Assert.That(actual).Count().IsEqualTo(2); await Assert.That(actual[0].Name).IsEqualTo("Alice"); await Assert.That(actual[1].Name).IsEqualTo("Bob"); @@ -502,7 +548,7 @@ var actual = new[] { (1, "a"), (2, "b") }; // await Assert.That(actual).IsEquivalentTo(expected); // Workaround - assert individual elements -await Assert.That(actual).HasCount().EqualTo(2); +await Assert.That(actual).Count().IsEqualTo(2); await Assert.That(actual[0]).IsEqualTo((1, "a")); await Assert.That(actual[1]).IsEqualTo((2, "b")); ``` @@ -516,7 +562,7 @@ var list = new List { 1, 2, 3 }; await Assert.That(list).IsEquivalentTo(new[] { 1, 2, 3 }); // Check specific properties -await Assert.That(list).HasCount().EqualTo(3); +await Assert.That(list).Count().IsEqualTo(3); await Assert.That(list).Contains(2); await Assert.That(list).DoesNotContain(5); ``` diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index d9b66cd3c6..5c2a8f85c9 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -31,6 +31,10 @@ const config: Config = { locales: ['en'], }, + plugins: [ + 'docusaurus-plugin-llms', + ], + presets: [ [ 'classic', @@ -105,6 +109,11 @@ const config: Config = { label: 'GitHub', position: 'right', }, + { + href: 'https://github.com/sponsors/thomhurst', + label: '❤️ Sponsor', + position: 'right', + }, ], }, footer: { diff --git a/docs/package.json b/docs/package.json index 4aab3e1654..ae51cac527 100644 --- a/docs/package.json +++ b/docs/package.json @@ -18,16 +18,17 @@ "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", "@docusaurus/theme-mermaid": "^3.9.2", - "@mdx-js/react": "^3.0.0", + "@mdx-js/react": "^3.1.1", "clsx": "^2.1.1", "prism-react-renderer": "^2.4.0", - "react": "^19.0.0", + "react": "^19.2.0", "react-dom": "^19.0.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "3.9.2", "@docusaurus/tsconfig": "3.9.2", "@docusaurus/types": "3.9.2", + "docusaurus-plugin-llms": "^0.2.2", "typescript": "~5.9.0" }, "browserslist": { diff --git a/docs/sidebars.ts b/docs/sidebars.ts index 5e446d9f36..e87e4d3b52 100644 --- a/docs/sidebars.ts +++ b/docs/sidebars.ts @@ -46,9 +46,11 @@ const sidebars: SidebarsConfig = { label: 'Core Concepts', collapsed: false, items: [ + 'test-lifecycle/lifecycle-overview', 'test-lifecycle/setup', 'test-lifecycle/cleanup', 'test-lifecycle/test-context', + 'test-lifecycle/artifacts', 'test-lifecycle/properties', 'test-lifecycle/property-injection', 'test-lifecycle/event-subscribing', diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index 0242064ad4..38695fbafe 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -75,6 +75,12 @@ public async Task MyTest() View Tutorial + + ❤️ Sponsor +
diff --git a/docs/static/benchmarks/AsyncTests.json b/docs/static/benchmarks/AsyncTests.json index a7ec56b194..b3ce1ba857 100644 --- a/docs/static/benchmarks/AsyncTests.json +++ b/docs/static/benchmarks/AsyncTests.json @@ -1,51 +1,51 @@ { - "timestamp": "2025-11-18T00:28:14.218Z", + "timestamp": "2025-12-19T00:29:51.613Z", "category": "AsyncTests", "environment": { - "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.7, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", - "sdk": ".NET SDK 10.0.100", - "host": ".NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3" + "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", + "sdk": ".NET SDK 10.0.101", + "host": ".NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3" }, "results": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "556.5 ms", - "Error": "2.29 ms", - "StdDev": "2.03 ms", - "Median": "556.3 ms" + "Version": "1.5.70", + "Mean": "584.8 ms", + "Error": "6.94 ms", + "StdDev": "6.49 ms", + "Median": "585.8 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "663.3 ms", - "Error": "10.81 ms", - "StdDev": "9.02 ms", - "Median": "662.2 ms" + "Mean": "771.5 ms", + "Error": "9.31 ms", + "StdDev": "8.25 ms", + "Median": "770.5 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "635.0 ms", - "Error": "12.02 ms", - "StdDev": "11.80 ms", - "Median": "632.0 ms" + "Mean": "647.1 ms", + "Error": "7.82 ms", + "StdDev": "7.31 ms", + "Median": "646.7 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "715.8 ms", - "Error": "9.34 ms", - "StdDev": "8.28 ms", - "Median": "716.7 ms" + "Version": "3.2.1", + "Mean": "725.9 ms", + "Error": "11.34 ms", + "StdDev": "10.05 ms", + "Median": "723.1 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "124.5 ms", - "Error": "0.31 ms", - "StdDev": "0.26 ms", - "Median": "124.5 ms" + "Version": "1.5.70", + "Mean": "124.6 ms", + "Error": "0.50 ms", + "StdDev": "0.47 ms", + "Median": "124.7 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/BuildTime.json b/docs/static/benchmarks/BuildTime.json index a53dfb6c92..fb96fdc0a3 100644 --- a/docs/static/benchmarks/BuildTime.json +++ b/docs/static/benchmarks/BuildTime.json @@ -1,43 +1,43 @@ { - "timestamp": "2025-11-18T00:28:14.220Z", + "timestamp": "2025-12-19T00:29:51.615Z", "category": "BuildTime", "environment": { - "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.7, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", - "sdk": ".NET SDK 10.0.100", - "host": ".NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3" + "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", + "sdk": ".NET SDK 10.0.101", + "host": ".NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3" }, "results": [ { "Method": "Build_TUnit", - "Version": "1.2.3", - "Mean": "1.947 s", - "Error": "0.0371 s", - "StdDev": "0.0381 s", - "Median": "1.953 s" + "Version": "1.5.70", + "Mean": "2.051 s", + "Error": "0.0337 s", + "StdDev": "0.0315 s", + "Median": "2.049 s" }, { "Method": "Build_NUnit", "Version": "4.4.0", - "Mean": "1.556 s", - "Error": "0.0183 s", - "StdDev": "0.0171 s", - "Median": "1.557 s" + "Mean": "1.649 s", + "Error": "0.0255 s", + "StdDev": "0.0239 s", + "Median": "1.645 s" }, { "Method": "Build_MSTest", "Version": "4.0.2", - "Mean": "1.621 s", - "Error": "0.0167 s", - "StdDev": "0.0156 s", - "Median": "1.624 s" + "Mean": "1.745 s", + "Error": "0.0341 s", + "StdDev": "0.0350 s", + "Median": "1.732 s" }, { "Method": "Build_xUnit3", - "Version": "3.2.0", - "Mean": "1.523 s", - "Error": "0.0173 s", - "StdDev": "0.0162 s", - "Median": "1.527 s" + "Version": "3.2.1", + "Mean": "1.623 s", + "Error": "0.0231 s", + "StdDev": "0.0216 s", + "Median": "1.624 s" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/DataDrivenTests.json b/docs/static/benchmarks/DataDrivenTests.json index 482e01a132..5fc1454e51 100644 --- a/docs/static/benchmarks/DataDrivenTests.json +++ b/docs/static/benchmarks/DataDrivenTests.json @@ -1,51 +1,51 @@ { - "timestamp": "2025-11-18T00:28:14.219Z", + "timestamp": "2025-12-19T00:29:51.614Z", "category": "DataDrivenTests", "environment": { - "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.7, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", - "sdk": ".NET SDK 10.0.100", - "host": ".NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3" + "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", + "sdk": ".NET SDK 10.0.101", + "host": ".NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3" }, "results": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "493.26 ms", - "Error": "3.582 ms", - "StdDev": "3.175 ms", - "Median": "492.16 ms" + "Version": "1.5.70", + "Mean": "510.15 ms", + "Error": "5.720 ms", + "StdDev": "5.350 ms", + "Median": "510.45 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "601.85 ms", - "Error": "11.895 ms", - "StdDev": "13.698 ms", - "Median": "601.22 ms" + "Mean": "664.02 ms", + "Error": "7.163 ms", + "StdDev": "6.350 ms", + "Median": "664.38 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "617.89 ms", - "Error": "11.698 ms", - "StdDev": "12.013 ms", - "Median": "622.69 ms" + "Mean": "624.22 ms", + "Error": "9.080 ms", + "StdDev": "8.050 ms", + "Median": "621.89 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "610.52 ms", - "Error": "11.777 ms", - "StdDev": "11.016 ms", - "Median": "609.06 ms" + "Version": "3.2.1", + "Mean": "625.91 ms", + "Error": "12.339 ms", + "StdDev": "13.715 ms", + "Median": "622.72 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "24.33 ms", - "Error": "0.263 ms", - "StdDev": "0.233 ms", - "Median": "24.31 ms" + "Version": "1.5.70", + "Mean": "25.72 ms", + "Error": "0.301 ms", + "StdDev": "0.282 ms", + "Median": "25.63 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/MassiveParallelTests.json b/docs/static/benchmarks/MassiveParallelTests.json index 8519b4b264..50a6acc59a 100644 --- a/docs/static/benchmarks/MassiveParallelTests.json +++ b/docs/static/benchmarks/MassiveParallelTests.json @@ -1,51 +1,51 @@ { - "timestamp": "2025-11-18T00:28:14.219Z", + "timestamp": "2025-12-19T00:29:51.614Z", "category": "MassiveParallelTests", "environment": { - "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.7, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", - "sdk": ".NET SDK 10.0.100", - "host": ".NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3" + "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", + "sdk": ".NET SDK 10.0.101", + "host": ".NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3" }, "results": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "595.5 ms", - "Error": "2.81 ms", - "StdDev": "2.63 ms", - "Median": "595.7 ms" + "Version": "1.5.70", + "Mean": "599.4 ms", + "Error": "3.41 ms", + "StdDev": "3.19 ms", + "Median": "599.3 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "1,164.1 ms", - "Error": "6.78 ms", - "StdDev": "6.01 ms", - "Median": "1,163.4 ms" + "Mean": "1,287.1 ms", + "Error": "10.49 ms", + "StdDev": "9.81 ms", + "Median": "1,285.7 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "2,947.1 ms", - "Error": "6.79 ms", - "StdDev": "6.02 ms", - "Median": "2,948.2 ms" + "Mean": "2,971.9 ms", + "Error": "7.15 ms", + "StdDev": "6.34 ms", + "Median": "2,972.6 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "3,048.5 ms", - "Error": "15.99 ms", - "StdDev": "14.96 ms", - "Median": "3,046.6 ms" + "Version": "3.2.1", + "Mean": "3,072.8 ms", + "Error": "8.31 ms", + "StdDev": "7.78 ms", + "Median": "3,069.3 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "130.9 ms", - "Error": "0.44 ms", - "StdDev": "0.41 ms", - "Median": "130.9 ms" + "Version": "1.5.70", + "Mean": "131.6 ms", + "Error": "0.62 ms", + "StdDev": "0.58 ms", + "Median": "131.8 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/MatrixTests.json b/docs/static/benchmarks/MatrixTests.json index d48c2a3178..c335e858c9 100644 --- a/docs/static/benchmarks/MatrixTests.json +++ b/docs/static/benchmarks/MatrixTests.json @@ -1,51 +1,51 @@ { - "timestamp": "2025-11-18T00:28:14.219Z", + "timestamp": "2025-12-19T00:29:51.614Z", "category": "MatrixTests", "environment": { - "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.7, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", - "sdk": ".NET SDK 10.0.100", - "host": ".NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3" + "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", + "sdk": ".NET SDK 10.0.101", + "host": ".NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3" }, "results": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "561.43 ms", - "Error": "5.322 ms", - "StdDev": "4.718 ms", - "Median": "562.61 ms" + "Version": "1.5.70", + "Mean": "558.07 ms", + "Error": "3.405 ms", + "StdDev": "3.185 ms", + "Median": "558.31 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "1,556.36 ms", - "Error": "8.816 ms", - "StdDev": "7.815 ms", - "Median": "1,557.95 ms" + "Mean": "1,651.26 ms", + "Error": "8.632 ms", + "StdDev": "8.074 ms", + "Median": "1,651.55 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "1,517.35 ms", - "Error": "13.710 ms", - "StdDev": "12.824 ms", - "Median": "1,517.64 ms" + "Mean": "1,516.31 ms", + "Error": "5.461 ms", + "StdDev": "4.841 ms", + "Median": "1,516.34 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "1,602.60 ms", - "Error": "12.743 ms", - "StdDev": "11.920 ms", - "Median": "1,603.72 ms" + "Version": "3.2.1", + "Mean": "1,606.91 ms", + "Error": "8.208 ms", + "StdDev": "7.677 ms", + "Median": "1,604.13 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "79.00 ms", - "Error": "0.238 ms", - "StdDev": "0.223 ms", - "Median": "79.03 ms" + "Version": "1.5.70", + "Mean": "78.72 ms", + "Error": "0.439 ms", + "StdDev": "0.411 ms", + "Median": "78.66 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/ScaleTests.json b/docs/static/benchmarks/ScaleTests.json index a6dfe8b640..c95f53c229 100644 --- a/docs/static/benchmarks/ScaleTests.json +++ b/docs/static/benchmarks/ScaleTests.json @@ -1,51 +1,51 @@ { - "timestamp": "2025-11-18T00:28:14.219Z", + "timestamp": "2025-12-19T00:29:51.615Z", "category": "ScaleTests", "environment": { - "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.7, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", - "sdk": ".NET SDK 10.0.100", - "host": ".NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3" + "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", + "sdk": ".NET SDK 10.0.101", + "host": ".NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3" }, "results": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "541.57 ms", - "Error": "4.341 ms", - "StdDev": "3.848 ms", - "Median": "540.90 ms" + "Version": "1.5.70", + "Mean": "528.92 ms", + "Error": "3.910 ms", + "StdDev": "3.466 ms", + "Median": "529.16 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "591.49 ms", - "Error": "10.469 ms", - "StdDev": "9.280 ms", - "Median": "591.89 ms" + "Mean": "692.00 ms", + "Error": "10.778 ms", + "StdDev": "9.554 ms", + "Median": "692.40 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "512.41 ms", - "Error": "10.194 ms", - "StdDev": "11.331 ms", - "Median": "507.96 ms" + "Mean": "583.69 ms", + "Error": "11.402 ms", + "StdDev": "18.412 ms", + "Median": "584.50 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "596.95 ms", - "Error": "10.457 ms", - "StdDev": "9.270 ms", - "Median": "593.96 ms" + "Version": "3.2.1", + "Mean": "601.26 ms", + "Error": "10.818 ms", + "StdDev": "9.034 ms", + "Median": "602.87 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "43.84 ms", - "Error": "1.157 ms", - "StdDev": "3.376 ms", - "Median": "43.96 ms" + "Version": "1.5.70", + "Mean": "38.89 ms", + "Error": "0.840 ms", + "StdDev": "2.465 ms", + "Median": "38.74 ms" } ] } \ No newline at end of file diff --git a/docs/static/benchmarks/SetupTeardownTests.json b/docs/static/benchmarks/SetupTeardownTests.json index 650a5db7b8..d823c8a26d 100644 --- a/docs/static/benchmarks/SetupTeardownTests.json +++ b/docs/static/benchmarks/SetupTeardownTests.json @@ -1,47 +1,47 @@ { - "timestamp": "2025-11-18T00:28:14.220Z", + "timestamp": "2025-12-19T00:29:51.615Z", "category": "SetupTeardownTests", "environment": { - "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.7, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", - "sdk": ".NET SDK 10.0.100", - "host": ".NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3" + "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", + "sdk": ".NET SDK 10.0.101", + "host": ".NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3" }, "results": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "578.7 ms", - "Error": "6.61 ms", - "StdDev": "6.18 ms", - "Median": "578.1 ms" + "Version": "1.5.70", + "Mean": "602.4 ms", + "Error": "11.93 ms", + "StdDev": "11.72 ms", + "Median": "598.3 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "1,182.7 ms", - "Error": "9.46 ms", - "StdDev": "8.39 ms", - "Median": "1,182.8 ms" + "Mean": "1,338.1 ms", + "Error": "14.29 ms", + "StdDev": "12.67 ms", + "Median": "1,338.7 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "1,152.5 ms", - "Error": "7.45 ms", - "StdDev": "6.97 ms", - "Median": "1,151.9 ms" + "Mean": "1,180.3 ms", + "Error": "10.74 ms", + "StdDev": "10.05 ms", + "Median": "1,176.7 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "1,229.3 ms", - "Error": "9.96 ms", - "StdDev": "9.31 ms", - "Median": "1,225.5 ms" + "Version": "3.2.1", + "Mean": "1,273.1 ms", + "Error": "17.85 ms", + "StdDev": "15.82 ms", + "Median": "1,269.2 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", + "Version": "1.5.70", "Mean": "NA", "Error": "NA", "StdDev": "NA", diff --git a/docs/static/benchmarks/historical.json b/docs/static/benchmarks/historical.json index 26988cc598..38413fcbd6 100644 --- a/docs/static/benchmarks/historical.json +++ b/docs/static/benchmarks/historical.json @@ -54,5 +54,109 @@ { "date": "2025-11-18", "environment": "Ubuntu" + }, + { + "date": "2025-11-19", + "environment": "Ubuntu" + }, + { + "date": "2025-11-20", + "environment": "Ubuntu" + }, + { + "date": "2025-11-23", + "environment": "Ubuntu" + }, + { + "date": "2025-11-24", + "environment": "Ubuntu" + }, + { + "date": "2025-11-25", + "environment": "Ubuntu" + }, + { + "date": "2025-11-26", + "environment": "Ubuntu" + }, + { + "date": "2025-11-27", + "environment": "Ubuntu" + }, + { + "date": "2025-12-01", + "environment": "Ubuntu" + }, + { + "date": "2025-12-02", + "environment": "Ubuntu" + }, + { + "date": "2025-12-03", + "environment": "Ubuntu" + }, + { + "date": "2025-12-04", + "environment": "Ubuntu" + }, + { + "date": "2025-12-05", + "environment": "Ubuntu" + }, + { + "date": "2025-12-06", + "environment": "Ubuntu" + }, + { + "date": "2025-12-07", + "environment": "Ubuntu" + }, + { + "date": "2025-12-08", + "environment": "Ubuntu" + }, + { + "date": "2025-12-09", + "environment": "Ubuntu" + }, + { + "date": "2025-12-10", + "environment": "Ubuntu" + }, + { + "date": "2025-12-11", + "environment": "Ubuntu" + }, + { + "date": "2025-12-12", + "environment": "Ubuntu" + }, + { + "date": "2025-12-13", + "environment": "Ubuntu" + }, + { + "date": "2025-12-14", + "environment": "Ubuntu" + }, + { + "date": "2025-12-15", + "environment": "Ubuntu" + }, + { + "date": "2025-12-16", + "environment": "Ubuntu" + }, + { + "date": "2025-12-17", + "environment": "Ubuntu" + }, + { + "date": "2025-12-18", + "environment": "Ubuntu" + }, + { + "date": "2025-12-19", + "environment": "Ubuntu" } ] \ No newline at end of file diff --git a/docs/static/benchmarks/latest.json b/docs/static/benchmarks/latest.json index 55ec1a5025..75657f0194 100644 --- a/docs/static/benchmarks/latest.json +++ b/docs/static/benchmarks/latest.json @@ -1,257 +1,257 @@ { - "timestamp": "2025-11-18T00:28:14.221Z", + "timestamp": "2025-12-19T00:29:51.616Z", "environment": { - "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.7, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", - "sdk": ".NET SDK 10.0.100", - "host": ".NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3" + "benchmarkDotNetVersion": "BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.3 LTS (Noble Numbat)", + "sdk": ".NET SDK 10.0.101", + "host": ".NET 10.0.1 (10.0.1, 10.0.125.57005), X64 RyuJIT x86-64-v3" }, "categories": { "AsyncTests": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "556.5 ms", - "Error": "2.29 ms", - "StdDev": "2.03 ms", - "Median": "556.3 ms" + "Version": "1.5.70", + "Mean": "584.8 ms", + "Error": "6.94 ms", + "StdDev": "6.49 ms", + "Median": "585.8 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "663.3 ms", - "Error": "10.81 ms", - "StdDev": "9.02 ms", - "Median": "662.2 ms" + "Mean": "771.5 ms", + "Error": "9.31 ms", + "StdDev": "8.25 ms", + "Median": "770.5 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "635.0 ms", - "Error": "12.02 ms", - "StdDev": "11.80 ms", - "Median": "632.0 ms" + "Mean": "647.1 ms", + "Error": "7.82 ms", + "StdDev": "7.31 ms", + "Median": "646.7 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "715.8 ms", - "Error": "9.34 ms", - "StdDev": "8.28 ms", - "Median": "716.7 ms" + "Version": "3.2.1", + "Mean": "725.9 ms", + "Error": "11.34 ms", + "StdDev": "10.05 ms", + "Median": "723.1 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "124.5 ms", - "Error": "0.31 ms", - "StdDev": "0.26 ms", - "Median": "124.5 ms" + "Version": "1.5.70", + "Mean": "124.6 ms", + "Error": "0.50 ms", + "StdDev": "0.47 ms", + "Median": "124.7 ms" } ], "DataDrivenTests": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "493.26 ms", - "Error": "3.582 ms", - "StdDev": "3.175 ms", - "Median": "492.16 ms" + "Version": "1.5.70", + "Mean": "510.15 ms", + "Error": "5.720 ms", + "StdDev": "5.350 ms", + "Median": "510.45 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "601.85 ms", - "Error": "11.895 ms", - "StdDev": "13.698 ms", - "Median": "601.22 ms" + "Mean": "664.02 ms", + "Error": "7.163 ms", + "StdDev": "6.350 ms", + "Median": "664.38 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "617.89 ms", - "Error": "11.698 ms", - "StdDev": "12.013 ms", - "Median": "622.69 ms" + "Mean": "624.22 ms", + "Error": "9.080 ms", + "StdDev": "8.050 ms", + "Median": "621.89 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "610.52 ms", - "Error": "11.777 ms", - "StdDev": "11.016 ms", - "Median": "609.06 ms" + "Version": "3.2.1", + "Mean": "625.91 ms", + "Error": "12.339 ms", + "StdDev": "13.715 ms", + "Median": "622.72 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "24.33 ms", - "Error": "0.263 ms", - "StdDev": "0.233 ms", - "Median": "24.31 ms" + "Version": "1.5.70", + "Mean": "25.72 ms", + "Error": "0.301 ms", + "StdDev": "0.282 ms", + "Median": "25.63 ms" } ], "MassiveParallelTests": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "595.5 ms", - "Error": "2.81 ms", - "StdDev": "2.63 ms", - "Median": "595.7 ms" + "Version": "1.5.70", + "Mean": "599.4 ms", + "Error": "3.41 ms", + "StdDev": "3.19 ms", + "Median": "599.3 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "1,164.1 ms", - "Error": "6.78 ms", - "StdDev": "6.01 ms", - "Median": "1,163.4 ms" + "Mean": "1,287.1 ms", + "Error": "10.49 ms", + "StdDev": "9.81 ms", + "Median": "1,285.7 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "2,947.1 ms", - "Error": "6.79 ms", - "StdDev": "6.02 ms", - "Median": "2,948.2 ms" + "Mean": "2,971.9 ms", + "Error": "7.15 ms", + "StdDev": "6.34 ms", + "Median": "2,972.6 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "3,048.5 ms", - "Error": "15.99 ms", - "StdDev": "14.96 ms", - "Median": "3,046.6 ms" + "Version": "3.2.1", + "Mean": "3,072.8 ms", + "Error": "8.31 ms", + "StdDev": "7.78 ms", + "Median": "3,069.3 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "130.9 ms", - "Error": "0.44 ms", - "StdDev": "0.41 ms", - "Median": "130.9 ms" + "Version": "1.5.70", + "Mean": "131.6 ms", + "Error": "0.62 ms", + "StdDev": "0.58 ms", + "Median": "131.8 ms" } ], "MatrixTests": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "561.43 ms", - "Error": "5.322 ms", - "StdDev": "4.718 ms", - "Median": "562.61 ms" + "Version": "1.5.70", + "Mean": "558.07 ms", + "Error": "3.405 ms", + "StdDev": "3.185 ms", + "Median": "558.31 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "1,556.36 ms", - "Error": "8.816 ms", - "StdDev": "7.815 ms", - "Median": "1,557.95 ms" + "Mean": "1,651.26 ms", + "Error": "8.632 ms", + "StdDev": "8.074 ms", + "Median": "1,651.55 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "1,517.35 ms", - "Error": "13.710 ms", - "StdDev": "12.824 ms", - "Median": "1,517.64 ms" + "Mean": "1,516.31 ms", + "Error": "5.461 ms", + "StdDev": "4.841 ms", + "Median": "1,516.34 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "1,602.60 ms", - "Error": "12.743 ms", - "StdDev": "11.920 ms", - "Median": "1,603.72 ms" + "Version": "3.2.1", + "Mean": "1,606.91 ms", + "Error": "8.208 ms", + "StdDev": "7.677 ms", + "Median": "1,604.13 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "79.00 ms", - "Error": "0.238 ms", - "StdDev": "0.223 ms", - "Median": "79.03 ms" + "Version": "1.5.70", + "Mean": "78.72 ms", + "Error": "0.439 ms", + "StdDev": "0.411 ms", + "Median": "78.66 ms" } ], "ScaleTests": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "541.57 ms", - "Error": "4.341 ms", - "StdDev": "3.848 ms", - "Median": "540.90 ms" + "Version": "1.5.70", + "Mean": "528.92 ms", + "Error": "3.910 ms", + "StdDev": "3.466 ms", + "Median": "529.16 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "591.49 ms", - "Error": "10.469 ms", - "StdDev": "9.280 ms", - "Median": "591.89 ms" + "Mean": "692.00 ms", + "Error": "10.778 ms", + "StdDev": "9.554 ms", + "Median": "692.40 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "512.41 ms", - "Error": "10.194 ms", - "StdDev": "11.331 ms", - "Median": "507.96 ms" + "Mean": "583.69 ms", + "Error": "11.402 ms", + "StdDev": "18.412 ms", + "Median": "584.50 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "596.95 ms", - "Error": "10.457 ms", - "StdDev": "9.270 ms", - "Median": "593.96 ms" + "Version": "3.2.1", + "Mean": "601.26 ms", + "Error": "10.818 ms", + "StdDev": "9.034 ms", + "Median": "602.87 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", - "Mean": "43.84 ms", - "Error": "1.157 ms", - "StdDev": "3.376 ms", - "Median": "43.96 ms" + "Version": "1.5.70", + "Mean": "38.89 ms", + "Error": "0.840 ms", + "StdDev": "2.465 ms", + "Median": "38.74 ms" } ], "SetupTeardownTests": [ { "Method": "TUnit", - "Version": "1.2.3", - "Mean": "578.7 ms", - "Error": "6.61 ms", - "StdDev": "6.18 ms", - "Median": "578.1 ms" + "Version": "1.5.70", + "Mean": "602.4 ms", + "Error": "11.93 ms", + "StdDev": "11.72 ms", + "Median": "598.3 ms" }, { "Method": "NUnit", "Version": "4.4.0", - "Mean": "1,182.7 ms", - "Error": "9.46 ms", - "StdDev": "8.39 ms", - "Median": "1,182.8 ms" + "Mean": "1,338.1 ms", + "Error": "14.29 ms", + "StdDev": "12.67 ms", + "Median": "1,338.7 ms" }, { "Method": "MSTest", "Version": "4.0.2", - "Mean": "1,152.5 ms", - "Error": "7.45 ms", - "StdDev": "6.97 ms", - "Median": "1,151.9 ms" + "Mean": "1,180.3 ms", + "Error": "10.74 ms", + "StdDev": "10.05 ms", + "Median": "1,176.7 ms" }, { "Method": "xUnit3", - "Version": "3.2.0", - "Mean": "1,229.3 ms", - "Error": "9.96 ms", - "StdDev": "9.31 ms", - "Median": "1,225.5 ms" + "Version": "3.2.1", + "Mean": "1,273.1 ms", + "Error": "17.85 ms", + "StdDev": "15.82 ms", + "Median": "1,269.2 ms" }, { "Method": "TUnit_AOT", - "Version": "1.2.3", + "Version": "1.5.70", "Mean": "NA", "Error": "NA", "StdDev": "NA", @@ -263,35 +263,35 @@ "BuildTime": [ { "Method": "Build_TUnit", - "Version": "1.2.3", - "Mean": "1.947 s", - "Error": "0.0371 s", - "StdDev": "0.0381 s", - "Median": "1.953 s" + "Version": "1.5.70", + "Mean": "2.051 s", + "Error": "0.0337 s", + "StdDev": "0.0315 s", + "Median": "2.049 s" }, { "Method": "Build_NUnit", "Version": "4.4.0", - "Mean": "1.556 s", - "Error": "0.0183 s", - "StdDev": "0.0171 s", - "Median": "1.557 s" + "Mean": "1.649 s", + "Error": "0.0255 s", + "StdDev": "0.0239 s", + "Median": "1.645 s" }, { "Method": "Build_MSTest", "Version": "4.0.2", - "Mean": "1.621 s", - "Error": "0.0167 s", - "StdDev": "0.0156 s", - "Median": "1.624 s" + "Mean": "1.745 s", + "Error": "0.0341 s", + "StdDev": "0.0350 s", + "Median": "1.732 s" }, { "Method": "Build_xUnit3", - "Version": "3.2.0", - "Mean": "1.523 s", - "Error": "0.0173 s", - "StdDev": "0.0162 s", - "Median": "1.527 s" + "Version": "3.2.1", + "Mean": "1.623 s", + "Error": "0.0231 s", + "StdDev": "0.0216 s", + "Median": "1.624 s" } ] }, @@ -299,6 +299,6 @@ "runtimeCategories": 6, "buildCategories": 1, "totalBenchmarks": 7, - "lastUpdated": "2025-11-18T00:28:14.217Z" + "lastUpdated": "2025-12-19T00:29:51.613Z" } } \ No newline at end of file diff --git a/docs/static/benchmarks/summary.json b/docs/static/benchmarks/summary.json index efa7dfbc9c..ec4acc6806 100644 --- a/docs/static/benchmarks/summary.json +++ b/docs/static/benchmarks/summary.json @@ -10,6 +10,6 @@ "build": [ "BuildTime" ], - "timestamp": "2025-11-18", - "environment": "Ubuntu Latest • .NET SDK 10.0.100" + "timestamp": "2025-12-19", + "environment": "Ubuntu Latest • .NET SDK 10.0.101" } \ No newline at end of file diff --git a/docs/yarn.lock b/docs/yarn.lock index 884c7aca68..76deb35bd7 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -2,22 +2,23 @@ # yarn lockfile v1 -"@ai-sdk/gateway@1.0.29": - version "1.0.29" - resolved "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.29.tgz" - integrity sha512-o9LtmBiG2WAgs3GAmL79F8idan/UupxHG8Tyr2gP4aUSOzflM0bsvfzozBp8x6WatQnOx+Pio7YNw45Y6I16iw== +"@ai-sdk/gateway@2.0.14": + version "2.0.14" + resolved "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.14.tgz" + integrity sha512-QU+ZVizSXN/V5uWgwapXrCLvkUEmmJeojAbikMH4gLgbeQF3oRugcQm3D8X9B+Rnestbz5cevNap7vKyJT/jfA== dependencies: "@ai-sdk/provider" "2.0.0" - "@ai-sdk/provider-utils" "3.0.9" + "@ai-sdk/provider-utils" "3.0.17" + "@vercel/oidc" "3.0.5" -"@ai-sdk/provider-utils@3.0.9": - version "3.0.9" - resolved "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.9.tgz" - integrity sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ== +"@ai-sdk/provider-utils@3.0.17": + version "3.0.17" + resolved "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.17.tgz" + integrity sha512-TR3Gs4I3Tym4Ll+EPdzRdvo/rc8Js6c4nVhFLuvGLX/Y4V9ZcQMa/HTiYsHEgmYrf1zVi6Q145UEZUfleOwOjw== dependencies: "@ai-sdk/provider" "2.0.0" "@standard-schema/spec" "^1.0.0" - eventsource-parser "^3.0.5" + eventsource-parser "^3.0.6" "@ai-sdk/provider@2.0.0": version "2.0.0" @@ -27,24 +28,24 @@ json-schema "^0.4.0" "@ai-sdk/react@^2.0.30": - version "2.0.52" - resolved "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.52.tgz" - integrity sha512-4/i40pykN4gTGH264+k1g4tMGdw4xN7vZ1qESFCIm/lhS/8YiJPYheBOk9c349hytOT1sGxp3UNPcOWzWS0H2A== + version "2.0.100" + resolved "https://registry.npmjs.org/@ai-sdk/react/-/react-2.0.100.tgz" + integrity sha512-bC4cEoX9xfBKKnVyhyrhdRraji71uxpCKlNAwD6u1xPgcsCnz99X+9ByoBKvOHRzBtSFUq2K6016huip8Cvq6w== dependencies: - "@ai-sdk/provider-utils" "3.0.9" - ai "5.0.52" + "@ai-sdk/provider-utils" "3.0.17" + ai "5.0.100" swr "^2.2.5" throttleit "2.1.0" -"@algolia/abtesting@1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.4.0.tgz" - integrity sha512-N0blWT/C0KOZ/OJ9GXBX66odJZlrYjMj3M+01y8ob1mjBFnBaBo7gOCyHBDQy60+H4pJXp3pSGlJOqJIueBH+A== +"@algolia/abtesting@1.10.0": + version "1.10.0" + resolved "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.10.0.tgz" + integrity sha512-mQT3jwuTgX8QMoqbIR7mPlWkqQqBPQaPabQzm37xg2txMlaMogK/4hCiiESGdg39MlHZOVHeV+0VJuE7f5UK8A== dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" "@algolia/autocomplete-core@1.19.2": version "1.19.2" @@ -66,170 +67,162 @@ resolved "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.19.2.tgz" integrity sha512-jEazxZTVD2nLrC+wYlVHQgpBoBB5KPStrJxLzsIFl6Kqd1AlG9sIAGl39V5tECLpIQzB3Qa2T6ZPJ1ChkwMK/w== -"@algolia/client-abtesting@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.38.0.tgz" - integrity sha512-15d6zv8vtj2l9pnnp/EH7Rhq3/snCCHRz56NnX6xIUPrbJl5gCsIYXAz8C2IEkwOpoDb0r5G6ArY2gKdVMNezw== - dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" - -"@algolia/client-analytics@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.38.0.tgz" - integrity sha512-jJIbYAhYvTG3+gEAP5Q5Dp6PFJfUR+atz5rsqm5KjAKK+faLFdHJbM2IbOo0xdyGd+SH259MzfQKLJ9mZZ27dQ== - dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" - -"@algolia/client-common@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.38.0.tgz" - integrity sha512-aMCXzVPGJTeQnVU3Sdf30TfMN2+QyWcjfPTCCHyqVVgjPipb6RnK40aISGoO+rlYjh9LunDsNVFLwv+JEIF8bQ== - -"@algolia/client-common@5.42.0": - version "5.42.0" - resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.42.0.tgz" - integrity sha512-6iiFbm2tRn6B2OqFv9XDTcw5LdWPudiJWIbRk+fsTX+hkPrPm4e1/SbU+lEYBciPoaTShLkDbRge4UePEyCPMQ== - -"@algolia/client-insights@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.38.0.tgz" - integrity sha512-4c3FbpMiJX+VcaAj0rYaQdTLS/CkrdOn4hW+5y1plPov7KC7iSHai/VBbirmHuAfW1hVPCIh1w/4erKKTKuo+Q== - dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" - -"@algolia/client-personalization@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.38.0.tgz" - integrity sha512-FzLs6c8TBL4FSgNfnH2NL7O33ktecGiaKO4ZFG51QYORUzD5d6YwB9UBteaIYu/sgFoEdY57diYU4vyBH8R6iA== - dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" - -"@algolia/client-query-suggestions@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.38.0.tgz" - integrity sha512-7apiahlgZLvOqrh0+hAYAp/UWjqz6AfSJrCwnsoQNzgIT09dLSPIKREelkuQeUrKy38vHWWpSQE3M0zWSp/YrA== - dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" +"@algolia/client-abtesting@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.44.0.tgz" + integrity sha512-KY5CcrWhRTUo/lV7KcyjrZkPOOF9bjgWpMj9z98VA+sXzVpZtkuskBLCKsWYFp2sbwchZFTd3wJM48H0IGgF7g== + dependencies: + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" + +"@algolia/client-analytics@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.44.0.tgz" + integrity sha512-LKOCE8S4ewI9bN3ot9RZoYASPi8b78E918/DVPW3HHjCMUe6i+NjbNG6KotU4RpP6AhRWZjjswbOkWelUO+OoA== + dependencies: + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" + +"@algolia/client-common@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.44.0.tgz" + integrity sha512-1yyJm4OYC2cztbS28XYVWwLXdwpLsMG4LoZLOltVglQ2+hc/i9q9fUDZyjRa2Bqt4DmkIfezagfMrokhyH4uxQ== + +"@algolia/client-common@5.46.0": + version "5.46.0" + resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.46.0.tgz" + integrity sha512-0emZTaYOeI9WzJi0TcNd2k3SxiN6DZfdWc2x2gHt855Jl9jPUOzfVTL6gTvCCrOlT4McvpDGg5nGO+9doEjjig== + +"@algolia/client-insights@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.44.0.tgz" + integrity sha512-wVQWK6jYYsbEOjIMI+e5voLGPUIbXrvDj392IckXaCPvQ6vCMTXakQqOYCd+znQdL76S+3wHDo77HZWiAYKrtA== + dependencies: + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" + +"@algolia/client-personalization@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.44.0.tgz" + integrity sha512-lkgRjOjOkqmIkebHjHpU9rLJcJNUDMm+eVSW/KJQYLjGqykEZxal+nYJJTBbLceEU2roByP/+27ZmgIwCdf0iA== + dependencies: + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" + +"@algolia/client-query-suggestions@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.44.0.tgz" + integrity sha512-sYfhgwKu6NDVmZHL1WEKVLsOx/jUXCY4BHKLUOcYa8k4COCs6USGgz6IjFkUf+niwq8NCECMmTC4o/fVQOalsA== + dependencies: + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" "@algolia/client-search@>= 4.9.1 < 6": - version "5.42.0" - resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.42.0.tgz" - integrity sha512-NZR7yyHj2WzK6D5X8gn+/KOxPdzYEXOqVdSaK/biU8QfYUpUuEA0sCWg/XlO05tPVEcJelF/oLrrNY3UjRbOww== + version "5.46.0" + resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.46.0.tgz" + integrity sha512-22SHEEVNjZfFWkFks3P6HilkR3rS7a6GjnCIqR22Zz4HNxdfT0FG+RE7efTcFVfLUkTTMQQybvaUcwMrHXYa7Q== dependencies: - "@algolia/client-common" "5.42.0" - "@algolia/requester-browser-xhr" "5.42.0" - "@algolia/requester-fetch" "5.42.0" - "@algolia/requester-node-http" "5.42.0" + "@algolia/client-common" "5.46.0" + "@algolia/requester-browser-xhr" "5.46.0" + "@algolia/requester-fetch" "5.46.0" + "@algolia/requester-node-http" "5.46.0" -"@algolia/client-search@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.38.0.tgz" - integrity sha512-PTAFMJOpVtJweExEYYgdmSCC6n4V/R+ctDL3fRQy77ulZM/p+zMLIQC9c7HCQE1zqpauvVck3f2zYSejaUTtrw== +"@algolia/client-search@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.44.0.tgz" + integrity sha512-/FRKUM1G4xn3vV8+9xH1WJ9XknU8rkBGlefruq9jDhYUAvYozKimhrmC2pRqw/RyHhPivmgZCRuC8jHP8piz4Q== dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" "@algolia/events@^4.0.1": version "4.0.1" resolved "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz" integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== -"@algolia/ingestion@1.38.0": - version "1.38.0" - resolved "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.38.0.tgz" - integrity sha512-qGSUGgceJHGyJLZ06bFLwVe2Tpf9KwabmoBjFvFscVmMmU5scKya6voCYd9bdX7V0Xy1qya9MGbmTm4zlLuveQ== - dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" - -"@algolia/monitoring@1.38.0": - version "1.38.0" - resolved "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.38.0.tgz" - integrity sha512-VnCtAUcHirvv/dDHg9jK1Z5oo4QOC5FKDxe40x8qloru2qDcjueT34jiAsB0gRos3VWf9v4iPSYTqMIFOcADpQ== +"@algolia/ingestion@1.44.0": + version "1.44.0" + resolved "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.44.0.tgz" + integrity sha512-5+S5ynwMmpTpCLXGjTDpeIa81J+R4BLH0lAojOhmeGSeGEHQTqacl/4sbPyDTcidvnWhaqtyf8m42ue6lvISAw== dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" -"@algolia/recommend@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.38.0.tgz" - integrity sha512-fqgeU9GqxQorFUeGP4et1MyY28ccf9PCeciHwDPSbPYYiTqBItHdUIiytsNpjC5Dnc0RWtuXWCltLwSw9wN/bQ== +"@algolia/monitoring@1.44.0": + version "1.44.0" + resolved "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.44.0.tgz" + integrity sha512-xhaTN8pXJjR6zkrecg4Cc9YZaQK2LKm2R+LkbAq+AYGBCWJxtSGlNwftozZzkUyq4AXWoyoc0x2SyBtq5LRtqQ== dependencies: - "@algolia/client-common" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" -"@algolia/requester-browser-xhr@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.38.0.tgz" - integrity sha512-nAUKbv4YQIXbpPi02AQvSPisD5FDDbT8XeYSh9HFoYP0Z3IpBLLDg7R4ahPvzd7gGsVKgEbXzRPWESXSji5yIg== +"@algolia/recommend@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.44.0.tgz" + integrity sha512-GNcite/uOIS7wgRU1MT7SdNIupGSW+vbK9igIzMePvD2Dl8dy0O3urKPKIbTuZQqiVH1Cb84y5cgLvwNrdCj/Q== dependencies: - "@algolia/client-common" "5.38.0" + "@algolia/client-common" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" -"@algolia/requester-browser-xhr@5.42.0": - version "5.42.0" - resolved "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.42.0.tgz" - integrity sha512-EbuxgteaYBlKgc2Fs3JzoPIKAIaevAIwmv1F+fakaEXeibG4pkmVNsyTUjpOZIgJ1kXeqNvDrcjRb6g3vYBJ9A== +"@algolia/requester-browser-xhr@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.44.0.tgz" + integrity sha512-YZHBk72Cd7pcuNHzbhNzF/FbbYszlc7JhZlDyQAchnX5S7tcemSS96F39Sy8t4O4WQLpFvUf1MTNedlitWdOsQ== dependencies: - "@algolia/client-common" "5.42.0" + "@algolia/client-common" "5.44.0" -"@algolia/requester-fetch@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.38.0.tgz" - integrity sha512-bkuAHaadC6OxJd3SVyQQnU1oJ9G/zdCqua7fwr1tJDrA/v7KzeS5np4/m6BuRUpTgVgFZHSewGnMcgj9DLBoaQ== +"@algolia/requester-browser-xhr@5.46.0": + version "5.46.0" + resolved "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.46.0.tgz" + integrity sha512-eW6xyHCyYrJD0Kjk9Mz33gQ40LfWiEA51JJTVfJy3yeoRSw/NXhAL81Pljpa0qslTs6+LO/5DYPZddct6HvISQ== dependencies: - "@algolia/client-common" "5.38.0" + "@algolia/client-common" "5.46.0" -"@algolia/requester-fetch@5.42.0": - version "5.42.0" - resolved "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.42.0.tgz" - integrity sha512-4vnFvY5Q8QZL9eDNkywFLsk/eQCRBXCBpE8HWs8iUsFNHYoamiOxAeYMin0W/nszQj6abc+jNxMChHmejO+ftQ== +"@algolia/requester-fetch@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.44.0.tgz" + integrity sha512-B9WHl+wQ7uf46t9cq+vVM/ypVbOeuldVDq9OtKsX2ApL2g/htx6ImB9ugDOOJmB5+fE31/XPTuCcYz/j03+idA== dependencies: - "@algolia/client-common" "5.42.0" + "@algolia/client-common" "5.44.0" -"@algolia/requester-node-http@5.38.0": - version "5.38.0" - resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.38.0.tgz" - integrity sha512-yHDKZTnMPR3/4bY0CVC1/uRnnbAaJ+pctRuX7G/HflBkKOrnUBDEGtQQHzEfMz2FHZ/tbCL+Q9r6mvwTSGp8nw== +"@algolia/requester-fetch@5.46.0": + version "5.46.0" + resolved "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.46.0.tgz" + integrity sha512-Vn2+TukMGHy4PIxmdvP667tN/MhS7MPT8EEvEhS6JyFLPx3weLcxSa1F9gVvrfHWCUJhLWoMVJVB2PT8YfRGcw== dependencies: - "@algolia/client-common" "5.38.0" + "@algolia/client-common" "5.46.0" -"@algolia/requester-node-http@5.42.0": - version "5.42.0" - resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.42.0.tgz" - integrity sha512-gkLNpU+b1pCIwk1hKTJz2NWQPT8gsfGhQasnZ5QVv4jd79fKRL/1ikd86P0AzuIQs9tbbhlMwxsSTyJmlq502w== +"@algolia/requester-node-http@5.44.0": + version "5.44.0" + resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.44.0.tgz" + integrity sha512-MULm0qeAIk4cdzZ/ehJnl1o7uB5NMokg83/3MKhPq0Pk7+I0uELGNbzIfAkvkKKEYcHALemKdArtySF9eKzh/A== dependencies: - "@algolia/client-common" "5.42.0" + "@algolia/client-common" "5.44.0" -"@ampproject/remapping@^2.2.0": - version "2.2.1" - resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz" - integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== +"@algolia/requester-node-http@5.46.0": + version "5.46.0" + resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.46.0.tgz" + integrity sha512-xaqXyna5yBZ+r1SJ9my/DM6vfTqJg9FJgVydRJ0lnO+D5NhqGW/qaRG/iBGKr/d4fho34el6WakV7BqJvrl/HQ== dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" + "@algolia/client-common" "5.46.0" "@antfu/install-pkg@^1.1.0": version "1.1.0" @@ -244,259 +237,248 @@ resolved "https://registry.npmjs.org/@antfu/utils/-/utils-9.3.0.tgz" integrity sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA== -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": - version "7.26.2" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz" - integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== dependencies: - "@babel/helper-validator-identifier" "^7.25.9" + "@babel/helper-validator-identifier" "^7.27.1" js-tokens "^4.0.0" - picocolors "^1.0.0" + picocolors "^1.1.1" -"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.25.9", "@babel/compat-data@^7.26.0": - version "7.26.2" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz" - integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg== +"@babel/compat-data@^7.27.2", "@babel/compat-data@^7.27.7", "@babel/compat-data@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz" + integrity sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA== "@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.12.0", "@babel/core@^7.13.0", "@babel/core@^7.21.3", "@babel/core@^7.25.9", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz" - integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.26.0" - "@babel/generator" "^7.26.0" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-module-transforms" "^7.26.0" - "@babel/helpers" "^7.26.0" - "@babel/parser" "^7.26.0" - "@babel/template" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.26.0" + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz" + integrity sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helpers" "^7.28.4" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/remapping" "^2.3.5" convert-source-map "^2.0.0" debug "^4.1.0" gensync "^1.0.0-beta.2" json5 "^2.2.3" semver "^6.3.1" -"@babel/generator@^7.25.9", "@babel/generator@^7.26.0": - version "7.26.2" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz" - integrity sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw== +"@babel/generator@^7.25.9", "@babel/generator@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz" + integrity sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ== dependencies: - "@babel/parser" "^7.26.2" - "@babel/types" "^7.26.0" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" + "@babel/parser" "^7.28.5" + "@babel/types" "^7.28.5" + "@jridgewell/gen-mapping" "^0.3.12" + "@jridgewell/trace-mapping" "^0.3.28" jsesc "^3.0.2" -"@babel/helper-annotate-as-pure@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz" - integrity sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g== - dependencies: - "@babel/types" "^7.25.9" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz" - integrity sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g== +"@babel/helper-annotate-as-pure@^7.27.1", "@babel/helper-annotate-as-pure@^7.27.3": + version "7.27.3" + resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz" + integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg== dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/types" "^7.27.3" -"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz" - integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== +"@babel/helper-compilation-targets@^7.27.1", "@babel/helper-compilation-targets@^7.27.2": + version "7.27.2" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz" + integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ== dependencies: - "@babel/compat-data" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" + "@babel/compat-data" "^7.27.2" + "@babel/helper-validator-option" "^7.27.1" browserslist "^4.24.0" lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-create-class-features-plugin@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz" - integrity sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-member-expression-to-functions" "^7.25.9" - "@babel/helper-optimise-call-expression" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/traverse" "^7.25.9" +"@babel/helper-create-class-features-plugin@^7.27.1", "@babel/helper-create-class-features-plugin@^7.28.3", "@babel/helper-create-class-features-plugin@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.5.tgz" + integrity sha512-q3WC4JfdODypvxArsJQROfupPBq9+lMwjKq7C33GhbFYJsufD0yd/ziwD+hJucLeWsnFPWZjsU2DNFqBPE7jwQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-member-expression-to-functions" "^7.28.5" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.28.5" semver "^6.3.1" -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz" - integrity sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw== +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz" + integrity sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - regexpu-core "^6.1.1" + "@babel/helper-annotate-as-pure" "^7.27.3" + regexpu-core "^6.3.1" semver "^6.3.1" -"@babel/helper-define-polyfill-provider@^0.6.2": - version "0.6.2" - resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz" - integrity sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ== +"@babel/helper-define-polyfill-provider@^0.6.5": + version "0.6.5" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz" + integrity sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg== dependencies: - "@babel/helper-compilation-targets" "^7.22.6" - "@babel/helper-plugin-utils" "^7.22.5" - debug "^4.1.1" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + debug "^4.4.1" lodash.debounce "^4.0.8" - resolve "^1.14.2" - -"@babel/helper-member-expression-to-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz" - integrity sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-module-imports@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz" - integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-module-transforms@^7.25.9", "@babel/helper-module-transforms@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz" - integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/helper-optimise-call-expression@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz" - integrity sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ== - dependencies: - "@babel/types" "^7.25.9" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.24.0", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.8.0": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz" - integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== - -"@babel/helper-remap-async-to-generator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz" - integrity sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-wrap-function" "^7.25.9" - "@babel/traverse" "^7.25.9" + resolve "^1.22.10" + +"@babel/helper-globals@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz" + integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw== + +"@babel/helper-member-expression-to-functions@^7.27.1", "@babel/helper-member-expression-to-functions@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz" + integrity sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg== + dependencies: + "@babel/traverse" "^7.28.5" + "@babel/types" "^7.28.5" + +"@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-transforms@^7.27.1", "@babel/helper-module-transforms@^7.28.3": + version "7.28.3" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz" + integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.28.3" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.27.1", "@babel/helper-plugin-utils@^7.8.0": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-replace-supers@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz" + integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz" + integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helper-wrap-function@^7.27.1": + version "7.28.3" + resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz" + integrity sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g== + dependencies: + "@babel/template" "^7.27.2" + "@babel/traverse" "^7.28.3" + "@babel/types" "^7.28.2" + +"@babel/helpers@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz" + integrity sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w== + dependencies: + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.4" -"@babel/helper-replace-supers@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz" - integrity sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ== +"@babel/parser@^7.27.2", "@babel/parser@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz" + integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== dependencies: - "@babel/helper-member-expression-to-functions" "^7.25.9" - "@babel/helper-optimise-call-expression" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/helper-simple-access@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz" - integrity sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-skip-transparent-expression-wrappers@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz" - integrity sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - -"@babel/helper-validator-identifier@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz" - integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== - -"@babel/helper-validator-option@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz" - integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== - -"@babel/helper-wrap-function@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz" - integrity sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g== - dependencies: - "@babel/template" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helpers@^7.26.0": - version "7.26.10" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz" - integrity sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g== - dependencies: - "@babel/template" "^7.26.9" - "@babel/types" "^7.26.10" - -"@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.2", "@babel/parser@^7.26.9": - version "7.26.10" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.26.10.tgz" - integrity sha512-6aQR2zGE/QFi8JpDLjUZEPYOs7+mhKXm86VaKFiLP35JQwQb6bwUE+XbvkH0EptsYhbNBSUGaUBLKqxH1xSgsA== - dependencies: - "@babel/types" "^7.26.10" - -"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz" - integrity sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz" - integrity sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz" - integrity sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug== + "@babel/types" "^7.28.5" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz" + integrity sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz" - integrity sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g== + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/plugin-transform-optional-chaining" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz" - integrity sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg== +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.28.3": + version "7.28.3" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz" + integrity sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.3" "@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": version "7.21.0-placeholder-for-preset-env.2" @@ -510,33 +492,33 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" -"@babel/plugin-syntax-import-assertions@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz" - integrity sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg== +"@babel/plugin-syntax-import-assertions@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz" + integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-import-attributes@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz" - integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== +"@babel/plugin-syntax-import-attributes@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-jsx@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz" - integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== +"@babel/plugin-syntax-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz" + integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-syntax-typescript@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz" - integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== +"@babel/plugin-syntax-typescript@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz" + integrity sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-syntax-unicode-sets-regex@^7.18.6": version "7.18.6" @@ -546,531 +528,540 @@ "@babel/helper-create-regexp-features-plugin" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-arrow-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz" - integrity sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg== +"@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-async-generator-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz" - integrity sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw== +"@babel/plugin-transform-async-generator-functions@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz" + integrity sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-remap-async-to-generator" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.28.0" -"@babel/plugin-transform-async-to-generator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz" - integrity sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ== +"@babel/plugin-transform-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz" + integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-remap-async-to-generator" "^7.25.9" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" -"@babel/plugin-transform-block-scoped-functions@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.9.tgz" - integrity sha512-toHc9fzab0ZfenFpsyYinOX0J/5dgJVA2fm64xPewu7CoYHWEivIWKxkK2rMi4r3yQqLnVmheMXRdG+k239CgA== +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-block-scoping@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz" - integrity sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg== +"@babel/plugin-transform-block-scoping@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.5.tgz" + integrity sha512-45DmULpySVvmq9Pj3X9B+62Xe+DJGov27QravQJU1LLcapR6/10i+gYVAucGGJpHBp5mYxIMK4nDAT/QDLr47g== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-class-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz" - integrity sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q== +"@babel/plugin-transform-class-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz" + integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-class-static-block@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz" - integrity sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ== +"@babel/plugin-transform-class-static-block@^7.28.3": + version "7.28.3" + resolved "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz" + integrity sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.28.3" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-classes@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz" - integrity sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg== +"@babel/plugin-transform-classes@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz" + integrity sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" - "@babel/traverse" "^7.25.9" - globals "^11.1.0" + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-globals" "^7.28.0" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/traverse" "^7.28.4" -"@babel/plugin-transform-computed-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz" - integrity sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA== +"@babel/plugin-transform-computed-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz" + integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/template" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/template" "^7.27.1" -"@babel/plugin-transform-destructuring@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz" - integrity sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ== +"@babel/plugin-transform-destructuring@^7.28.0", "@babel/plugin-transform-destructuring@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz" + integrity sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.28.5" -"@babel/plugin-transform-dotall-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz" - integrity sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA== +"@babel/plugin-transform-dotall-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz" + integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-duplicate-keys@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz" - integrity sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw== +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz" - integrity sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog== +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz" + integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-dynamic-import@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz" - integrity sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg== +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-exponentiation-operator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz" - integrity sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA== +"@babel/plugin-transform-explicit-resource-management@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz" + integrity sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ== dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.0" -"@babel/plugin-transform-export-namespace-from@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz" - integrity sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww== +"@babel/plugin-transform-exponentiation-operator@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.5.tgz" + integrity sha512-D4WIMaFtwa2NizOp+dnoFjRez/ClKiC2BqqImwKd1X28nqBtZEyCYJ2ozQrrzlxAFrcrjxo39S6khe9RNDlGzw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-for-of@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz" - integrity sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A== +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-function-name@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz" - integrity sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA== +"@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== dependencies: - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" -"@babel/plugin-transform-json-strings@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz" - integrity sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw== +"@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" -"@babel/plugin-transform-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz" - integrity sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ== +"@babel/plugin-transform-json-strings@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz" + integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-logical-assignment-operators@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz" - integrity sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q== +"@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-member-expression-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz" - integrity sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA== +"@babel/plugin-transform-logical-assignment-operators@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.5.tgz" + integrity sha512-axUuqnUTBuXyHGcJEVVh9pORaN6wC5bYfE7FGzPiaWa3syib9m7g+/IT/4VgCOe2Upef43PHzeAvcrVek6QuuA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-amd@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz" - integrity sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw== +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-commonjs@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz" - integrity sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg== +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-simple-access" "^7.25.9" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-modules-systemjs@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz" - integrity sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA== +"@babel/plugin-transform-modules-commonjs@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz" + integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-systemjs@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz" + integrity sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew== + dependencies: + "@babel/helper-module-transforms" "^7.28.3" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" + "@babel/traverse" "^7.28.5" -"@babel/plugin-transform-modules-umd@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz" - integrity sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw== +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== dependencies: - "@babel/helper-module-transforms" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-named-capturing-groups-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz" - integrity sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA== +"@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz" + integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-new-target@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz" - integrity sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ== +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-nullish-coalescing-operator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.9.tgz" - integrity sha512-ENfftpLZw5EItALAD4WsY/KUWvhUlZndm5GC7G3evUsVeSJB6p0pBeLQUnRnBCBx7zV0RKQjR9kCuwrsIrjWog== +"@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz" + integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-numeric-separator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz" - integrity sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q== +"@babel/plugin-transform-numeric-separator@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz" + integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-object-rest-spread@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz" - integrity sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg== +"@babel/plugin-transform-object-rest-spread@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz" + integrity sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew== dependencies: - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-transform-parameters" "^7.25.9" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.0" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/traverse" "^7.28.4" -"@babel/plugin-transform-object-super@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz" - integrity sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A== +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-replace-supers" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" -"@babel/plugin-transform-optional-catch-binding@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz" - integrity sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g== +"@babel/plugin-transform-optional-catch-binding@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz" + integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-optional-chaining@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz" - integrity sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A== +"@babel/plugin-transform-optional-chaining@^7.27.1", "@babel/plugin-transform-optional-chaining@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.5.tgz" + integrity sha512-N6fut9IZlPnjPwgiQkXNhb+cT8wQKFlJNqcZkWlcTqkcqx6/kU4ynGmLFoa4LViBSirn05YAwk+sQBbPfxtYzQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" -"@babel/plugin-transform-parameters@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz" - integrity sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g== +"@babel/plugin-transform-parameters@^7.27.7": + version "7.27.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz" + integrity sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-private-methods@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz" - integrity sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw== +"@babel/plugin-transform-private-methods@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz" + integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== dependencies: - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-private-property-in-object@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz" - integrity sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw== +"@babel/plugin-transform-private-property-in-object@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz" + integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-property-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz" - integrity sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA== +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-react-constant-elements@^7.21.3": - version "7.24.1" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.1.tgz" - integrity sha512-QXp1U9x0R7tkiGB0FOk8o74jhnap0FlZ5gNkRIWdG3eP+SvMFg118e1zaWewDzgABb106QSKpVsD3Wgd8t6ifA== + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz" + integrity sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug== dependencies: - "@babel/helper-plugin-utils" "^7.24.0" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-react-display-name@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.25.9.tgz" - integrity sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ== +"@babel/plugin-transform-react-display-name@^7.28.0": + version "7.28.0" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz" + integrity sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-react-jsx-development@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.25.9.tgz" - integrity sha512-9mj6rm7XVYs4mdLIpbZnHOYdpW42uoiBCTVowg7sP1thUOiANgMb4UtpRivR0pp5iL+ocvUv7X4mZgFRpJEzGw== +"@babel/plugin-transform-react-jsx-development@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz" + integrity sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q== dependencies: - "@babel/plugin-transform-react-jsx" "^7.25.9" + "@babel/plugin-transform-react-jsx" "^7.27.1" -"@babel/plugin-transform-react-jsx@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.9.tgz" - integrity sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw== +"@babel/plugin-transform-react-jsx@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz" + integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/plugin-syntax-jsx" "^7.25.9" - "@babel/types" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/types" "^7.27.1" -"@babel/plugin-transform-react-pure-annotations@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.25.9.tgz" - integrity sha512-KQ/Takk3T8Qzj5TppkS1be588lkbTp5uj7w6a0LeQaTMSckU/wK0oJ/pih+T690tkgI5jfmg2TqDJvd41Sj1Cg== +"@babel/plugin-transform-react-pure-annotations@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz" + integrity sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regenerator@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz" - integrity sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg== +"@babel/plugin-transform-regenerator@^7.28.4": + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz" + integrity sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - regenerator-transform "^0.15.2" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-regexp-modifiers@^7.26.0": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz" - integrity sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw== +"@babel/plugin-transform-regexp-modifiers@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz" + integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-reserved-words@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz" - integrity sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg== +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/plugin-transform-runtime@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz" - integrity sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.6" - babel-plugin-polyfill-regenerator "^0.6.1" + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.5.tgz" + integrity sha512-20NUVgOrinudkIBzQ2bNxP08YpKprUkRTiRSd2/Z5GOdPImJGkoN4Z7IQe1T5AdyKI1i5L6RBmluqdSzvaq9/w== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + babel-plugin-polyfill-corejs2 "^0.4.14" + babel-plugin-polyfill-corejs3 "^0.13.0" + babel-plugin-polyfill-regenerator "^0.6.5" semver "^6.3.1" -"@babel/plugin-transform-shorthand-properties@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz" - integrity sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng== +"@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-spread@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz" - integrity sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A== +"@babel/plugin-transform-spread@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz" + integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" -"@babel/plugin-transform-sticky-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz" - integrity sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA== +"@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-template-literals@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz" - integrity sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw== +"@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-typeof-symbol@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz" - integrity sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA== +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-typescript@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz" - integrity sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ== +"@babel/plugin-transform-typescript@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.5.tgz" + integrity sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA== dependencies: - "@babel/helper-annotate-as-pure" "^7.25.9" - "@babel/helper-create-class-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-skip-transparent-expression-wrappers" "^7.25.9" - "@babel/plugin-syntax-typescript" "^7.25.9" + "@babel/helper-annotate-as-pure" "^7.27.3" + "@babel/helper-create-class-features-plugin" "^7.28.5" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-syntax-typescript" "^7.27.1" -"@babel/plugin-transform-unicode-escapes@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz" - integrity sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q== +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-unicode-property-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz" - integrity sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg== +"@babel/plugin-transform-unicode-property-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz" + integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-unicode-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz" - integrity sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA== +"@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" -"@babel/plugin-transform-unicode-sets-regex@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz" - integrity sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ== +"@babel/plugin-transform-unicode-sets-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz" + integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" "@babel/preset-env@^7.20.2", "@babel/preset-env@^7.25.9": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz" - integrity sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw== - dependencies: - "@babel/compat-data" "^7.26.0" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.25.9" - "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.25.9" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.25.9" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.25.9" - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.25.9" + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.5.tgz" + integrity sha512-S36mOoi1Sb6Fz98fBfE+UZSpYw5mJm0NUHtIKrOuNcqeFauy1J6dIvXm2KRVKobOSaGq4t/hBXdN4HGU3wL9Wg== + dependencies: + "@babel/compat-data" "^7.28.5" + "@babel/helper-compilation-targets" "^7.27.2" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.28.5" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.28.3" "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" - "@babel/plugin-syntax-import-assertions" "^7.26.0" - "@babel/plugin-syntax-import-attributes" "^7.26.0" + "@babel/plugin-syntax-import-assertions" "^7.27.1" + "@babel/plugin-syntax-import-attributes" "^7.27.1" "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" - "@babel/plugin-transform-arrow-functions" "^7.25.9" - "@babel/plugin-transform-async-generator-functions" "^7.25.9" - "@babel/plugin-transform-async-to-generator" "^7.25.9" - "@babel/plugin-transform-block-scoped-functions" "^7.25.9" - "@babel/plugin-transform-block-scoping" "^7.25.9" - "@babel/plugin-transform-class-properties" "^7.25.9" - "@babel/plugin-transform-class-static-block" "^7.26.0" - "@babel/plugin-transform-classes" "^7.25.9" - "@babel/plugin-transform-computed-properties" "^7.25.9" - "@babel/plugin-transform-destructuring" "^7.25.9" - "@babel/plugin-transform-dotall-regex" "^7.25.9" - "@babel/plugin-transform-duplicate-keys" "^7.25.9" - "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.25.9" - "@babel/plugin-transform-dynamic-import" "^7.25.9" - "@babel/plugin-transform-exponentiation-operator" "^7.25.9" - "@babel/plugin-transform-export-namespace-from" "^7.25.9" - "@babel/plugin-transform-for-of" "^7.25.9" - "@babel/plugin-transform-function-name" "^7.25.9" - "@babel/plugin-transform-json-strings" "^7.25.9" - "@babel/plugin-transform-literals" "^7.25.9" - "@babel/plugin-transform-logical-assignment-operators" "^7.25.9" - "@babel/plugin-transform-member-expression-literals" "^7.25.9" - "@babel/plugin-transform-modules-amd" "^7.25.9" - "@babel/plugin-transform-modules-commonjs" "^7.25.9" - "@babel/plugin-transform-modules-systemjs" "^7.25.9" - "@babel/plugin-transform-modules-umd" "^7.25.9" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.25.9" - "@babel/plugin-transform-new-target" "^7.25.9" - "@babel/plugin-transform-nullish-coalescing-operator" "^7.25.9" - "@babel/plugin-transform-numeric-separator" "^7.25.9" - "@babel/plugin-transform-object-rest-spread" "^7.25.9" - "@babel/plugin-transform-object-super" "^7.25.9" - "@babel/plugin-transform-optional-catch-binding" "^7.25.9" - "@babel/plugin-transform-optional-chaining" "^7.25.9" - "@babel/plugin-transform-parameters" "^7.25.9" - "@babel/plugin-transform-private-methods" "^7.25.9" - "@babel/plugin-transform-private-property-in-object" "^7.25.9" - "@babel/plugin-transform-property-literals" "^7.25.9" - "@babel/plugin-transform-regenerator" "^7.25.9" - "@babel/plugin-transform-regexp-modifiers" "^7.26.0" - "@babel/plugin-transform-reserved-words" "^7.25.9" - "@babel/plugin-transform-shorthand-properties" "^7.25.9" - "@babel/plugin-transform-spread" "^7.25.9" - "@babel/plugin-transform-sticky-regex" "^7.25.9" - "@babel/plugin-transform-template-literals" "^7.25.9" - "@babel/plugin-transform-typeof-symbol" "^7.25.9" - "@babel/plugin-transform-unicode-escapes" "^7.25.9" - "@babel/plugin-transform-unicode-property-regex" "^7.25.9" - "@babel/plugin-transform-unicode-regex" "^7.25.9" - "@babel/plugin-transform-unicode-sets-regex" "^7.25.9" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.28.0" + "@babel/plugin-transform-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.28.5" + "@babel/plugin-transform-class-properties" "^7.27.1" + "@babel/plugin-transform-class-static-block" "^7.28.3" + "@babel/plugin-transform-classes" "^7.28.4" + "@babel/plugin-transform-computed-properties" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.28.5" + "@babel/plugin-transform-dotall-regex" "^7.27.1" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-explicit-resource-management" "^7.28.0" + "@babel/plugin-transform-exponentiation-operator" "^7.28.5" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.27.1" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.28.5" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-modules-systemjs" "^7.28.5" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" + "@babel/plugin-transform-numeric-separator" "^7.27.1" + "@babel/plugin-transform-object-rest-spread" "^7.28.4" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.28.5" + "@babel/plugin-transform-parameters" "^7.27.7" + "@babel/plugin-transform-private-methods" "^7.27.1" + "@babel/plugin-transform-private-property-in-object" "^7.27.1" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.28.4" + "@babel/plugin-transform-regexp-modifiers" "^7.27.1" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.27.1" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.27.1" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.27.1" "@babel/preset-modules" "0.1.6-no-external-plugins" - babel-plugin-polyfill-corejs2 "^0.4.10" - babel-plugin-polyfill-corejs3 "^0.10.6" - babel-plugin-polyfill-regenerator "^0.6.1" - core-js-compat "^3.38.1" + babel-plugin-polyfill-corejs2 "^0.4.14" + babel-plugin-polyfill-corejs3 "^0.13.0" + babel-plugin-polyfill-regenerator "^0.6.5" + core-js-compat "^3.43.0" semver "^6.3.1" "@babel/preset-modules@0.1.6-no-external-plugins": @@ -1083,72 +1074,71 @@ esutils "^2.0.2" "@babel/preset-react@^7.18.6", "@babel/preset-react@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.25.9.tgz" - integrity sha512-D3to0uSPiWE7rBrdIICCd0tJSIGpLaaGptna2+w7Pft5xMqLpA1sz99DK5TZ1TjGbdQ/VI1eCSZ06dv3lT4JOw== + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz" + integrity sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-transform-react-display-name" "^7.25.9" - "@babel/plugin-transform-react-jsx" "^7.25.9" - "@babel/plugin-transform-react-jsx-development" "^7.25.9" - "@babel/plugin-transform-react-pure-annotations" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-transform-react-display-name" "^7.28.0" + "@babel/plugin-transform-react-jsx" "^7.27.1" + "@babel/plugin-transform-react-jsx-development" "^7.27.1" + "@babel/plugin-transform-react-pure-annotations" "^7.27.1" "@babel/preset-typescript@^7.21.0", "@babel/preset-typescript@^7.25.9": - version "7.26.0" - resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.26.0.tgz" - integrity sha512-NMk1IGZ5I/oHhoXEElcm+xUnL/szL6xflkFZmoEU9xj1qSJXpiS7rsspYo92B4DRCDvZn2erT5LdsCeXAKNCkg== + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz" + integrity sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g== dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - "@babel/plugin-syntax-jsx" "^7.25.9" - "@babel/plugin-transform-modules-commonjs" "^7.25.9" - "@babel/plugin-transform-typescript" "^7.25.9" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-syntax-jsx" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-typescript" "^7.28.5" "@babel/runtime-corejs3@^7.25.9": - version "7.26.10" - resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.26.10.tgz" - integrity sha512-uITFQYO68pMEYR46AHgQoyBg7KPPJDAbGn4jUTIRgCFJIp88MIBUianVOplhZDEec07bp9zIyr4Kp0FCyQzmWg== + version "7.28.4" + resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz" + integrity sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ== dependencies: - core-js-pure "^3.30.2" - regenerator-runtime "^0.14.0" + core-js-pure "^3.43.0" -"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.25.9", "@babel/runtime@^7.8.4": +"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.25.9": version "7.26.10" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz" integrity sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw== dependencies: regenerator-runtime "^0.14.0" -"@babel/template@^7.25.9", "@babel/template@^7.26.9": - version "7.26.9" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz" - integrity sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA== - dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/parser" "^7.26.9" - "@babel/types" "^7.26.9" - -"@babel/traverse@^7.25.9": - version "7.25.9" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz" - integrity sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw== - dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/generator" "^7.25.9" - "@babel/parser" "^7.25.9" - "@babel/template" "^7.25.9" - "@babel/types" "^7.25.9" +"@babel/template@^7.27.1", "@babel/template@^7.27.2": + version "7.27.2" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz" + integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.2" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.25.9", "@babel/traverse@^7.27.1", "@babel/traverse@^7.28.0", "@babel/traverse@^7.28.3", "@babel/traverse@^7.28.4", "@babel/traverse@^7.28.5": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz" + integrity sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.28.5" + "@babel/helper-globals" "^7.28.0" + "@babel/parser" "^7.28.5" + "@babel/template" "^7.27.2" + "@babel/types" "^7.28.5" debug "^4.3.1" - globals "^11.1.0" -"@babel/types@^7.21.3", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.10", "@babel/types@^7.26.9", "@babel/types@^7.4.4": - version "7.26.10" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.26.10.tgz" - integrity sha512-emqcG3vHrpxUKTrxcblR36dcrcoRDvKmnL/dCL6ZsHaShW80qxCAcNhzQZrpeM765VzEos+xOi4s+r4IXzTwdQ== +"@babel/types@^7.21.3", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.4", "@babel/types@^7.28.5", "@babel/types@^7.4.4": + version "7.28.5" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz" + integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.28.5" "@braintree/sanitize-url@^7.1.1": version "7.1.1" @@ -1197,22 +1187,22 @@ resolved "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz" integrity sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A== -"@csstools/color-helpers@^5.0.2": - version "5.0.2" - resolved "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz" - integrity sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA== +"@csstools/color-helpers@^5.1.0": + version "5.1.0" + resolved "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz" + integrity sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA== "@csstools/css-calc@^2.1.4": version "2.1.4" resolved "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz" integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== -"@csstools/css-color-parser@^3.0.10": - version "3.0.10" - resolved "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz" - integrity sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg== +"@csstools/css-color-parser@^3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz" + integrity sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA== dependencies: - "@csstools/color-helpers" "^5.0.2" + "@csstools/color-helpers" "^5.1.0" "@csstools/css-calc" "^2.1.4" "@csstools/css-parser-algorithms@^3.0.5": @@ -1230,55 +1220,88 @@ resolved "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz" integrity sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ== -"@csstools/postcss-cascade-layers@^5.0.1": - version "5.0.1" - resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz" - integrity sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ== +"@csstools/postcss-alpha-function@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz" + integrity sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-cascade-layers@^5.0.2": + version "5.0.2" + resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz" + integrity sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg== dependencies: "@csstools/selector-specificity" "^5.0.0" postcss-selector-parser "^7.0.0" -"@csstools/postcss-color-function@^4.0.10": - version "4.0.10" - resolved "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.10.tgz" - integrity sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ== +"@csstools/postcss-color-function-display-p3-linear@^1.0.1": + version "1.0.1" + resolved "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz" + integrity sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" -"@csstools/postcss-color-mix-function@^3.0.10": - version "3.0.10" - resolved "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.10.tgz" - integrity sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q== +"@csstools/postcss-color-function@^4.0.12": + version "4.0.12" + resolved "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz" + integrity sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" -"@csstools/postcss-color-mix-variadic-function-arguments@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.0.tgz" - integrity sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ== +"@csstools/postcss-color-mix-function@^3.0.12": + version "3.0.12" + resolved "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz" + integrity sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" -"@csstools/postcss-content-alt-text@^2.0.6": - version "2.0.6" - resolved "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.6.tgz" - integrity sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ== +"@csstools/postcss-color-mix-variadic-function-arguments@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz" + integrity sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ== + dependencies: + "@csstools/css-color-parser" "^3.1.0" + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-content-alt-text@^2.0.8": + version "2.0.8" + resolved "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz" + integrity sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.5" + "@csstools/css-tokenizer" "^3.0.4" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-contrast-color-function@^2.0.12": + version "2.0.12" + resolved "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz" + integrity sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA== dependencies: + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" "@csstools/postcss-exponential-functions@^2.0.9": @@ -1298,43 +1321,43 @@ "@csstools/utilities" "^2.0.0" postcss-value-parser "^4.2.0" -"@csstools/postcss-gamut-mapping@^2.0.10": - version "2.0.10" - resolved "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.10.tgz" - integrity sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg== +"@csstools/postcss-gamut-mapping@^2.0.11": + version "2.0.11" + resolved "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz" + integrity sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" -"@csstools/postcss-gradients-interpolation-method@^5.0.10": - version "5.0.10" - resolved "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.10.tgz" - integrity sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw== +"@csstools/postcss-gradients-interpolation-method@^5.0.12": + version "5.0.12" + resolved "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz" + integrity sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" -"@csstools/postcss-hwb-function@^4.0.10": - version "4.0.10" - resolved "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.10.tgz" - integrity sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ== +"@csstools/postcss-hwb-function@^4.0.12": + version "4.0.12" + resolved "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz" + integrity sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" -"@csstools/postcss-ic-unit@^4.0.2": - version "4.0.2" - resolved "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.2.tgz" - integrity sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA== +"@csstools/postcss-ic-unit@^4.0.4": + version "4.0.4" + resolved "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz" + integrity sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg== dependencies: - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" postcss-value-parser "^4.2.0" @@ -1343,22 +1366,22 @@ resolved "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz" integrity sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg== -"@csstools/postcss-is-pseudo-class@^5.0.1": - version "5.0.1" - resolved "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz" - integrity sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ== +"@csstools/postcss-is-pseudo-class@^5.0.3": + version "5.0.3" + resolved "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz" + integrity sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ== dependencies: "@csstools/selector-specificity" "^5.0.0" postcss-selector-parser "^7.0.0" -"@csstools/postcss-light-dark-function@^2.0.9": - version "2.0.9" - resolved "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.9.tgz" - integrity sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ== +"@csstools/postcss-light-dark-function@^2.0.11": + version "2.0.11" + resolved "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz" + integrity sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA== dependencies: "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" "@csstools/postcss-logical-float-and-clear@^3.0.0": @@ -1425,21 +1448,21 @@ dependencies: postcss-value-parser "^4.2.0" -"@csstools/postcss-oklab-function@^4.0.10": - version "4.0.10" - resolved "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.10.tgz" - integrity sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg== +"@csstools/postcss-oklab-function@^4.0.12": + version "4.0.12" + resolved "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz" + integrity sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" -"@csstools/postcss-progressive-custom-properties@^4.1.0": - version "4.1.0" - resolved "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.1.0.tgz" - integrity sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA== +"@csstools/postcss-progressive-custom-properties@^4.2.1": + version "4.2.1" + resolved "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz" + integrity sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw== dependencies: postcss-value-parser "^4.2.0" @@ -1452,15 +1475,15 @@ "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" -"@csstools/postcss-relative-color-syntax@^3.0.10": - version "3.0.10" - resolved "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.10.tgz" - integrity sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g== +"@csstools/postcss-relative-color-syntax@^3.0.12": + version "3.0.12" + resolved "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz" + integrity sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" "@csstools/postcss-scope-pseudo-class@^4.0.1": @@ -1488,12 +1511,12 @@ "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" -"@csstools/postcss-text-decoration-shorthand@^4.0.2": - version "4.0.2" - resolved "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.2.tgz" - integrity sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA== +"@csstools/postcss-text-decoration-shorthand@^4.0.3": + version "4.0.3" + resolved "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz" + integrity sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA== dependencies: - "@csstools/color-helpers" "^5.0.2" + "@csstools/color-helpers" "^5.1.0" postcss-value-parser "^4.2.0" "@csstools/postcss-trigonometric-functions@^4.0.9": @@ -1510,10 +1533,10 @@ resolved "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz" integrity sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA== -"@csstools/selector-resolve-nested@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz" - integrity sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ== +"@csstools/selector-resolve-nested@^3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz" + integrity sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g== "@csstools/selector-specificity@^5.0.0": version "5.0.0" @@ -1530,19 +1553,25 @@ resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@docsearch/css@4.1.0": - version "4.1.0" - resolved "https://registry.npmjs.org/@docsearch/css/-/css-4.1.0.tgz" - integrity sha512-nuNKGjHj/FQeWgE9t+i83QD/V67QiaAmGY7xS9TVCRUiCqSljOgIKlsLoQZKKVwEG8f+OWKdznzZkJxGZ7d06A== +"@docsearch/core@4.3.1": + version "4.3.1" + resolved "https://registry.npmjs.org/@docsearch/core/-/core-4.3.1.tgz" + integrity sha512-ktVbkePE+2h9RwqCUMbWXOoebFyDOxHqImAqfs+lC8yOU+XwEW4jgvHGJK079deTeHtdhUNj0PXHSnhJINvHzQ== + +"@docsearch/css@4.3.2": + version "4.3.2" + resolved "https://registry.npmjs.org/@docsearch/css/-/css-4.3.2.tgz" + integrity sha512-K3Yhay9MgkBjJJ0WEL5MxnACModX9xuNt3UlQQkDEDZJZ0+aeWKtOkxHNndMRkMBnHdYvQjxkm6mdlneOtU1IQ== "@docsearch/react@^3.9.0 || ^4.1.0": - version "4.1.0" - resolved "https://registry.npmjs.org/@docsearch/react/-/react-4.1.0.tgz" - integrity sha512-4GHI7TT3sJZ2Vs4Kjadv7vAkMrTsJqHvzvxO3JA7UT8iPRKaDottG5o5uNshPWhVVaBYPC35Ukf8bfCotGpjSg== + version "4.3.2" + resolved "https://registry.npmjs.org/@docsearch/react/-/react-4.3.2.tgz" + integrity sha512-74SFD6WluwvgsOPqifYOviEEVwDxslxfhakTlra+JviaNcs7KK/rjsPj89kVEoQc9FUxRkAofaJnHIR7pb4TSQ== dependencies: "@ai-sdk/react" "^2.0.30" "@algolia/autocomplete-core" "1.19.2" - "@docsearch/css" "4.1.0" + "@docsearch/core" "4.3.1" + "@docsearch/css" "4.3.2" ai "^5.0.30" algoliasearch "^5.28.0" marked "^16.3.0" @@ -1599,7 +1628,7 @@ webpack "^5.95.0" webpackbar "^6.0.1" -"@docusaurus/core@3.9.2": +"@docusaurus/core@^3.0.0", "@docusaurus/core@3.9.2": version "3.9.2" resolved "https://registry.npmjs.org/@docusaurus/core/-/core-3.9.2.tgz" integrity sha512-HbjwKeC+pHUFBfLMNzuSjqFE/58+rLVKmOU3lxQrpsxLBOGosYco/Q0GduBb0/jEMRiyEqjNT/01rRdOMWq5pw== @@ -2086,13 +2115,20 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": + version "0.3.13" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz" + integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/remapping@^2.3.5": + version "2.3.5" + resolved "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz" + integrity sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/resolve-uri@^3.1.0": @@ -2100,11 +2136,6 @@ resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - "@jridgewell/source-map@^0.3.3": version "0.3.5" resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz" @@ -2113,15 +2144,15 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.5" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz" + integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.25" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== +"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.31" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz" + integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== dependencies: "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" @@ -2131,10 +2162,10 @@ resolved "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz" integrity sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA== -"@jsonjoy.com/buffers@^1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.0.0.tgz" - integrity sha512-NDigYR3PHqCnQLXYyoLbnEdzMMvzeiCWo1KOut7Q0CoIqg9tUAPKJ1iq/2nFhc5kZtexzutNY0LFjdwWL3Dw3Q== +"@jsonjoy.com/buffers@^1.0.0", "@jsonjoy.com/buffers@^1.2.0": + version "1.2.1" + resolved "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz" + integrity sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA== "@jsonjoy.com/codegen@^1.0.0": version "1.0.0" @@ -2142,19 +2173,20 @@ integrity sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g== "@jsonjoy.com/json-pack@^1.11.0": - version "1.14.0" - resolved "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.14.0.tgz" - integrity sha512-LpWbYgVnKzphN5S6uss4M25jJ/9+m6q6UJoeN6zTkK4xAGhKsiBRPVeF7OYMWonn5repMQbE5vieRXcMUrKDKw== + version "1.21.0" + resolved "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz" + integrity sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg== dependencies: "@jsonjoy.com/base64" "^1.1.2" - "@jsonjoy.com/buffers" "^1.0.0" + "@jsonjoy.com/buffers" "^1.2.0" "@jsonjoy.com/codegen" "^1.0.0" - "@jsonjoy.com/json-pointer" "^1.0.1" + "@jsonjoy.com/json-pointer" "^1.0.2" "@jsonjoy.com/util" "^1.9.0" hyperdyperid "^1.2.0" thingies "^2.5.0" + tree-dump "^1.1.0" -"@jsonjoy.com/json-pointer@^1.0.1": +"@jsonjoy.com/json-pointer@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz" integrity sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg== @@ -2171,29 +2203,31 @@ "@jsonjoy.com/codegen" "^1.0.0" "@leichtgewicht/ip-codec@^2.0.1": - version "2.0.4" - resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" - integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + version "2.0.5" + resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== "@mdx-js/mdx@^3.0.0": - version "3.0.1" - resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.0.1.tgz" - integrity sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA== + version "3.1.1" + resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz" + integrity sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ== dependencies: "@types/estree" "^1.0.0" "@types/estree-jsx" "^1.0.0" "@types/hast" "^3.0.0" "@types/mdx" "^2.0.0" + acorn "^8.0.0" collapse-white-space "^2.0.0" devlop "^1.0.0" - estree-util-build-jsx "^3.0.0" estree-util-is-identifier-name "^3.0.0" - estree-util-to-js "^2.0.0" + estree-util-scope "^1.0.0" estree-walker "^3.0.0" - hast-util-to-estree "^3.0.0" hast-util-to-jsx-runtime "^2.0.0" markdown-extensions "^2.0.0" - periscopic "^3.0.0" + recma-build-jsx "^1.0.0" + recma-jsx "^1.0.0" + recma-stringify "^1.0.0" + rehype-recma "^1.0.0" remark-mdx "^3.0.0" remark-parse "^11.0.0" remark-rehype "^11.0.0" @@ -2204,7 +2238,7 @@ unist-util-visit "^5.0.0" vfile "^6.0.0" -"@mdx-js/react@^3.0.0": +"@mdx-js/react@^3.0.0", "@mdx-js/react@^3.1.1": version "3.1.1" resolved "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz" integrity sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw== @@ -2434,17 +2468,10 @@ resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@types/acorn@^4.0.0": - version "4.0.6" - resolved "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz" - integrity sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ== - dependencies: - "@types/estree" "*" - "@types/body-parser@*": - version "1.19.5" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + version "1.19.6" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz" + integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g== dependencies: "@types/connect" "*" "@types/node" "*" @@ -2705,9 +2732,9 @@ "@types/json-schema" "*" "@types/estree-jsx@^1.0.0": - version "1.0.4" - resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.4.tgz" - integrity sha512-5idy3hvI9lAMqsyilBM+N+boaCf1MgoefbDxN6KEO5aK17TOHwFAYT9sjxzeKAiIWRUBgLxmZ9mPcnzZXtTcRQ== + version "1.0.5" + resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== dependencies: "@types/estree" "*" @@ -2717,9 +2744,9 @@ integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== "@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.21", "@types/express-serve-static-core@^4.17.33": - version "4.19.6" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz" - integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A== + version "4.19.7" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz" + integrity sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg== dependencies: "@types/node" "*" "@types/qs" "*" @@ -2727,14 +2754,14 @@ "@types/send" "*" "@types/express@*", "@types/express@^4.17.13", "@types/express@^4.17.21": - version "4.17.23" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz" - integrity sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ== + version "4.17.25" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz" + integrity sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "^4.17.33" "@types/qs" "*" - "@types/serve-static" "*" + "@types/serve-static" "^1" "@types/geojson@*": version "7946.0.16" @@ -2769,14 +2796,14 @@ integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== "@types/http-errors@*": - version "2.0.4" - resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz" - integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + version "2.0.5" + resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz" + integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg== "@types/http-proxy@^1.17.8": - version "1.17.14" - resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz" - integrity sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w== + version "1.17.17" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz" + integrity sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw== dependencies: "@types/node" "*" @@ -2805,9 +2832,9 @@ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== "@types/mdast@^4.0.0", "@types/mdast@^4.0.2": - version "4.0.3" - resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.3.tgz" - integrity sha512-LsjtqsyF+d2/yFOYaN22dHZI1Cpwkrj+g06G8+qtUKlhovPW89YhqSnfKtMbkgmEtYpH2gydRNULd6y8mciAFg== + version "4.0.4" + resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz" + integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA== dependencies: "@types/unist" "*" @@ -2822,14 +2849,14 @@ integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== "@types/ms@*": - version "0.7.34" - resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz" - integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + version "2.1.0" + resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz" + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== "@types/node-forge@^1.3.0": - version "1.3.11" - resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz" - integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + version "1.3.14" + resolved "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz" + integrity sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw== dependencies: "@types/node" "*" @@ -2851,9 +2878,9 @@ integrity sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ== "@types/qs@*": - version "6.9.11" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz" - integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ== + version "6.14.0" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz" + integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ== "@types/range-parser@*": version "1.2.7" @@ -2887,11 +2914,11 @@ "@types/react" "*" "@types/react@*", "@types/react@>= 16.8.0 < 20.0.0", "@types/react@>=16": - version "19.2.2" - resolved "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz" - integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA== + version "19.2.7" + resolved "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz" + integrity sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg== dependencies: - csstype "^3.0.2" + csstype "^3.2.2" "@types/retry@0.12.2": version "0.12.2" @@ -2906,9 +2933,16 @@ "@types/node" "*" "@types/send@*": - version "0.17.4" - resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + version "1.2.1" + resolved "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz" + integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ== + dependencies: + "@types/node" "*" + +"@types/send@<1": + version "0.17.6" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz" + integrity sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og== dependencies: "@types/mime" "^1" "@types/node" "*" @@ -2920,14 +2954,14 @@ dependencies: "@types/express" "*" -"@types/serve-static@*", "@types/serve-static@^1.15.5": - version "1.15.8" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz" - integrity sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg== +"@types/serve-static@^1", "@types/serve-static@^1.15.5": + version "1.15.10" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz" + integrity sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw== dependencies: "@types/http-errors" "*" "@types/node" "*" - "@types/send" "*" + "@types/send" "<1" "@types/sockjs@^0.3.36": version "0.3.36" @@ -2942,14 +2976,14 @@ integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== "@types/unist@*", "@types/unist@^3.0.0": - version "3.0.2" - resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz" - integrity sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ== + version "3.0.3" + resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== "@types/unist@^2.0.0": - version "2.0.10" - resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz" - integrity sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA== + version "2.0.11" + resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== "@types/ws@^8.5.10": version "8.18.1" @@ -2964,16 +2998,21 @@ integrity sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ== "@types/yargs@^17.0.8": - version "17.0.32" - resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz" - integrity sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog== + version "17.0.35" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz" + integrity sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg== dependencies: "@types/yargs-parser" "*" "@ungap/structured-clone@^1.0.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + version "1.3.0" + resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz" + integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== + +"@vercel/oidc@3.0.5": + version "3.0.5" + resolved "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.0.5.tgz" + integrity sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw== "@webassemblyjs/ast@^1.12.1", "@webassemblyjs/ast@1.12.1": version "1.12.1" @@ -3106,7 +3145,7 @@ resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: +accepts@~1.3.4, accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== @@ -3142,14 +3181,14 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" -ai@^5.0.30, ai@5.0.52: - version "5.0.52" - resolved "https://registry.npmjs.org/ai/-/ai-5.0.52.tgz" - integrity sha512-GLlRHjMlvN9+w7UYGxCpUQ8GgCRv5Z+JCprRH3Q8YbXJ/JyIc6EP9+YRUmQsyExX/qQsuehe7y/LLygarbSTOw== +ai@^5.0.30, ai@5.0.100: + version "5.0.100" + resolved "https://registry.npmjs.org/ai/-/ai-5.0.100.tgz" + integrity sha512-+ANP4EJomTcUKdEF3UpVAWEl6DGn+ozDLxVZKXmTV7NRfyEC2cLYcKwoU4o3sKJpqXMUKNzpFlJFBKOcsKdMyg== dependencies: - "@ai-sdk/gateway" "1.0.29" + "@ai-sdk/gateway" "2.0.14" "@ai-sdk/provider" "2.0.0" - "@ai-sdk/provider-utils" "3.0.9" + "@ai-sdk/provider-utils" "3.0.17" "@opentelemetry/api" "1.9.0" ajv-formats@^2.1.1: @@ -3182,41 +3221,41 @@ ajv@^6.12.5, ajv@^6.9.1: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.8.2, ajv@^8.9.0: - version "8.12.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + version "8.17.1" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" algoliasearch-helper@^3.26.0: - version "3.26.0" - resolved "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.0.tgz" - integrity sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw== + version "3.26.1" + resolved "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.1.tgz" + integrity sha512-CAlCxm4fYBXtvc5MamDzP6Svu8rW4z9me4DCBY1rQ2UDJ0u0flWmusQ8M3nOExZsLLRcUwUPoRAPMrhzOG3erw== dependencies: "@algolia/events" "^4.0.1" algoliasearch@^5.28.0, algoliasearch@^5.37.0, "algoliasearch@>= 3.1 < 6", "algoliasearch@>= 4.9.1 < 6": - version "5.38.0" - resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.38.0.tgz" - integrity sha512-8VJKIzheeI9cjuVJhU1hYEVetOTe7LvA+CujAI7yqvYsPtZfVEvv1pg9AeFNtHBg/ZoSLGU5LPijhcY5l3Ea9g== - dependencies: - "@algolia/abtesting" "1.4.0" - "@algolia/client-abtesting" "5.38.0" - "@algolia/client-analytics" "5.38.0" - "@algolia/client-common" "5.38.0" - "@algolia/client-insights" "5.38.0" - "@algolia/client-personalization" "5.38.0" - "@algolia/client-query-suggestions" "5.38.0" - "@algolia/client-search" "5.38.0" - "@algolia/ingestion" "1.38.0" - "@algolia/monitoring" "1.38.0" - "@algolia/recommend" "5.38.0" - "@algolia/requester-browser-xhr" "5.38.0" - "@algolia/requester-fetch" "5.38.0" - "@algolia/requester-node-http" "5.38.0" + version "5.44.0" + resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.44.0.tgz" + integrity sha512-f8IpsbdQjzTjr/4mJ/jv5UplrtyMnnciGax6/B0OnLCs2/GJTK13O4Y7Ff1AvJVAaztanH+m5nzPoUq6EAy+aA== + dependencies: + "@algolia/abtesting" "1.10.0" + "@algolia/client-abtesting" "5.44.0" + "@algolia/client-analytics" "5.44.0" + "@algolia/client-common" "5.44.0" + "@algolia/client-insights" "5.44.0" + "@algolia/client-personalization" "5.44.0" + "@algolia/client-query-suggestions" "5.44.0" + "@algolia/client-search" "5.44.0" + "@algolia/ingestion" "1.44.0" + "@algolia/monitoring" "1.44.0" + "@algolia/recommend" "5.44.0" + "@algolia/requester-browser-xhr" "5.44.0" + "@algolia/requester-fetch" "5.44.0" + "@algolia/requester-node-http" "5.44.0" ansi-align@^3.0.1: version "3.0.1" @@ -3295,18 +3334,18 @@ array-union@^2.1.0: integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== astring@^1.8.0: - version "1.8.6" - resolved "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz" - integrity sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg== + version "1.9.0" + resolved "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz" + integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg== autoprefixer@^10.4.19, autoprefixer@^10.4.21: - version "10.4.21" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz" - integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ== + version "10.4.22" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz" + integrity sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg== dependencies: - browserslist "^4.24.4" - caniuse-lite "^1.0.30001702" - fraction.js "^4.3.7" + browserslist "^4.27.0" + caniuse-lite "^1.0.30001754" + fraction.js "^5.3.4" normalize-range "^0.1.2" picocolors "^1.1.1" postcss-value-parser "^4.2.0" @@ -3326,29 +3365,29 @@ babel-plugin-dynamic-import-node@^2.3.3: dependencies: object.assign "^4.1.0" -babel-plugin-polyfill-corejs2@^0.4.10: - version "0.4.11" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz" - integrity sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q== +babel-plugin-polyfill-corejs2@^0.4.14: + version "0.4.14" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz" + integrity sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg== dependencies: - "@babel/compat-data" "^7.22.6" - "@babel/helper-define-polyfill-provider" "^0.6.2" + "@babel/compat-data" "^7.27.7" + "@babel/helper-define-polyfill-provider" "^0.6.5" semver "^6.3.1" -babel-plugin-polyfill-corejs3@^0.10.6: - version "0.10.6" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz" - integrity sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA== +babel-plugin-polyfill-corejs3@^0.13.0: + version "0.13.0" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz" + integrity sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A== dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.2" - core-js-compat "^3.38.0" + "@babel/helper-define-polyfill-provider" "^0.6.5" + core-js-compat "^3.43.0" -babel-plugin-polyfill-regenerator@^0.6.1: - version "0.6.2" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz" - integrity sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg== +babel-plugin-polyfill-regenerator@^0.6.5: + version "0.6.5" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz" + integrity sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg== dependencies: - "@babel/helper-define-polyfill-provider" "^0.6.2" + "@babel/helper-define-polyfill-provider" "^0.6.5" bail@^2.0.0: version "2.0.2" @@ -3360,6 +3399,11 @@ balanced-match@^1.0.0: resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +baseline-browser-mapping@^2.8.25: + version "2.8.30" + resolved "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.30.tgz" + integrity sha512-aTUKW4ptQhS64+v2d6IkPzymEzzhw+G0bA1g3uBRV3+ntkH+svttKseW5IOR4Ed6NUVKqnY7qT3dKvzQ7io4AA== + batch@0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" @@ -3371,9 +3415,9 @@ big.js@^5.2.2: integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + version "2.3.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== body-parser@1.20.3: version "1.20.3" @@ -3435,13 +3479,20 @@ boxen@^7.0.0: wrap-ansi "^8.1.0" brace-expansion@^1.1.7: - version "1.1.12" - resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz" - integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg== + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz" @@ -3449,15 +3500,16 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.24.2, browserslist@^4.24.4, browserslist@^4.25.0, "browserslist@>= 4.21.0": - version "4.25.0" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz" - integrity sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA== +browserslist@^4.0.0, browserslist@^4.23.0, browserslist@^4.24.0, browserslist@^4.26.0, browserslist@^4.27.0, browserslist@^4.28.0, "browserslist@>= 4.21.0": + version "4.28.0" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz" + integrity sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ== dependencies: - caniuse-lite "^1.0.30001718" - electron-to-chromium "^1.5.160" - node-releases "^2.0.19" - update-browserslist-db "^1.1.3" + baseline-browser-mapping "^2.8.25" + caniuse-lite "^1.0.30001754" + electron-to-chromium "^1.5.249" + node-releases "^2.0.27" + update-browserslist-db "^1.1.4" buffer-from@^1.0.0: version "1.1.2" @@ -3499,16 +3551,31 @@ cacheable-request@^10.2.8: normalize-url "^8.0.0" responselike "^3.0.0" -call-bind@^1.0.5, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== dependencies: - es-define-property "^1.0.0" es-errors "^1.3.0" function-bind "^1.1.2" + +call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" get-intrinsic "^1.2.4" - set-function-length "^1.2.1" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" callsites@^3.0.0: version "3.1.0" @@ -3543,10 +3610,10 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001718: - version "1.0.30001721" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz" - integrity sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001754: + version "1.0.30001756" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001756.tgz" + integrity sha512-4HnCNKbMLkLdhJz3TToeVWHSnfJvPaq6vu/eRP0Ahub/07n484XHhBF5AJoSGHdVrS8tKFauUQz8Bp9P7LVx7A== ccount@^2.0.0: version "2.0.1" @@ -3772,7 +3839,7 @@ common-path-prefix@^3.0.0: resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz" integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== -compressible@~2.0.16: +compressible@~2.0.18: version "2.0.18" resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== @@ -3780,16 +3847,16 @@ compressible@~2.0.16: mime-db ">= 1.43.0 < 2" compression@^1.7.4: - version "1.7.4" - resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + version "1.8.1" + resolved "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz" + integrity sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w== dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" + bytes "3.1.2" + compressible "~2.0.18" debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" + negotiator "~0.6.4" + on-headers "~1.1.0" + safe-buffer "5.2.1" vary "~1.1.2" concat-map@0.0.1: @@ -3832,9 +3899,9 @@ connect-history-api-fallback@^2.0.0: integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== consola@^3.2.3: - version "3.2.3" - resolved "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz" - integrity sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ== + version "3.4.2" + resolved "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz" + integrity sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA== content-disposition@0.5.2: version "0.5.2" @@ -3880,17 +3947,17 @@ copy-webpack-plugin@^11.0.0: schema-utils "^4.0.0" serialize-javascript "^6.0.0" -core-js-compat@^3.38.0, core-js-compat@^3.38.1: - version "3.39.0" - resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz" - integrity sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw== +core-js-compat@^3.43.0: + version "3.47.0" + resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.47.0.tgz" + integrity sha512-IGfuznZ/n7Kp9+nypamBhvwdwLsW6KC8IOaURw2doAK5e98AG3acVLdh0woOnEqCfUtS+Vu882JE4k/DAm3ItQ== dependencies: - browserslist "^4.24.2" + browserslist "^4.28.0" -core-js-pure@^3.30.2: - version "3.36.0" - resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.36.0.tgz" - integrity sha512-cN28qmhRNgbMZZMc/RFu5w8pK9VJzpb2rJVR/lHuZJKwmXnoWOpXmMkxqBB514igkp1Hu8WGROsiOAzUcKdHOQ== +core-js-pure@^3.43.0: + version "3.47.0" + resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.47.0.tgz" + integrity sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw== core-js@^3.31.1: version "3.36.0" @@ -3950,14 +4017,14 @@ css-blank-pseudo@^7.0.1: postcss-selector-parser "^7.0.0" css-declaration-sorter@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz" - integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow== + version "7.3.0" + resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz" + integrity sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ== -css-has-pseudo@^7.0.2: - version "7.0.2" - resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz" - integrity sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ== +css-has-pseudo@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz" + integrity sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA== dependencies: "@csstools/selector-specificity" "^5.0.0" postcss-selector-parser "^7.0.0" @@ -4006,9 +4073,9 @@ css-select@^4.1.3: nth-check "^2.0.1" css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + version "5.2.2" + resolved "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz" + integrity sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw== dependencies: boolbase "^1.0.0" css-what "^6.1.0" @@ -4037,10 +4104,10 @@ css-what@^6.0.1, css-what@^6.1.0: resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== -cssdb@^8.3.0: - version "8.3.0" - resolved "https://registry.npmjs.org/cssdb/-/cssdb-8.3.0.tgz" - integrity sha512-c7bmItIg38DgGjSwDPZOYF/2o0QU/sSgkWOMyl8votOfgFuyiFKWPesmCGEsrGLxEA9uL540cp8LdaGEjUGsZQ== +cssdb@^8.4.2: + version "8.4.2" + resolved "https://registry.npmjs.org/cssdb/-/cssdb-8.4.2.tgz" + integrity sha512-PzjkRkRUS+IHDJohtxkIczlxPPZqRo0nXplsYXOMBRPjcVRjj1W4DfvRgshUYTVuUigU7ptVYkFJQ7abUB0nyg== cssesc@^3.0.0: version "3.0.0" @@ -4116,10 +4183,10 @@ csso@^5.0.5: dependencies: css-tree "~2.2.0" -csstype@^3.0.2: - version "3.1.3" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +csstype@^3.2.2: + version "3.2.3" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== cytoscape-cose-bilkent@^4.1.0: version "4.1.0" @@ -4429,7 +4496,7 @@ debounce@^1.2.1: resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz" integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug== -debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.4.1, debug@4: +debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.4.1, debug@4: version "4.4.3" resolved "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz" integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== @@ -4444,9 +4511,9 @@ debug@2.6.9: ms "2.0.0" decode-named-character-reference@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz" - integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + version "1.2.0" + resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz" + integrity sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q== dependencies: character-entities "^2.0.0" @@ -4468,14 +4535,14 @@ deepmerge@^4.3.1: integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== default-browser-id@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz" - integrity sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA== + version "5.0.1" + resolved "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz" + integrity sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q== default-browser@^5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz" - integrity sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg== + version "5.4.0" + resolved "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz" + integrity sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg== dependencies: bundle-name "^4.1.0" default-browser-id "^5.0.0" @@ -4485,7 +4552,7 @@ defer-to-connect@^2.0.1: resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== -define-data-property@^1.0.1, define-data-property@^1.1.2: +define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz" integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== @@ -4574,6 +4641,15 @@ dns-packet@^5.2.2: dependencies: "@leichtgewicht/ip-codec" "^2.0.1" +docusaurus-plugin-llms@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/docusaurus-plugin-llms/-/docusaurus-plugin-llms-0.2.2.tgz" + integrity sha512-DZlZ6cv9p5poFE00Qg78aurBNWhLa4o0VhH4kI33DUT0y4ydlFEJJbf8Bks9BuuGPFbY/Guebn+hRc2QymMImg== + dependencies: + gray-matter "^4.0.3" + minimatch "^9.0.3" + yaml "^2.8.1" + dom-converter@^0.2.0: version "0.2.0" resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz" @@ -4635,9 +4711,9 @@ domutils@^2.5.2, domutils@^2.8.0: domhandler "^4.2.0" domutils@^3.0.1: - version "3.1.0" - resolved "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + version "3.2.2" + resolved "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz" + integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== dependencies: dom-serializer "^2.0.0" domelementtype "^2.3.0" @@ -4658,6 +4734,15 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + duplexer@^0.1.2: version "0.1.2" resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" @@ -4673,10 +4758,10 @@ ee-first@1.1.1: resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== -electron-to-chromium@^1.5.160: - version "1.5.165" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz" - integrity sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw== +electron-to-chromium@^1.5.249: + version "1.5.259" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.259.tgz" + integrity sha512-I+oLXgpEJzD6Cwuwt1gYjxsDmu/S/Kd41mmLA3O+/uH2pFRO/DvOjUyGozL8j3KeLV6WyZ7ssPwELMsXCcsJAQ== emoji-regex@^8.0.0: version "8.0.0" @@ -4699,9 +4784,9 @@ emojis-list@^3.0.0: integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== emoticon@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/emoticon/-/emoticon-4.0.1.tgz" - integrity sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw== + version "4.1.0" + resolved "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz" + integrity sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ== encodeurl@~1.0.2: version "1.0.2" @@ -4731,19 +4816,22 @@ entities@^4.2.0, entities@^4.4.0: resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz" + integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== + error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + version "1.3.4" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz" + integrity sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ== dependencies: is-arrayish "^0.2.1" -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== es-errors@^1.3.0: version "1.3.0" @@ -4755,6 +4843,33 @@ es-module-lexer@^1.2.1: resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz" integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +esast-util-from-estree@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz" + integrity sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ== + dependencies: + "@types/estree-jsx" "^1.0.0" + devlop "^1.0.0" + estree-util-visit "^2.0.0" + unist-util-position-from-estree "^2.0.0" + +esast-util-from-js@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz" + integrity sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw== + dependencies: + "@types/estree-jsx" "^1.0.0" + acorn "^8.0.0" + esast-util-from-estree "^2.0.0" + vfile-message "^4.0.0" + escalade@^3.1.1, escalade@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" @@ -4837,6 +4952,14 @@ estree-util-is-identifier-name@^3.0.0: resolved "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz" integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg== +estree-util-scope@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz" + integrity sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ== + dependencies: + "@types/estree" "^1.0.0" + devlop "^1.0.0" + estree-util-to-js@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz" @@ -4847,9 +4970,9 @@ estree-util-to-js@^2.0.0: source-map "^0.7.0" estree-util-value-to-estree@^3.0.1: - version "3.3.3" - resolved "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.3.3.tgz" - integrity sha512-Db+m1WSD4+mUO7UgMeKkAwdbfNWwIxLt48XF2oFU9emPfXkIu+k5/nlOj313v7wqtAPo0f9REhUvznFrPkG8CQ== + version "3.5.0" + resolved "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.5.0.tgz" + integrity sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ== dependencies: "@types/estree" "^1.0.0" @@ -4901,7 +5024,7 @@ events@^3.2.0: resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== -eventsource-parser@^3.0.5: +eventsource-parser@^3.0.6: version "3.0.6" resolved "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz" integrity sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg== @@ -4981,25 +5104,30 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.2.11, fast-glob@^3.2.9, fast-glob@^3.3.0: - version "3.3.2" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + version "3.3.3" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" glob-parent "^5.1.2" merge2 "^1.3.0" - micromatch "^4.0.4" + micromatch "^4.0.8" fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== +fast-uri@^3.0.1: + version "3.1.0" + resolved "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz" + integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== + fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + version "1.19.1" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== dependencies: reusify "^1.0.4" @@ -5081,9 +5209,9 @@ flat@^5.0.2: integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== follow-redirects@^1.0.0: - version "1.15.6" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz" - integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + version "1.15.11" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== form-data-encoder@^2.1.2: version "2.1.4" @@ -5100,10 +5228,10 @@ forwarded@0.2.0: resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== -fraction.js@^4.3.7: - version "4.3.7" - resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz" - integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== +fraction.js@^5.3.4: + version "5.3.4" + resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz" + integrity sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ== fresh@0.5.2: version "0.5.2" @@ -5129,22 +5257,35 @@ gensync@^1.0.0-beta.2: resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" es-errors "^1.3.0" + es-object-atoms "^1.1.1" function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== +get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + get-stream@^6.0.0, get-stream@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" @@ -5170,9 +5311,9 @@ glob-parent@^6.0.1: is-glob "^4.0.3" glob-to-regex.js@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.0.1.tgz" - integrity sha512-CG/iEvgQqfzoVsMUbxSJcwbG2JwyZ3naEqPkeltwl0BSS8Bp83k3xlGms+0QdWFUAwV+uvo80wNswKF6FWEkKg== + version "1.2.0" + resolved "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz" + integrity sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ== glob-to-regexp@^0.4.1: version "0.4.1" @@ -5186,11 +5327,6 @@ global-dirs@^3.0.0: dependencies: ini "2.0.0" -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - globals@^15.15.0: version "15.15.0" resolved "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz" @@ -5219,12 +5355,10 @@ globby@^13.1.1: merge2 "^1.4.1" slash "^4.0.0" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== got@^12.1.0: version "12.6.1" @@ -5285,45 +5419,40 @@ has-flag@^4.0.0: resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.1: +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz" integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: es-define-property "^1.0.0" -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-yarn@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz" integrity sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA== -hasown@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz" - integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" hast-util-from-parse5@^8.0.0: - version "8.0.1" - resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz" - integrity sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ== + version "8.0.3" + resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz" + integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg== dependencies: "@types/hast" "^3.0.0" "@types/unist" "^3.0.0" devlop "^1.0.0" - hastscript "^8.0.0" - property-information "^6.0.0" + hastscript "^9.0.0" + property-information "^7.0.0" vfile "^6.0.0" vfile-location "^5.0.0" web-namespaces "^2.0.0" @@ -5336,9 +5465,9 @@ hast-util-parse-selector@^4.0.0: "@types/hast" "^3.0.0" hast-util-raw@^9.0.0: - version "9.0.2" - resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.2.tgz" - integrity sha512-PldBy71wO9Uq1kyaMch9AHIghtQvIwxBUkv823pKmkTM3oV1JxtsTNYdevMxvUHqcnOAuO65JKU2+0NOxc2ksA== + version "9.1.0" + resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz" + integrity sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw== dependencies: "@types/hast" "^3.0.0" "@types/unist" "^3.0.0" @@ -5355,9 +5484,9 @@ hast-util-raw@^9.0.0: zwitch "^2.0.0" hast-util-to-estree@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.0.tgz" - integrity sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw== + version "3.1.3" + resolved "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz" + integrity sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w== dependencies: "@types/estree" "^1.0.0" "@types/estree-jsx" "^1.0.0" @@ -5370,16 +5499,16 @@ hast-util-to-estree@^3.0.0: mdast-util-mdx-expression "^2.0.0" mdast-util-mdx-jsx "^3.0.0" mdast-util-mdxjs-esm "^2.0.0" - property-information "^6.0.0" + property-information "^7.0.0" space-separated-tokens "^2.0.0" - style-to-object "^0.4.0" + style-to-js "^1.0.0" unist-util-position "^5.0.0" zwitch "^2.0.0" hast-util-to-jsx-runtime@^2.0.0: - version "2.3.0" - resolved "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.0.tgz" - integrity sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ== + version "2.3.6" + resolved "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz" + integrity sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg== dependencies: "@types/estree" "^1.0.0" "@types/hast" "^3.0.0" @@ -5391,9 +5520,9 @@ hast-util-to-jsx-runtime@^2.0.0: mdast-util-mdx-expression "^2.0.0" mdast-util-mdx-jsx "^3.0.0" mdast-util-mdxjs-esm "^2.0.0" - property-information "^6.0.0" + property-information "^7.0.0" space-separated-tokens "^2.0.0" - style-to-object "^1.0.0" + style-to-js "^1.0.0" unist-util-position "^5.0.0" vfile-message "^4.0.0" @@ -5417,15 +5546,15 @@ hast-util-whitespace@^3.0.0: dependencies: "@types/hast" "^3.0.0" -hastscript@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz" - integrity sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw== +hastscript@^9.0.0: + version "9.0.1" + resolved "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz" + integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w== dependencies: "@types/hast" "^3.0.0" comma-separated-tokens "^2.0.0" hast-util-parse-selector "^4.0.0" - property-information "^6.0.0" + property-information "^7.0.0" space-separated-tokens "^2.0.0" he@^1.2.0: @@ -5566,9 +5695,9 @@ http-errors@2.0.0: toidentifier "1.0.1" http-parser-js@>=0.5.1: - version "0.5.8" - resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" - integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + version "0.5.10" + resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz" + integrity sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA== http-proxy-middleware@^2.0.9: version "2.0.9" @@ -5628,9 +5757,9 @@ icss-utils@^5.0.0, icss-utils@^5.1.0: integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== ignore@^5.2.0, ignore@^5.2.4: - version "5.3.1" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + version "5.3.2" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== image-size@^2.0.2: version "2.0.2" @@ -5638,9 +5767,9 @@ image-size@^2.0.2: integrity sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w== import-fresh@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + version "3.3.1" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz" + integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== dependencies: parent-module "^1.0.0" resolve-from "^4.0.0" @@ -5690,15 +5819,10 @@ ini@2.0.0: resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== -inline-style-parser@0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz" - integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== - -inline-style-parser@0.2.2: - version "0.2.2" - resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.2.tgz" - integrity sha512-EcKzdTHVe8wFVOGEYXiW9WmJXPjqi1T+234YpJr98RiFYKHV3cdy1+3mkTE+KHTHxFFLH51SfaGOoUdW+v7ViQ== +inline-style-parser@0.2.7: + version "0.2.7" + resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz" + integrity sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA== internmap@^1.0.0: version "1.0.1" @@ -5759,12 +5883,12 @@ is-ci@^3.0.1: dependencies: ci-info "^3.2.0" -is-core-module@^2.13.0: - version "2.13.1" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz" - integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== +is-core-module@^2.16.1: + version "2.16.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== dependencies: - hasown "^2.0.0" + hasown "^2.0.2" is-decimal@^2.0.0: version "2.0.1" @@ -5870,13 +5994,6 @@ is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" -is-reference@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz" - integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg== - dependencies: - "@types/estree" "*" - is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz" @@ -5963,14 +6080,14 @@ jest-worker@^29.4.3: supports-color "^8.0.0" jiti@^1.20.0: - version "1.21.0" - resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz" - integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + version "1.21.7" + resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz" + integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A== joi@^17.9.2: - version "17.12.1" - resolved "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz" - integrity sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ== + version "17.13.3" + resolved "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz" + integrity sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA== dependencies: "@hapi/hoek" "^9.3.0" "@hapi/topo" "^5.1.0" @@ -5984,24 +6101,24 @@ joi@^17.9.2: integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + version "3.14.2" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz" + integrity sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg== dependencies: argparse "^1.0.7" esprima "^4.0.0" js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + version "4.1.1" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz" + integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA== dependencies: argparse "^2.0.1" -jsesc@^3.0.2, jsesc@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== +jsesc@^3.0.2, jsesc@~3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== json-buffer@3.0.1: version "3.0.1" @@ -6095,9 +6212,9 @@ latest-version@^7.0.0: package-json "^8.1.0" launch-editor@^2.6.1: - version "2.11.1" - resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz" - integrity sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg== + version "2.12.0" + resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz" + integrity sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg== dependencies: picocolors "^1.1.1" shell-quote "^1.8.3" @@ -6118,9 +6235,9 @@ leven@^3.1.0: integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== lilconfig@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz" - integrity sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ== + version "3.1.3" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== lines-and-columns@^1.1.6: version "1.2.4" @@ -6233,22 +6350,28 @@ markdown-table@^2.0.0: repeat-string "^1.0.0" markdown-table@^3.0.0: - version "3.0.3" - resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz" - integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== + version "3.0.4" + resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz" + integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw== marked@^16.2.1, marked@^16.3.0: - version "16.3.0" - resolved "https://registry.npmjs.org/marked/-/marked-16.3.0.tgz" - integrity sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w== + version "16.4.2" + resolved "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz" + integrity sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA== + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== mdast-util-directive@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.0.0.tgz" - integrity sha512-JUpYOqKI4mM3sZcNxmF/ox04XYFFkNwr0CFlrQIkCwbvH0xzMCqkMqAde9wRd80VAhaUrwFwKm2nxretdT1h7Q== + version "3.1.0" + resolved "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz" + integrity sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q== dependencies: "@types/mdast" "^4.0.0" "@types/unist" "^3.0.0" + ccount "^2.0.0" devlop "^1.0.0" mdast-util-from-markdown "^2.0.0" mdast-util-to-markdown "^2.0.0" @@ -6257,9 +6380,9 @@ mdast-util-directive@^3.0.0: unist-util-visit-parents "^6.0.0" mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz" - integrity sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA== + version "3.0.2" + resolved "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz" + integrity sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg== dependencies: "@types/mdast" "^4.0.0" escape-string-regexp "^5.0.0" @@ -6267,9 +6390,9 @@ mdast-util-find-and-replace@^3.0.0, mdast-util-find-and-replace@^3.0.1: unist-util-visit-parents "^6.0.0" mdast-util-from-markdown@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz" - integrity sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA== + version "2.0.2" + resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz" + integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA== dependencies: "@types/mdast" "^4.0.0" "@types/unist" "^3.0.0" @@ -6297,9 +6420,9 @@ mdast-util-frontmatter@^2.0.0: micromark-extension-frontmatter "^2.0.0" mdast-util-gfm-autolink-literal@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz" - integrity sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg== + version "2.0.1" + resolved "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz" + integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ== dependencies: "@types/mdast" "^4.0.0" ccount "^2.0.0" @@ -6308,9 +6431,9 @@ mdast-util-gfm-autolink-literal@^2.0.0: micromark-util-character "^2.0.0" mdast-util-gfm-footnote@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz" - integrity sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ== + version "2.1.0" + resolved "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz" + integrity sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ== dependencies: "@types/mdast" "^4.0.0" devlop "^1.1.0" @@ -6349,9 +6472,9 @@ mdast-util-gfm-task-list-item@^2.0.0: mdast-util-to-markdown "^2.0.0" mdast-util-gfm@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz" - integrity sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw== + version "3.1.0" + resolved "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz" + integrity sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ== dependencies: mdast-util-from-markdown "^2.0.0" mdast-util-gfm-autolink-literal "^2.0.0" @@ -6362,9 +6485,9 @@ mdast-util-gfm@^3.0.0: mdast-util-to-markdown "^2.0.0" mdast-util-mdx-expression@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.0.tgz" - integrity sha512-fGCu8eWdKUKNu5mohVGkhBXCXGnOTLuFqOvGMvdikr+J1w7lDJgxThOKpwRWzzbyXAU2hhSwsmssOY4yTokluw== + version "2.0.1" + resolved "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz" + integrity sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ== dependencies: "@types/estree-jsx" "^1.0.0" "@types/hast" "^3.0.0" @@ -6374,9 +6497,9 @@ mdast-util-mdx-expression@^2.0.0: mdast-util-to-markdown "^2.0.0" mdast-util-mdx-jsx@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.0.tgz" - integrity sha512-A8AJHlR7/wPQ3+Jre1+1rq040fX9A4Q1jG8JxmSNp/PLPHg80A6475wxTp3KzHpApFH6yWxFotHrJQA3dXP6/w== + version "3.2.0" + resolved "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz" + integrity sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q== dependencies: "@types/estree-jsx" "^1.0.0" "@types/hast" "^3.0.0" @@ -6388,7 +6511,6 @@ mdast-util-mdx-jsx@^3.0.0: mdast-util-to-markdown "^2.0.0" parse-entities "^4.0.0" stringify-entities "^4.0.0" - unist-util-remove-position "^5.0.0" unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" @@ -6424,9 +6546,9 @@ mdast-util-phrasing@^4.0.0: unist-util-is "^6.0.0" mdast-util-to-hast@^13.0.0: - version "13.1.0" - resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.1.0.tgz" - integrity sha512-/e2l/6+OdGp/FB+ctrJ9Avz71AN/GRH3oi/3KAx/kMnoUsD6q0woXlDT8lLEeViVKE7oZxE7RXzvO3T8kF2/sA== + version "13.2.1" + resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz" + integrity sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA== dependencies: "@types/hast" "^3.0.0" "@types/mdast" "^4.0.0" @@ -6439,15 +6561,16 @@ mdast-util-to-hast@^13.0.0: vfile "^6.0.0" mdast-util-to-markdown@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz" - integrity sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ== + version "2.1.2" + resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz" + integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA== dependencies: "@types/mdast" "^4.0.0" "@types/unist" "^3.0.0" longest-streak "^3.0.0" mdast-util-phrasing "^4.0.0" mdast-util-to-string "^4.0.0" + micromark-util-classify-character "^2.0.0" micromark-util-decode-string "^2.0.0" unist-util-visit "^5.0.0" zwitch "^2.0.0" @@ -6475,9 +6598,9 @@ media-typer@0.3.0: integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== memfs@^4.43.1: - version "4.46.1" - resolved "https://registry.npmjs.org/memfs/-/memfs-4.46.1.tgz" - integrity sha512-2wjHDg7IjP+ufAqqqSxjiNePFDrvWviA79ajUwG9lkHhk3HzZOLBzzoUG8cx9vCagj6VvBQD7oXuLuFz5LaAOQ== + version "4.51.0" + resolved "https://registry.npmjs.org/memfs/-/memfs-4.51.0.tgz" + integrity sha512-4zngfkVM/GpIhC8YazOsM6E8hoB33NP0BCESPOA6z7qaL6umPJNqkO8CNYaLV2FB2MV6H1O3x2luHHOSqppv+A== dependencies: "@jsonjoy.com/json-pack" "^1.11.0" "@jsonjoy.com/util" "^1.9.0" @@ -6533,9 +6656,9 @@ methods@~1.1.2: integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== micromark-core-commonmark@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz" - integrity sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA== + version "2.0.3" + resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz" + integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg== dependencies: decode-named-character-reference "^1.0.0" devlop "^1.0.0" @@ -6555,9 +6678,9 @@ micromark-core-commonmark@^2.0.0: micromark-util-types "^2.0.0" micromark-extension-directive@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.0.tgz" - integrity sha512-61OI07qpQrERc+0wEysLHMvoiO3s2R56x5u7glHq2Yqq6EHbH4dW25G9GfDdGCDYqA21KE6DWgNSzxSwHc2hSg== + version "3.0.2" + resolved "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz" + integrity sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA== dependencies: devlop "^1.0.0" micromark-factory-space "^2.0.0" @@ -6578,9 +6701,9 @@ micromark-extension-frontmatter@^2.0.0: micromark-util-types "^2.0.0" micromark-extension-gfm-autolink-literal@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.0.0.tgz" - integrity sha512-rTHfnpt/Q7dEAK1Y5ii0W8bhfJlVJFnJMHIPisfPK3gpVNuOP0VnRl96+YJ3RYWV/P4gFeQoGKNlT3RhuvpqAg== + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz" + integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw== dependencies: micromark-util-character "^2.0.0" micromark-util-sanitize-uri "^2.0.0" @@ -6588,9 +6711,9 @@ micromark-extension-gfm-autolink-literal@^2.0.0: micromark-util-types "^2.0.0" micromark-extension-gfm-footnote@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.0.0.tgz" - integrity sha512-6Rzu0CYRKDv3BfLAUnZsSlzx3ak6HAoI85KTiijuKIz5UxZxbUI+pD6oHgw+6UtQuiRwnGRhzMmPRv4smcz0fg== + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz" + integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw== dependencies: devlop "^1.0.0" micromark-core-commonmark "^2.0.0" @@ -6602,9 +6725,9 @@ micromark-extension-gfm-footnote@^2.0.0: micromark-util-types "^2.0.0" micromark-extension-gfm-strikethrough@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.0.0.tgz" - integrity sha512-c3BR1ClMp5fxxmwP6AoOY2fXO9U8uFMKs4ADD66ahLTNcwzSCyRVU4k7LPV5Nxo/VJiR4TdzxRQY2v3qIUceCw== + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz" + integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw== dependencies: devlop "^1.0.0" micromark-util-chunked "^2.0.0" @@ -6614,9 +6737,9 @@ micromark-extension-gfm-strikethrough@^2.0.0: micromark-util-types "^2.0.0" micromark-extension-gfm-table@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.0.0.tgz" - integrity sha512-PoHlhypg1ItIucOaHmKE8fbin3vTLpDOUg8KAr8gRCF1MOZI9Nquq2i/44wFvviM4WuxJzc3demT8Y3dkfvYrw== + version "2.1.1" + resolved "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz" + integrity sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg== dependencies: devlop "^1.0.0" micromark-factory-space "^2.0.0" @@ -6632,9 +6755,9 @@ micromark-extension-gfm-tagfilter@^2.0.0: micromark-util-types "^2.0.0" micromark-extension-gfm-task-list-item@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.0.1.tgz" - integrity sha512-cY5PzGcnULaN5O7T+cOzfMoHjBW7j+T9D2sucA5d/KbsBTPcYdebm9zUd9zzdgJGCwahV+/W78Z3nbulBYVbTw== + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz" + integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw== dependencies: devlop "^1.0.0" micromark-factory-space "^2.0.0" @@ -6657,9 +6780,9 @@ micromark-extension-gfm@^3.0.0: micromark-util-types "^2.0.0" micromark-extension-mdx-expression@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.0.tgz" - integrity sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ== + version "3.0.1" + resolved "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz" + integrity sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q== dependencies: "@types/estree" "^1.0.0" devlop "^1.0.0" @@ -6671,17 +6794,17 @@ micromark-extension-mdx-expression@^3.0.0: micromark-util-types "^2.0.0" micromark-extension-mdx-jsx@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.0.tgz" - integrity sha512-uvhhss8OGuzR4/N17L1JwvmJIpPhAd8oByMawEKx6NVdBCbesjH4t+vjEp3ZXft9DwvlKSD07fCeI44/N0Vf2w== + version "3.0.2" + resolved "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz" + integrity sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ== dependencies: - "@types/acorn" "^4.0.0" "@types/estree" "^1.0.0" devlop "^1.0.0" estree-util-is-identifier-name "^3.0.0" micromark-factory-mdx-expression "^2.0.0" micromark-factory-space "^2.0.0" micromark-util-character "^2.0.0" + micromark-util-events-to-acorn "^2.0.0" micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" vfile-message "^4.0.0" @@ -6723,18 +6846,18 @@ micromark-extension-mdxjs@^3.0.0: micromark-util-types "^2.0.0" micromark-factory-destination@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz" - integrity sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz" + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== dependencies: micromark-util-character "^2.0.0" micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" micromark-factory-label@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz" - integrity sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz" + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== dependencies: devlop "^1.0.0" micromark-util-character "^2.0.0" @@ -6742,12 +6865,13 @@ micromark-factory-label@^2.0.0: micromark-util-types "^2.0.0" micromark-factory-mdx-expression@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.1.tgz" - integrity sha512-F0ccWIUHRLRrYp5TC9ZYXmZo+p2AM13ggbsW4T0b5CRKP8KHVRB8t4pwtBgTxtjRmwrK0Irwm7vs2JOZabHZfg== + version "2.0.3" + resolved "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz" + integrity sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ== dependencies: "@types/estree" "^1.0.0" devlop "^1.0.0" + micromark-factory-space "^2.0.0" micromark-util-character "^2.0.0" micromark-util-events-to-acorn "^2.0.0" micromark-util-symbol "^2.0.0" @@ -6764,17 +6888,17 @@ micromark-factory-space@^1.0.0: micromark-util-types "^1.0.0" micromark-factory-space@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz" - integrity sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz" + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== dependencies: micromark-util-character "^2.0.0" micromark-util-types "^2.0.0" micromark-factory-title@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz" - integrity sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz" + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== dependencies: micromark-factory-space "^2.0.0" micromark-util-character "^2.0.0" @@ -6782,9 +6906,9 @@ micromark-factory-title@^2.0.0: micromark-util-types "^2.0.0" micromark-factory-whitespace@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz" - integrity sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz" + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== dependencies: micromark-factory-space "^2.0.0" micromark-util-character "^2.0.0" @@ -6800,48 +6924,48 @@ micromark-util-character@^1.0.0, micromark-util-character@^1.1.0: micromark-util-types "^1.0.0" micromark-util-character@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz" - integrity sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ== + version "2.1.1" + resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz" + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== dependencies: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" micromark-util-chunked@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz" - integrity sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz" + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== dependencies: micromark-util-symbol "^2.0.0" micromark-util-classify-character@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz" - integrity sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz" + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== dependencies: micromark-util-character "^2.0.0" micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" micromark-util-combine-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz" - integrity sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz" + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== dependencies: micromark-util-chunked "^2.0.0" micromark-util-types "^2.0.0" micromark-util-decode-numeric-character-reference@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz" - integrity sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ== + version "2.0.2" + resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz" + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== dependencies: micromark-util-symbol "^2.0.0" micromark-util-decode-string@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz" - integrity sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz" + integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ== dependencies: decode-named-character-reference "^1.0.0" micromark-util-character "^2.0.0" @@ -6849,16 +6973,15 @@ micromark-util-decode-string@^2.0.0: micromark-util-symbol "^2.0.0" micromark-util-encode@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz" - integrity sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz" + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== micromark-util-events-to-acorn@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.2.tgz" - integrity sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA== + version "2.0.3" + resolved "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz" + integrity sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg== dependencies: - "@types/acorn" "^4.0.0" "@types/estree" "^1.0.0" "@types/unist" "^3.0.0" devlop "^1.0.0" @@ -6868,37 +6991,37 @@ micromark-util-events-to-acorn@^2.0.0: vfile-message "^4.0.0" micromark-util-html-tag-name@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz" - integrity sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz" + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== micromark-util-normalize-identifier@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz" - integrity sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz" + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== dependencies: micromark-util-symbol "^2.0.0" micromark-util-resolve-all@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz" - integrity sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz" + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== dependencies: micromark-util-types "^2.0.0" micromark-util-sanitize-uri@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz" - integrity sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz" + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== dependencies: micromark-util-character "^2.0.0" micromark-util-encode "^2.0.0" micromark-util-symbol "^2.0.0" micromark-util-subtokenize@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz" - integrity sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg== + version "2.1.0" + resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz" + integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA== dependencies: devlop "^1.0.0" micromark-util-chunked "^2.0.0" @@ -6911,9 +7034,9 @@ micromark-util-symbol@^1.0.0, micromark-util-symbol@^1.0.1: integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== micromark-util-symbol@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz" - integrity sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw== + version "2.0.1" + resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz" + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== micromark-util-types@^1.0.0: version "1.1.0" @@ -6921,14 +7044,14 @@ micromark-util-types@^1.0.0: integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== micromark-util-types@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz" - integrity sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w== + version "2.0.2" + resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz" + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== micromark@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz" - integrity sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ== + version "4.0.2" + resolved "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz" + integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA== dependencies: "@types/debug" "^4.0.0" debug "^4.0.0" @@ -6948,7 +7071,7 @@ micromark@^4.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: +micromatch@^4.0.2, micromatch@^4.0.5, micromatch@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== @@ -6984,9 +7107,9 @@ mime-types@^2.1.27: mime-db "1.52.0" mime-types@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz" - integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA== + version "3.0.2" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz" + integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A== dependencies: mime-db "^1.54.0" @@ -7032,9 +7155,9 @@ mimic-response@^4.0.0: integrity sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg== mini-css-extract-plugin@^2.9.2: - version "2.9.2" - resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz" - integrity sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w== + version "2.9.4" + resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz" + integrity sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ== dependencies: schema-utils "^4.0.0" tapable "^2.2.1" @@ -7044,6 +7167,13 @@ minimalistic-assert@^1.0.0: resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== +minimatch@^9.0.3: + version "9.0.5" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimatch@3.1.2: version "3.1.2" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" @@ -7094,6 +7224,11 @@ nanoid@^3.3.11: resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz" integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== +negotiator@~0.6.4: + version "0.6.4" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz" + integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w== + negotiator@0.6.3: version "0.6.3" resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" @@ -7113,9 +7248,9 @@ no-case@^3.0.4: tslib "^2.0.3" node-emoji@^2.1.0: - version "2.1.3" - resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz" - integrity sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA== + version "2.2.0" + resolved "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz" + integrity sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw== dependencies: "@sindresorhus/is" "^4.6.0" char-regex "^1.0.2" @@ -7123,14 +7258,14 @@ node-emoji@^2.1.0: skin-tone "^2.0.0" node-forge@^1: - version "1.3.1" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + version "1.3.2" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz" + integrity sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw== -node-releases@^2.0.19: - version "2.0.19" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz" - integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== +node-releases@^2.0.27: + version "2.0.27" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz" + integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -7179,10 +7314,10 @@ object-assign@^4.1.1: resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== -object-inspect@^1.13.1: - version "1.13.1" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz" - integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== object-keys@^1.1.1: version "1.1.1" @@ -7190,13 +7325,15 @@ object-keys@^1.1.1: integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object.assign@^4.1.0: - version "4.1.5" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + version "4.1.7" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== dependencies: - call-bind "^1.0.5" + call-bind "^1.0.8" + call-bound "^1.0.3" define-properties "^1.2.1" - has-symbols "^1.0.3" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" object-keys "^1.1.1" obuf@^1.0.0, obuf@^1.1.2: @@ -7211,10 +7348,10 @@ on-finished@^2.4.1, on-finished@2.4.1: dependencies: ee-first "1.1.1" -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== +on-headers@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz" + integrity sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A== onetime@^5.1.2: version "5.1.2" @@ -7333,12 +7470,11 @@ parent-module@^1.0.0: callsites "^3.0.0" parse-entities@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz" - integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== + version "4.0.2" + resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz" + integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw== dependencies: "@types/unist" "^2.0.0" - character-entities "^2.0.0" character-entities-legacy "^3.0.0" character-reference-invalid "^2.0.0" decode-named-character-reference "^1.0.0" @@ -7362,19 +7498,19 @@ parse-numeric-range@^1.3.0: integrity sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ== parse5-htmlparser2-tree-adapter@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz" - integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + version "7.1.0" + resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz" + integrity sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g== dependencies: - domhandler "^5.0.2" + domhandler "^5.0.3" parse5 "^7.0.0" parse5@^7.0.0: - version "7.1.2" - resolved "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + version "7.3.0" + resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz" + integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== dependencies: - entities "^4.4.0" + entities "^6.0.0" parseurl@~1.3.2, parseurl@~1.3.3: version "1.3.3" @@ -7441,15 +7577,6 @@ pathe@^2.0.1, pathe@^2.0.3: resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz" integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== -periscopic@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz" - integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^3.0.0" - is-reference "^3.0.0" - picocolors@^1.0.0, picocolors@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" @@ -7520,15 +7647,15 @@ postcss-clamp@^4.1.0: dependencies: postcss-value-parser "^4.2.0" -postcss-color-functional-notation@^7.0.10: - version "7.0.10" - resolved "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.10.tgz" - integrity sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA== +postcss-color-functional-notation@^7.0.12: + version "7.0.12" + resolved "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz" + integrity sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" postcss-color-hex-alpha@^10.0.0: @@ -7630,12 +7757,12 @@ postcss-discard-unused@^6.0.5: dependencies: postcss-selector-parser "^6.0.16" -postcss-double-position-gradients@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.2.tgz" - integrity sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q== +postcss-double-position-gradients@^6.0.4: + version "6.0.4" + resolved "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz" + integrity sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g== dependencies: - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" postcss-value-parser "^4.2.0" @@ -7671,15 +7798,15 @@ postcss-image-set-function@^7.0.0: "@csstools/utilities" "^2.0.0" postcss-value-parser "^4.2.0" -postcss-lab-function@^7.0.10: - version "7.0.10" - resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.10.tgz" - integrity sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ== +postcss-lab-function@^7.0.12: + version "7.0.12" + resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz" + integrity sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w== dependencies: - "@csstools/css-color-parser" "^3.0.10" + "@csstools/css-color-parser" "^3.1.0" "@csstools/css-parser-algorithms" "^3.0.5" "@csstools/css-tokenizer" "^3.0.4" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/utilities" "^2.0.0" postcss-loader@^7.3.4: @@ -7784,12 +7911,12 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" -postcss-nesting@^13.0.1: - version "13.0.1" - resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz" - integrity sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ== +postcss-nesting@^13.0.2: + version "13.0.2" + resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz" + integrity sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ== dependencies: - "@csstools/selector-resolve-nested" "^3.0.0" + "@csstools/selector-resolve-nested" "^3.1.0" "@csstools/selector-specificity" "^5.0.0" postcss-selector-parser "^7.0.0" @@ -7888,24 +8015,27 @@ postcss-place@^10.0.0: postcss-value-parser "^4.2.0" postcss-preset-env@^10.2.1: - version "10.2.1" - resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.2.1.tgz" - integrity sha512-mDInnlm4mYhmR0S79hNLzseW9nx4Ihd8s15K99iu6u6QhoSQgqWX9Oj6nTd/8Dz3b0T7v2JSrfnXsDfv9TFvDg== - dependencies: - "@csstools/postcss-cascade-layers" "^5.0.1" - "@csstools/postcss-color-function" "^4.0.10" - "@csstools/postcss-color-mix-function" "^3.0.10" - "@csstools/postcss-color-mix-variadic-function-arguments" "^1.0.0" - "@csstools/postcss-content-alt-text" "^2.0.6" + version "10.4.0" + resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.4.0.tgz" + integrity sha512-2kqpOthQ6JhxqQq1FSAAZGe9COQv75Aw8WbsOvQVNJ2nSevc9Yx/IKZGuZ7XJ+iOTtVon7LfO7ELRzg8AZ+sdw== + dependencies: + "@csstools/postcss-alpha-function" "^1.0.1" + "@csstools/postcss-cascade-layers" "^5.0.2" + "@csstools/postcss-color-function" "^4.0.12" + "@csstools/postcss-color-function-display-p3-linear" "^1.0.1" + "@csstools/postcss-color-mix-function" "^3.0.12" + "@csstools/postcss-color-mix-variadic-function-arguments" "^1.0.2" + "@csstools/postcss-content-alt-text" "^2.0.8" + "@csstools/postcss-contrast-color-function" "^2.0.12" "@csstools/postcss-exponential-functions" "^2.0.9" "@csstools/postcss-font-format-keywords" "^4.0.0" - "@csstools/postcss-gamut-mapping" "^2.0.10" - "@csstools/postcss-gradients-interpolation-method" "^5.0.10" - "@csstools/postcss-hwb-function" "^4.0.10" - "@csstools/postcss-ic-unit" "^4.0.2" + "@csstools/postcss-gamut-mapping" "^2.0.11" + "@csstools/postcss-gradients-interpolation-method" "^5.0.12" + "@csstools/postcss-hwb-function" "^4.0.12" + "@csstools/postcss-ic-unit" "^4.0.4" "@csstools/postcss-initial" "^2.0.1" - "@csstools/postcss-is-pseudo-class" "^5.0.1" - "@csstools/postcss-light-dark-function" "^2.0.9" + "@csstools/postcss-is-pseudo-class" "^5.0.3" + "@csstools/postcss-light-dark-function" "^2.0.11" "@csstools/postcss-logical-float-and-clear" "^3.0.0" "@csstools/postcss-logical-overflow" "^2.0.0" "@csstools/postcss-logical-overscroll-behavior" "^2.0.0" @@ -7915,40 +8045,40 @@ postcss-preset-env@^10.2.1: "@csstools/postcss-media-queries-aspect-ratio-number-values" "^3.0.5" "@csstools/postcss-nested-calc" "^4.0.0" "@csstools/postcss-normalize-display-values" "^4.0.0" - "@csstools/postcss-oklab-function" "^4.0.10" - "@csstools/postcss-progressive-custom-properties" "^4.1.0" + "@csstools/postcss-oklab-function" "^4.0.12" + "@csstools/postcss-progressive-custom-properties" "^4.2.1" "@csstools/postcss-random-function" "^2.0.1" - "@csstools/postcss-relative-color-syntax" "^3.0.10" + "@csstools/postcss-relative-color-syntax" "^3.0.12" "@csstools/postcss-scope-pseudo-class" "^4.0.1" "@csstools/postcss-sign-functions" "^1.1.4" "@csstools/postcss-stepped-value-functions" "^4.0.9" - "@csstools/postcss-text-decoration-shorthand" "^4.0.2" + "@csstools/postcss-text-decoration-shorthand" "^4.0.3" "@csstools/postcss-trigonometric-functions" "^4.0.9" "@csstools/postcss-unset-value" "^4.0.0" autoprefixer "^10.4.21" - browserslist "^4.25.0" + browserslist "^4.26.0" css-blank-pseudo "^7.0.1" - css-has-pseudo "^7.0.2" + css-has-pseudo "^7.0.3" css-prefers-color-scheme "^10.0.0" - cssdb "^8.3.0" + cssdb "^8.4.2" postcss-attribute-case-insensitive "^7.0.1" postcss-clamp "^4.1.0" - postcss-color-functional-notation "^7.0.10" + postcss-color-functional-notation "^7.0.12" postcss-color-hex-alpha "^10.0.0" postcss-color-rebeccapurple "^10.0.0" postcss-custom-media "^11.0.6" postcss-custom-properties "^14.0.6" postcss-custom-selectors "^8.0.5" postcss-dir-pseudo-class "^9.0.1" - postcss-double-position-gradients "^6.0.2" + postcss-double-position-gradients "^6.0.4" postcss-focus-visible "^10.0.1" postcss-focus-within "^9.0.1" postcss-font-variant "^5.0.0" postcss-gap-properties "^6.0.0" postcss-image-set-function "^7.0.0" - postcss-lab-function "^7.0.10" + postcss-lab-function "^7.0.12" postcss-logical "^8.1.0" - postcss-nesting "^13.0.1" + postcss-nesting "^13.0.2" postcss-opacity-percentage "^3.0.0" postcss-overflow-shorthand "^6.0.0" postcss-page-break "^3.0.4" @@ -7999,17 +8129,17 @@ postcss-selector-not@^8.0.1: postcss-selector-parser "^7.0.0" postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16: - version "6.0.16" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz" - integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw== + version "6.1.2" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" postcss-selector-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz" - integrity sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ== + version "7.1.0" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz" + integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -8047,9 +8177,9 @@ postcss-zindex@^6.0.2: integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg== "postcss@^7.0.0 || ^8.0.1", postcss@^8, postcss@^8.0.3, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.2.2, postcss@^8.4, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.31, postcss@^8.4.33, postcss@^8.4.6, postcss@^8.5.4: - version "8.5.4" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz" - integrity sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w== + version "8.5.6" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz" + integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg== dependencies: nanoid "^3.3.11" picocolors "^1.1.1" @@ -8104,9 +8234,14 @@ prop-types@^15.6.2, prop-types@^15.7.2: react-is "^16.13.1" property-information@^6.0.0: - version "6.4.1" - resolved "https://registry.npmjs.org/property-information/-/property-information-6.4.1.tgz" - integrity sha512-OHYtXfu5aI2sS2LWFSN5rgJjrQ4pCy8i1jubJLe2QvMF8JJ++HXTUIVWFLfXJoaOfvYYjk2SN8J2wFUWIGXT4w== + version "6.5.0" + resolved "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + +property-information@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz" + integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ== proto-list@~1.2.1: version "1.2.4" @@ -8198,9 +8333,9 @@ rc@1.2.8: strip-json-comments "~2.0.1" react-dom@*, "react-dom@^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18.0.0 || ^19.0.0", react-dom@^19.0.0, "react-dom@>= 16.8.0 < 20.0.0": - version "19.2.0" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz" - integrity sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ== + version "19.2.3" + resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz" + integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg== dependencies: scheduler "^0.27.0" @@ -8226,9 +8361,9 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== react-json-view-lite@^2.3.0: - version "2.4.1" - resolved "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.4.1.tgz" - integrity sha512-fwFYknRIBxjbFm0kBDrzgBy1xa5tDg2LyXXBepC5f1b+MY3BUClMCsvanMPn089JbV1Eg3nZcrp0VCuH43aXnA== + version "2.5.0" + resolved "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz" + integrity sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g== react-loadable-ssr-addon-v5-slorber@^1.0.1: version "1.0.1" @@ -8279,10 +8414,10 @@ react-router@^5.3.4, react-router@>=5, react-router@5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react@*, "react@^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18 || ^19 || ^19.0.0-rc", "react@^18.0.0 || ^19.0.0", react@^19.0.0, react@^19.2.0, "react@>= 16.8.0 < 20.0.0", react@>=15, react@>=16, react@>=16.0.0: - version "19.2.0" - resolved "https://registry.npmjs.org/react/-/react-19.2.0.tgz" - integrity sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ== +react@*, "react@^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18 || ^19 || ^19.0.0-rc", "react@^18.0.0 || ^19.0.0", react@^19.2.0, react@^19.2.3, "react@>= 16.8.0 < 20.0.0", react@>=15, react@>=16, react@>=16.0.0: + version "19.2.3" + resolved "https://registry.npmjs.org/react/-/react-19.2.3.tgz" + integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA== readable-stream@^2.0.1: version "2.3.8" @@ -8313,10 +8448,50 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -regenerate-unicode-properties@^10.2.0: - version "10.2.0" - resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz" - integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== +recma-build-jsx@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz" + integrity sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew== + dependencies: + "@types/estree" "^1.0.0" + estree-util-build-jsx "^3.0.0" + vfile "^6.0.0" + +recma-jsx@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz" + integrity sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w== + dependencies: + acorn-jsx "^5.0.0" + estree-util-to-js "^2.0.0" + recma-parse "^1.0.0" + recma-stringify "^1.0.0" + unified "^11.0.0" + +recma-parse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz" + integrity sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ== + dependencies: + "@types/estree" "^1.0.0" + esast-util-from-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +recma-stringify@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz" + integrity sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g== + dependencies: + "@types/estree" "^1.0.0" + estree-util-to-js "^2.0.0" + unified "^11.0.0" + vfile "^6.0.0" + +regenerate-unicode-properties@^10.2.2: + version "10.2.2" + resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz" + integrity sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g== dependencies: regenerate "^1.4.2" @@ -8330,24 +8505,17 @@ regenerator-runtime@^0.14.0: resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== -regenerator-transform@^0.15.2: - version "0.15.2" - resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz" - integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== - dependencies: - "@babel/runtime" "^7.8.4" - -regexpu-core@^6.1.1: - version "6.1.1" - resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz" - integrity sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw== +regexpu-core@^6.3.1: + version "6.4.0" + resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz" + integrity sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA== dependencies: regenerate "^1.4.2" - regenerate-unicode-properties "^10.2.0" + regenerate-unicode-properties "^10.2.2" regjsgen "^0.8.0" - regjsparser "^0.11.0" + regjsparser "^0.13.0" unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" + unicode-match-property-value-ecmascript "^2.2.1" registry-auth-token@^5.0.1: version "5.0.2" @@ -8368,12 +8536,12 @@ regjsgen@^0.8.0: resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz" integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== -regjsparser@^0.11.0: - version "0.11.2" - resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.2.tgz" - integrity sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA== +regjsparser@^0.13.0: + version "0.13.0" + resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz" + integrity sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q== dependencies: - jsesc "~3.0.2" + jsesc "~3.1.0" rehype-raw@^7.0.0: version "7.0.0" @@ -8384,15 +8552,24 @@ rehype-raw@^7.0.0: hast-util-raw "^9.0.0" vfile "^6.0.0" +rehype-recma@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz" + integrity sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw== + dependencies: + "@types/estree" "^1.0.0" + "@types/hast" "^3.0.0" + hast-util-to-estree "^3.0.0" + relateurl@^0.2.7: version "0.2.7" resolved "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz" integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== remark-directive@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.0.tgz" - integrity sha512-l1UyWJ6Eg1VPU7Hm/9tt0zKtReJQNOA4+iDMAxTyZNWnJnFlbS/7zhiel/rogTLQ2vMYwDzSJa4BiVNqGlqIMA== + version "3.0.1" + resolved "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz" + integrity sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A== dependencies: "@types/mdast" "^4.0.0" mdast-util-directive "^3.0.0" @@ -8421,9 +8598,9 @@ remark-frontmatter@^5.0.0: unified "^11.0.0" remark-gfm@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz" - integrity sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA== + version "4.0.1" + resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz" + integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg== dependencies: "@types/mdast" "^4.0.0" mdast-util-gfm "^3.0.0" @@ -8433,9 +8610,9 @@ remark-gfm@^4.0.0: unified "^11.0.0" remark-mdx@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.0.1.tgz" - integrity sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA== + version "3.1.1" + resolved "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz" + integrity sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg== dependencies: mdast-util-mdx "^3.0.0" micromark-extension-mdxjs "^3.0.0" @@ -8451,9 +8628,9 @@ remark-parse@^11.0.0: unified "^11.0.0" remark-rehype@^11.0.0: - version "11.1.0" - resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz" - integrity sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g== + version "11.1.2" + resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz" + integrity sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw== dependencies: "@types/hast" "^3.0.0" "@types/mdast" "^4.0.0" @@ -8516,12 +8693,12 @@ resolve-pathname@^3.0.0: resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz" integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== -resolve@^1.14.2: - version "1.22.8" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== +resolve@^1.22.10: + version "1.22.11" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz" + integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ== dependencies: - is-core-module "^2.13.0" + is-core-module "^2.16.1" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -8538,9 +8715,9 @@ retry@^0.13.1: integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + version "1.1.0" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== robust-predicates@^3.0.2: version "3.0.2" @@ -8558,9 +8735,9 @@ roughjs@^4.6.6: points-on-path "^0.2.1" rtlcss@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz" - integrity sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ== + version "4.3.0" + resolved "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz" + integrity sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig== dependencies: escalade "^3.1.1" picocolors "^1.0.0" @@ -8594,20 +8771,15 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== sax@^1.2.4: - version "1.3.0" - resolved "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz" - integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== + version "1.4.3" + resolved "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz" + integrity sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ== scheduler@^0.27.0: version "0.27.0" @@ -8647,9 +8819,9 @@ schema-utils@^3.2.0: ajv-keywords "^3.5.2" schema-utils@^4.0.0, schema-utils@^4.0.1, schema-utils@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz" - integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + version "4.3.3" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz" + integrity sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA== dependencies: "@types/json-schema" "^7.0.9" ajv "^8.9.0" @@ -8763,17 +8935,17 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" -set-function-length@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz" - integrity sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g== +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== dependencies: - define-data-property "^1.1.2" + define-data-property "^1.1.4" es-errors "^1.3.0" function-bind "^1.1.2" - get-intrinsic "^1.2.3" + get-intrinsic "^1.2.4" gopd "^1.0.1" - has-property-descriptors "^1.0.1" + has-property-descriptors "^1.0.2" setprototypeof@1.1.0: version "1.1.0" @@ -8814,15 +8986,45 @@ shell-quote@^1.8.3: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz" integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw== +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + version "1.1.0" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== dependencies: - call-bind "^1.0.7" es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" @@ -8844,9 +9046,9 @@ sisteransi@^1.0.5: integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== sitemap@^7.1.1: - version "7.1.1" - resolved "https://registry.npmjs.org/sitemap/-/sitemap-7.1.1.tgz" - integrity sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg== + version "7.1.2" + resolved "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz" + integrity sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw== dependencies: "@types/node" "^17.0.5" "@types/sax" "^1.2.1" @@ -8911,9 +9113,9 @@ source-map@^0.6.0: integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== source-map@^0.7.0: - version "0.7.4" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + version "0.7.6" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz" + integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ== source-map@~0.6.0: version "0.6.1" @@ -8969,9 +9171,9 @@ statuses@2.0.1: integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== std-env@^3.7.0: - version "3.7.0" - resolved "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz" - integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== + version "3.10.0" + resolved "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz" + integrity sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== string_decoder@^1.1.1: version "1.3.0" @@ -9015,9 +9217,9 @@ string-width@^5.0.1, string-width@^5.1.2: strip-ansi "^7.0.1" stringify-entities@^4.0.0: - version "4.0.3" - resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.3.tgz" - integrity sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g== + version "4.0.4" + resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== dependencies: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" @@ -9065,19 +9267,19 @@ strip-json-comments@~2.0.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== -style-to-object@^0.4.0: - version "0.4.4" - resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-0.4.4.tgz" - integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== +style-to-js@^1.0.0: + version "1.1.21" + resolved "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz" + integrity sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ== dependencies: - inline-style-parser "0.1.1" + style-to-object "1.0.14" -style-to-object@^1.0.0: - version "1.0.5" - resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.5.tgz" - integrity sha512-rDRwHtoDD3UMMrmZ6BzOW0naTjMsVZLIjsGleSKS/0Oz+cgCfAPRspaqJuE8rDzpKha/nEvnM0IF4seEAZUTKQ== +style-to-object@1.0.14: + version "1.0.14" + resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz" + integrity sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw== dependencies: - inline-style-parser "0.2.2" + inline-style-parser "0.2.7" stylehacks@^6.1.1: version "6.1.1" @@ -9117,9 +9319,9 @@ svg-parser@^2.0.4: integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== svgo@^3.0.2, svgo@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/svgo/-/svgo-3.2.0.tgz" - integrity sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ== + version "3.3.2" + resolved "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz" + integrity sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw== dependencies: "@trysound/sax" "0.2.0" commander "^7.2.0" @@ -9215,7 +9417,7 @@ totalist@^3.0.0: resolved "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz" integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== -tree-dump@^1.0.3: +tree-dump@^1.0.3, tree-dump@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz" integrity sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA== @@ -9286,9 +9488,9 @@ undici-types@~5.26.4: integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + version "2.0.1" + resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== unicode-emoji-modifier-base@^1.0.0: version "1.0.0" @@ -9303,20 +9505,20 @@ unicode-match-property-ecmascript@^2.0.0: unicode-canonical-property-names-ecmascript "^2.0.0" unicode-property-aliases-ecmascript "^2.0.0" -unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== +unicode-match-property-value-ecmascript@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz" + integrity sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg== unicode-property-aliases-ecmascript@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" - integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + version "2.2.0" + resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz" + integrity sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ== unified@^11.0.0, unified@^11.0.3, unified@^11.0.4: - version "11.0.4" - resolved "https://registry.npmjs.org/unified/-/unified-11.0.4.tgz" - integrity sha512-apMPnyLjAX+ty4OrNap7yumyVAMlKx5IWU2wlzzUdYJO9A8f1p9m/gywF/GM2ZDFcjQPrx59Mc90KwmxsoklxQ== + version "11.0.5" + resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz" + integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA== dependencies: "@types/unist" "^3.0.0" bail "^2.0.0" @@ -9334,9 +9536,9 @@ unique-string@^3.0.0: crypto-random-string "^4.0.0" unist-util-is@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz" - integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw== + version "6.0.1" + resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz" + integrity sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g== dependencies: "@types/unist" "^3.0.0" @@ -9354,14 +9556,6 @@ unist-util-position@^5.0.0: dependencies: "@types/unist" "^3.0.0" -unist-util-remove-position@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz" - integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q== - dependencies: - "@types/unist" "^3.0.0" - unist-util-visit "^5.0.0" - unist-util-stringify-position@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz" @@ -9370,9 +9564,9 @@ unist-util-stringify-position@^4.0.0: "@types/unist" "^3.0.0" unist-util-visit-parents@^6.0.0: - version "6.0.1" - resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz" - integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw== + version "6.0.2" + resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz" + integrity sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ== dependencies: "@types/unist" "^3.0.0" unist-util-is "^6.0.0" @@ -9396,10 +9590,10 @@ unpipe@~1.0.0, unpipe@1.0.0: resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== -update-browserslist-db@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz" - integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== +update-browserslist-db@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz" + integrity sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A== dependencies: escalade "^3.2.0" picocolors "^1.1.1" @@ -9441,9 +9635,9 @@ url-loader@^4.1.1: schema-utils "^3.0.0" use-sync-external-store@^1.4.0: - version "1.5.0" - resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz" - integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A== + version "1.6.0" + resolved "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" @@ -9486,28 +9680,27 @@ vary@~1.1.2: integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== vfile-location@^5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.2.tgz" - integrity sha512-NXPYyxyBSH7zB5U6+3uDdd6Nybz6o6/od9rk8bp9H8GR3L+cm/fC0uUTbqBmUTnMCUDslAGBOIKNfvvb+gGlDg== + version "5.0.3" + resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz" + integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg== dependencies: "@types/unist" "^3.0.0" vfile "^6.0.0" vfile-message@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz" - integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + version "4.0.3" + resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz" + integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw== dependencies: "@types/unist" "^3.0.0" unist-util-stringify-position "^4.0.0" vfile@^6.0.0, vfile@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz" - integrity sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw== + version "6.0.3" + resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== dependencies: "@types/unist" "^3.0.0" - unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" vscode-jsonrpc@8.2.0: @@ -9795,15 +9988,20 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^2.8.1: + version "2.8.1" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz" + integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw== + yocto-queue@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz" - integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + version "1.2.2" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz" + integrity sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ== -"zod@^3.25.76 || ^4", zod@^4.1.8: - version "4.1.11" - resolved "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz" - integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg== +"zod@^3.25.76 || ^4.1.8", zod@^4.1.8: + version "4.1.12" + resolved "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz" + integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ== zwitch@^2.0.0: version "2.0.4"