diff --git a/.editorconfig b/.editorconfig index 077ee2a..5a911ad 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,8 +1,8 @@ # ATC coding rules - https://github.com/atc-net/atc-coding-rules -# Version: 1.0.1 -# Updated: 03-06-2024 +# Version: 1.0.0 +# Updated: 01-03-2025 # Location: Root -# Distribution: DotNet8 +# Distribution: DotNet9 # Inspired by: https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options ########################################## @@ -492,6 +492,8 @@ dotnet_diagnostic.CA1867.severity = suggestion # Use char overload dotnet_diagnostic.CA1868.severity = suggestion # Unnecessary call to 'Contains(item)' dotnet_diagnostic.CA1869.severity = suggestion # Cache and reuse 'JsonSerializerOptions' instances dotnet_diagnostic.CA1870.severity = suggestion # Use a cached 'SearchValues' instance +dotnet_diagnostic.CA1871.severity = suggestion # Do not pass a nullable struct to 'ArgumentNullException.ThrowIfNull' +dotnet_diagnostic.CA1872.severity = suggestion # Prefer 'Convert.ToHexString' and 'Convert.ToHexStringLower' over call chains based on 'BitConverter.ToString' dotnet_diagnostic.CA2007.severity = suggestion # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA2007.md dotnet_diagnostic.CA2017.severity = error # Parameter count mismatch dotnet_diagnostic.CA2018.severity = error # The count argument to Buffer.BlockCopy should specify the number of bytes to copy @@ -505,6 +507,9 @@ dotnet_diagnostic.CA2255.severity = suggestion # The ModuleInitializer attr dotnet_diagnostic.CA2259.severity = error # Ensure ThreadStatic is only used with static fields dotnet_diagnostic.CA2260.severity = error # Implement generic math interfaces correctly dotnet_diagnostic.CA2261.severity = error # Do not use ConfigureAwaitOptions.SuppressThrowing with Task +dotnet_diagnostic.CA2262.severity = suggestion # Set 'MaxResponseHeadersLength' properly +dotnet_diagnostic.CA2263.severity = suggestion # Prefer generic overload when type is known +dotnet_diagnostic.CA2264.severity = error # Do not pass a non-nullable value to 'ArgumentNullException.ThrowIfNull' dotnet_diagnostic.IDE0005.severity = warning # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/IDE0005.md dotnet_diagnostic.IDE0010.severity = suggestion # Populate switch dotnet_diagnostic.IDE0028.severity = suggestion # Collection initialization can be simplified @@ -530,6 +535,7 @@ dotnet_diagnostic.CS4014.severity = error # https://github.com/atc-net/ # StyleCop # https://github.com/DotNetAnalyzers/StyleCopAnalyzers dotnet_diagnostic.SA1009.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1009.md +dotnet_diagnostic.SA1010.severity = none # False positive when using collection initializers dotnet_diagnostic.SA1101.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1101.md dotnet_diagnostic.SA1122.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1122.md dotnet_diagnostic.SA1133.severity = error # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/StyleCop/SA1133.md @@ -556,6 +562,7 @@ dotnet_diagnostic.S3358.severity = none # Extract this nested ternary dotnet_diagnostic.S6602.severity = none # "Find" method should be used instead of the "FirstOrDefault" dotnet_diagnostic.S6603.severity = none # The collection-specific "TrueForAll" method should be used instead of the "All" dotnet_diagnostic.S6605.severity = none # Collection-specific "Exists" method should be used instead of the "Any" +dotnet_diagnostic.S6964.severity = none # Value type property used as input in a controller action should be nullable, required or annotated with the JsonRequiredAttribute to avoid under-posting. ########################################## diff --git a/.github/chatmodes/GPT5-Beast.chatmode.md b/.github/chatmodes/GPT5-Beast.chatmode.md new file mode 100644 index 0000000..06f980e --- /dev/null +++ b/.github/chatmodes/GPT5-Beast.chatmode.md @@ -0,0 +1,121 @@ +--- +description: 'GPT 5 as a top-notch coding agent.' +model: GPT-5 (Preview) +title: 'GPT 5 Beast Mode (VS Code v1.102)' +--- + +You are an agent - please keep going until the user’s query is completely resolved, before ending your turn and yielding back to the user. + +Your thinking should be thorough and so it's fine if it's very long. However, avoid unnecessary repetition and verbosity. You should be concise, but thorough. + +You MUST iterate and keep going until the problem is solved. + +You have everything you need to resolve this problem. I want you to fully solve this autonomously before coming back to me. + +Only terminate your turn when you are sure that the problem is solved and all items have been checked off. Go through the problem step by step, and make sure to verify that your changes are correct. NEVER end your turn without having truly and completely solved the problem, and when you say you are going to make a tool call, make sure you ACTUALLY make the tool call, instead of ending your turn. + +THE PROBLEM CAN NOT BE SOLVED WITHOUT EXTENSIVE INTERNET RESEARCH. + +You must use the fetch_webpage tool to recursively gather all information from URL's provided to you by the user, as well as any links you find in the content of those pages. + +Your knowledge on everything is out of date because your training date is in the past. + +You CANNOT successfully complete this task without using Google to verify your understanding of third party packages and dependencies is up to date. You must use the fetch_webpage tool to search google for how to properly use libraries, packages, frameworks, dependencies, etc. every single time you install or implement one. It is not enough to just search, you must also read the content of the pages you find and recursively gather all relevant information by fetching additional links until you have all the information you need. + +Always tell the user what you are going to do before making a tool call with a single concise sentence. This will help them understand what you are doing and why. + +If the user request is "resume" or "continue" or "try again", check the previous conversation history to see what the next incomplete step in the todo list is. Continue from that step, and do not hand back control to the user until the entire todo list is complete and all items are checked off. Inform the user that you are continuing from the last incomplete step, and what that step is. + +Take your time and think through every step - remember to check your solution rigorously and watch out for boundary cases, especially with the changes you made. Use the sequential thinking tool if available. Your solution must be perfect. If not, continue working on it. At the end, you must test your code rigorously using the tools provided, and do it many times, to catch all edge cases. If it is not robust, iterate more and make it perfect. Failing to test your code sufficiently rigorously is the NUMBER ONE failure mode on these types of tasks; make sure you handle all edge cases, and run existing tests if they are provided. + +You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully. + +You MUST keep working until the problem is completely solved, and all items in the todo list are checked off. Do not end your turn until you have completed all steps in the todo list and verified that everything is working correctly. When you say "Next I will do X" or "Now I will do Y" or "I will do X", you MUST actually do X or Y instead of just saying that you will do it. + +You are a highly capable and autonomous agent, and you can definitely solve this problem without needing to ask the user for further input. + +# Workflow + +1. Fetch any URL's provided by the user using the `fetch_webpage` tool. +2. Understand the problem deeply. Carefully read the issue and think critically about what is required. Use sequential thinking to break down the problem into manageable parts. Consider the following: + - What is the expected behavior? + - What are the edge cases? + - What are the potential pitfalls? + - How does this fit into the larger context of the codebase? + - What are the dependencies and interactions with other parts of the code? +3. Investigate the codebase. Explore relevant files, search for key functions, and gather context. +4. Research the problem on the internet by reading relevant articles, documentation, and forums. +5. Develop a clear, step-by-step plan. Break down the fix into manageable, incremental steps. Display those steps in a simple todo list using standard markdown format. Make sure you wrap the todo list in triple backticks so that it is formatted correctly. +6. Implement the fix incrementally. Make small, testable code changes. +7. Debug as needed. Use debugging techniques to isolate and resolve issues. +8. Test frequently. Run tests after each change to verify correctness. +9. Iterate until the root cause is fixed and all tests pass. +10. Reflect and validate comprehensively. After tests pass, think about the original intent, write additional tests to ensure correctness, and remember there are hidden tests that must also pass before the solution is truly complete. + +Refer to the detailed sections below for more information on each step. + +## 1. Fetch Provided URLs +- If the user provides a URL, use the `functions.fetch_webpage` tool to retrieve the content of the provided URL. +- After fetching, review the content returned by the fetch tool. +- If you find any additional URLs or links that are relevant, use the `fetch_webpage` tool again to retrieve those links. +- Recursively gather all relevant information by fetching additional links until you have all the information you need. + +## 2. Deeply Understand the Problem +Carefully read the issue and think hard about a plan to solve it before coding. + +## 3. Codebase Investigation +- Explore relevant files and directories. +- Search for key functions, classes, or variables related to the issue. +- Read and understand relevant code snippets. +- Identify the root cause of the problem. +- Validate and update your understanding continuously as you gather more context. + +## 4. Internet Research +- Use the `fetch_webpage` tool to search google by fetching the URL `https://www.google.com/search?q=your+search+query`. +- After fetching, review the content returned by the fetch tool. +- If you find any additional URLs or links that are relevant, use the `fetch_webpage` tool again to retrieve those links. +- Recursively gather all relevant information by fetching additional links until you have all the information you need. + +## 5. Develop a Detailed Plan +- Outline a specific, simple, and verifiable sequence of steps to fix the problem. +- Create a todo list in markdown format to track your progress. +- Each time you complete a step, check it off using `[x]` syntax. +- Each time you check off a step, display the updated todo list to the user. +- Make sure that you ACTUALLY continue on to the next step after checking off a step instead of ending your turn and asking the user what they want to do next. + +## 6. Making Code Changes +- Before editing, always read the relevant file contents or section to ensure complete context. +- Always read 2000 lines of code at a time to ensure you have enough context. +- If a patch is not applied correctly, attempt to reapply it. +- Make small, testable, incremental changes that logically follow from your investigation and plan. + +## 7. Debugging +- Use the `get_errors` tool to identify and report any issues in the code. This tool replaces the previously used `#problems` tool. +- Make code changes only if you have high confidence they can solve the problem +- When debugging, try to determine the root cause rather than addressing symptoms +- Debug for as long as needed to identify the root cause and identify a fix +- Use print statements, logs, or temporary code to inspect program state, including descriptive statements or error messages to understand what's happening +- To test hypotheses, you can also add test statements or functions +- Revisit your assumptions if unexpected behavior occurs. + +# How to create a Todo List +Use the following format to create a todo list: +```markdown +- [ ] Step 1: Description of the first step +- [ ] Step 2: Description of the second step +- [ ] Step 3: Description of the third step +``` + +Do not ever use HTML tags or any other formatting for the todo list, as it will not be rendered correctly. Always use the markdown format shown above. + +# Communication Guidelines +Always communicate clearly and concisely in a casual, friendly yet professional tone. + + +"Let me fetch the URL you provided to gather more information." +"Ok, I've got all of the information I need on the LIFX API and I know how to use it." +"Now, I will search the codebase for the function that handles the LIFX API requests." +"I need to update several files here - stand by" +"OK! Now let's run the tests to make sure everything is working correctly." +"Help - I see we have some problems. Let's fix those up." + \ No newline at end of file diff --git a/.github/instructions/csharp.instructions.md b/.github/instructions/csharp.instructions.md new file mode 100644 index 0000000..66b151f --- /dev/null +++ b/.github/instructions/csharp.instructions.md @@ -0,0 +1,34 @@ +--- +applyTo: '**/*.{cs}' +--- + +C# Usage + +- Always use latest C# features: + - File-scoped namespaces (`namespace X;` syntax) + - Leverage primary constructors for simple classes + - Collection initializers/expressions + - Pattern matching with `is null` and `is not null` instead of `== null` and `!= null` + - Init-only properties (`init` accessor) for immutable objects when records aren't appropriate + - Global using directives for commonly used imports + - Default interface implementations when appropriate +- Enable and respect nullable reference types (`string?` vs `string`) +- Use records for immutable data structures +- Mark all types as sealed unless designed for inheritance +- Use `var` when the type is obvious from the right side of the assignment +- Use clear names instead of making comments +- Avoid using exceptions for control flow: + - When exceptions are thrown, always use meaningful exceptions following .NET conventions + - Use `UnreachableException` to signal unreachable code, that cannot be reached by tests +- Async/await best practices: + - Avoid `async void` except for event handlers + - Use `ConfigureAwait(false)` in library code + - Propagate cancellation tokens + - Use `Task.WhenAll` for parallel task execution +- Logging guidance: + - Log only meaningful events at appropriate severity levels + - Logging messages should not include a period + - Use structured source generated logging +- Code comments: + - Don't add comments unless the code is truly not expressing the intent + - Never remove TODO: comments \ No newline at end of file diff --git a/.github/workflows/post-integration.yml b/.github/workflows/post-integration.yml index c99fb26..b938d2f 100644 --- a/.github/workflows/post-integration.yml +++ b/.github/workflows/post-integration.yml @@ -30,10 +30,10 @@ jobs: with: setAllVars: true - - name: ⚙️ Setup dotnet 8.0.x - uses: actions/setup-dotnet@v4 + - name: ⚙️ Setup dotnet 9.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: ⚙️ Set up JDK 17 uses: actions/setup-java@v3 diff --git a/.github/workflows/pre-integration.yml b/.github/workflows/pre-integration.yml index 7a7009f..2bc89a9 100644 --- a/.github/workflows/pre-integration.yml +++ b/.github/workflows/pre-integration.yml @@ -19,10 +19,10 @@ jobs: with: fetch-depth: 0 - - name: ⚙️ Setup dotnet 8.0.x - uses: actions/setup-dotnet@v4 + - name: ⚙️ Setup dotnet 9.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: 🧹 Clean run: dotnet clean -c Release && dotnet nuget locals all --clear @@ -43,10 +43,10 @@ jobs: with: fetch-depth: 0 - - name: ⚙️ Setup dotnet 8.0.x - uses: actions/setup-dotnet@v4 + - name: ⚙️ Setup dotnet 9.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: 🔁 Restore packages run: dotnet restore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0822c83..493295e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,10 +27,10 @@ jobs: with: setAllVars: true - - name: ⚙️ Setup dotnet 8.0.x - uses: actions/setup-dotnet@v4 + - name: ⚙️ Setup dotnet 9.0.x + uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: 🧹 Clean run: dotnet clean -c Release && dotnet nuget locals all --clear diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..ec38615 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,31 @@ +{ + "servers": { + "fetch": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "mcp/fetch" + ] + }, + "sequential-thinking": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-sequential-thinking" + ] + }, + "context7": { + "command": "npx", + "args": [ + "-y", + "@upstash/context7-mcp@latest" + ] + }, + "microsoft.docs.mcp": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp" + }, + } +} \ No newline at end of file diff --git a/Atc.Test.sln b/Atc.Test.sln deleted file mode 100644 index 0c16179..0000000 --- a/Atc.Test.sln +++ /dev/null @@ -1,79 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 15.0.26124.0 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Atc.Test", "src\Atc.Test\Atc.Test.csproj", "{7E15F9B0-040B-454B-BD29-E0A0CBC3473B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Atc.Test.Tests", "test\Atc.Test.Tests\Atc.Test.Tests.csproj", "{FE44F21D-22E5-47ED-A551-74AF79E23C89}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".config", ".config", "{23CF9054-75B8-4E9E-A252-52C5C1455ECA}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - Directory.Build.props = Directory.Build.props - README.md = README.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{2E82F528-471D-4451-ADD9-C0AF97692C8D}" - ProjectSection(SolutionItems) = preProject - src\.editorconfig = src\.editorconfig - src\Directory.Build.props = src\Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{4B46C47D-DE74-4FA4-9A27-4CBCB354AE05}" - ProjectSection(SolutionItems) = preProject - test\.editorconfig = test\.editorconfig - test\Directory.Build.props = test\Directory.Build.props - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{014851A9-0672-4268-9856-181B0BB26F71}" - ProjectSection(SolutionItems) = preProject - README.md = README.md - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|x64 = Debug|x64 - Debug|x86 = Debug|x86 - Release|Any CPU = Release|Any CPU - Release|x64 = Release|x64 - Release|x86 = Release|x86 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Debug|x64.ActiveCfg = Debug|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Debug|x64.Build.0 = Debug|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Debug|x86.ActiveCfg = Debug|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Debug|x86.Build.0 = Debug|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Release|Any CPU.Build.0 = Release|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Release|x64.ActiveCfg = Release|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Release|x64.Build.0 = Release|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Release|x86.ActiveCfg = Release|Any CPU - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B}.Release|x86.Build.0 = Release|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Debug|x64.ActiveCfg = Debug|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Debug|x64.Build.0 = Debug|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Debug|x86.ActiveCfg = Debug|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Debug|x86.Build.0 = Debug|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Release|Any CPU.Build.0 = Release|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Release|x64.ActiveCfg = Release|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Release|x64.Build.0 = Release|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Release|x86.ActiveCfg = Release|Any CPU - {FE44F21D-22E5-47ED-A551-74AF79E23C89}.Release|x86.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {7E15F9B0-040B-454B-BD29-E0A0CBC3473B} = {2E82F528-471D-4451-ADD9-C0AF97692C8D} - {FE44F21D-22E5-47ED-A551-74AF79E23C89} = {4B46C47D-DE74-4FA4-9A27-4CBCB354AE05} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {5E657CBB-3A3C-4C41-B832-7ACE196175AD} - EndGlobalSection -EndGlobal diff --git a/Atc.Test.sln.DotSettings b/Atc.Test.sln.DotSettings deleted file mode 100644 index cd9e4f7..0000000 --- a/Atc.Test.sln.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - True \ No newline at end of file diff --git a/Atc.Test.slnx b/Atc.Test.slnx new file mode 100644 index 0000000..692a037 --- /dev/null +++ b/Atc.Test.slnx @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Directory.Build.props b/Directory.Build.props index 022b012..767ff34 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -42,10 +42,10 @@ - + - + \ No newline at end of file diff --git a/README.md b/README.md index eed8a85..1e6393f 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,270 @@ -[![NuGet Version](https://img.shields.io/nuget/v/Atc.Test.svg?logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/atc.test) +# Introduction -# ATC Test +![NuGet Version](https://img.shields.io/nuget/v/Atc.Test.svg?logo=nuget&style=for-the-badge) -Common tools for writing tests using XUnit, AutoFixture, NSubstitute and FluentAssertions. +`Atc.Test` is a .NET helper library that streamlines authoring tests with xUnit v3, AutoFixture, NSubstitute, and FluentAssertions. It provides rich data attributes, automatic specimen customization, and ergonomic frozen value reuse to reduce ceremony and improve test readability. -> **Version Notes:** -> If you are using the `MemberAutoNSubstituteData` attribute, ensure that both `xunit` and `xunit.extensibility.core` packages are set to version **2.9.0**. -> This is because the latest versions of xUnit are currently incompatible with `MemberAutoNSubstituteData` due to an unresolved issue. Although [xunit/xunit#3031](https://github.com/xunit/xunit/issues/3031) has been closed, the problem remains unresolved. -> Upgrading beyond version 2.9.0 may result in test failures or unexpected behavior. +## Why Atc.Test +> You can “just wire everything manually” with plain xUnit and hand‑rolled mocks—so why use this instead? -## Test Attributes +| Problem Without | What You Gain With `Atc.Test` | Why It Matters Over Time | +|-----------------|-------------------------------|--------------------------| +| Repeating constructor/mocker boilerplate in every test | Parameter-only intent: you list just what the test cares about | Lower cognitive load; faster review – noise removed | +| Fragile refactors (add a ctor param ⇒ touch many files) | Fixture-driven auto‑supply of new dependencies | Constructor churn becomes O(1) instead of O(N tests) | +| Divergent ad‑hoc mock styles (naming, setup order) | Central factory + consistent frozen reuse semantics | Suite stays uniform; easier large-scale edits / audits | +| Accidental duplicate substitutes for logically single collaborator | `[Frozen]` exact-type reuse + early supplied promotion (member data) | Prevents subtle mismatch bugs & expectation gaps | +| Manual re-creation of “shared conventions” (recursion handling, generators) | One-time customization via `[AutoRegister]` | New test inherits standards automatically | +| AI-generated setup drifts over time | Declarative attributes act as a stable policy layer | Reduces maintenance & future prompt dependency | -| Name | Description | -|-|-| -| `AutoNSubstituteData` | Provides auto-generated data specimens generated by AutoFixture and NSubstitute as an extension to XUnit's [Theory] attribute.| -| `InlineAutoNSubstituteData` | Provides a data source for a data theory, with the data coming from inline values combined with auto-generated data specimens generated by AutoFixture and NSubstitute.| -| `MemberAutoNSubstituteData` | Provides a data source for a data theory, with the data coming from one of the following sources and combined with auto-generated data specimens generated by AutoFixture and NSubstitute.| +### When It Delivers the Most Value -> **Note:** -> NSubstitute is used when the type being created is abstract, or when the `[Substitute]` is applied. +* Mid/large test suites (hundreds+ of theory cases). +* Domain services with evolving constructor graphs / dependencies. +* Teams that value refactor safety and consistent test style. +* Situations where only a few parameters per test truly matter. -## Test Helpers +### When Bare xUnit (+ manual mocks) May Be Enough -| Name | Description | -|-|-| -| `EquivalencyAssertionOptionsExtensions` | Extensions for FluentAssertions to compare dates with a precision when using `.BeEquivalentTo()`.| -| `FixtureFactory` | Static factory for creating AutoFixture `Fixture` instances.| -| `ObjectExtensions` | Extensions calling protected members on an object.| -| `SubstituteExtensions` | Extensions for NSubstitutes to wait for calls and get arguments of a received call.| -| `TaskExtensions` | Extensions for Tasks to add timeouts when awaiting. | +* Very small or short‑lived codebases. +* Highly bespoke object graphs where you override almost every generated value anyway. +* Educational contexts emphasizing explicit wiring for learning. -## Extensibility +### Summary -The default `Fixture` returned by the `FixtrueFactory.Create()` method is used for all the `Attributes` mentioned above. +`Atc.Test` trades a tiny amount of initial abstraction for compounding savings in refactors, readability, and consistency. AI can quickly generate boilerplate; this library’s value is eliminating the need for that boilerplate in the first place—and giving you a single, policy‑driven locus for customization and reuse. -To add customizations to this, you can add the `AutoRegisterAttribute` to any custom `ICustomization` or `ISpecimenBuilder` to have it automatically added to the Fixture. +## Table of Content -See [`CancellationTokenGenerator`](src/Atc.Test/Customizations/Generators/CancellationTokenGenerator.cs) for an example on how to do this. +* [Introduction](#introduction) + * [Why Atc.Test](#why-atctest) +* [Table of Content](#table-of-content) + * [Features](#features) + * [Getting Started](#getting-started) + * [Install Package](#install-package) + * [Why xUnit Must Be Referenced Directly](#why-xunit-must-be-referenced-directly) + * [First Test Examples](#first-test-examples) + * [Advanced Usage](#advanced-usage) + * [Frozen Reuse Scenarios](#frozen-reuse-scenarios) + * [Auto Registration of Customizations](#auto-registration-of-customizations) + * [Helper Extensions](#helper-extensions) + * [Requirements](#requirements) + * [How to Contribute](#how-to-contribute) -## How to contribute +## Features -[Contribution Guidelines](https://atc-net.github.io/introduction/about-atc#how-to-contribute) +* Data attributes integrating AutoFixture + NSubstitute: `AutoNSubstituteData`, `InlineAutoNSubstituteData`, `MemberAutoNSubstituteData`, `ClassAutoNSubstituteData`. +* Automatic interface/abstract substitution via NSubstitute. +* Exact-type frozen promotion for member data (reuse supplied instance across later `[Frozen]` parameters). +* Deterministic fixture configuration with opt‑in auto-registration of custom `ICustomization` / `ISpecimenBuilder` via `[AutoRegister]`. +* Convenience extensions: equivalency options, substitute inspection helpers, task timeout helpers, object protected member access. +* Multi-targeted (netstandard2.1, net8.0, net9.0) for broad compatibility. +* Clear separation of concerns: you own the xUnit runner/version. +## Getting Started + +### Install Package + +Add `Atc.Test` to your test project along with explicit references to xUnit and the test SDK: + +```xml + + + net9.0 + Exe + true + + + + + + + +``` + +### Why xUnit Must Be Referenced Directly + +`Atc.Test` depends on `xunit.v3.extensibility.core` (the extensibility surface) but intentionally does **not** bring in the `xunit.v3` meta-package: + +* Avoid NU1701 warnings from runner assets not targeting `netstandard2.1`. +* Let you pin or float the xUnit version independently. +* Keep framework + runner decisions in your test project for predictable upgrades. +* Preserve the library’s focus: providing attributes/utilities instead of prescribing test infrastructure. + +If you want a different xUnit patch/minor version, change the `` line—no changes to `Atc.Test` required. + +#### xUnit v3 Only (Incompatible With v2) + +`Atc.Test` relies on xUnit v3 extensibility APIs: + +* Async data attribute signature: `ValueTask> GetData(...)`. +* `ITheoryDataRow` & metadata (Label, Explicit, Timeout) preservation. +* `DisposalTracker` parameter passed to data attributes. + +These do not exist in xUnit v2. Attempting to use a v2 framework or runner will result in discovery failures or compile errors. + +| Scenario | Outcome | +|----------|---------| +| Replace `xunit.v3` with `xunit` (v2) | Build errors: missing v3 types & method signatures | +| Run with legacy v2 runner | Test discovery fails (no v3 discovery support) | +| Mix projects: some v2, some using `Atc.Test` | Allowed, but they must not share v3-based base test classes | +| Remove explicit `xunit.v3` reference | Build error / missing types (transitive reference intentionally absent) | + +Optional guard rails (not included by default): + +```xml + + + + +``` + +“Why no v2 support?” the answer is simply that the library embraces the cleaner v3 data extensibility model; back-porting would require a parallel code path and reduce clarity. + +### First Test Examples + +```csharp +public class CalculatorTests +{ + [Theory] + [AutoNSubstituteData] + public void AutoData_Generates_Specimens(int a, int b, Calculator sut) + => sut.Add(a, b).Should().Be(a + b); + + [Theory] + [InlineAutoNSubstituteData(2, 3)] + public void InlineAutoData_Mixes_Inline_And_Auto(int a, int b, Calculator sut) + => sut.Add(a, b).Should().Be(5); + + public static IEnumerable MemberSource() + { + yield return new object?[] { 1, 2 }; + yield return new object?[] { 10, 20 }; + } + + [Theory] + [MemberAutoNSubstituteData(nameof(MemberSource))] + public void MemberAutoData_Augments_Member_Data(int a, int b, Calculator sut) + => sut.Add(a, b).Should().Be(a + b); +} +``` + +All remaining parameters (after inline/member supplied ones) are created via an AutoFixture `IFixture` that substitutes interfaces/abstract classes using NSubstitute. + +> **Note** +> NSubstitute is used automatically when the requested type is an interface or abstract class. + +## Advanced Usage + +### Frozen Reuse Scenarios + +When you decorate a parameter with `[Frozen]`, its resolved instance is reused for other specimens requiring that exact type. `MemberAutoNSubstituteData` adds **exact-type promotion**: reusing an earlier supplied value for a later `[Frozen]` parameter when that later slot was not part of the member row. + +| Scenario | Attribute | Behavior | +|----------|-----------|----------| +| Positional frozen reuse | `ClassAutoNSubstituteData` & `MemberAutoNSubstituteData` | If a value is supplied at the same index as a `[Frozen]` parameter, it is frozen and reused. | +| Exact-type promotion (member data only) | `MemberAutoNSubstituteData` | Later `[Frozen] T` without a supplied value reuses an earlier supplied parameter whose declared type is exactly `T`. | +| No interface/base promotion | Both | Only exact parameter type matches are reused (no interface or base class widening). | + +#### Example: Positional Reuse + +```csharp +[Theory] +[InlineAutoNSubstituteData(42)] +public void Positional_Frozen_Reuses_Inline_Value( + [Frozen] int number, + SomeConsumer consumer) +{ + consumer.NumberDependency.Should().Be(number); +} +``` + +#### Example: Exact-Type Promotion (Member Data) + +```csharp +public static IEnumerable ServiceRow() +{ + yield return new object?[] { Substitute.For() }; // supplies parameter 0 only +} + +[Theory] +[MemberAutoNSubstituteData(nameof(ServiceRow))] +public void Promotion_Reuses_Earlier_Same_Type( + IMyService supplied, + [Frozen] IMyService frozenLater, + NeedsService consumer) +{ + frozenLater.Should().BeSameAs(supplied); + consumer.Service.Should().BeSameAs(supplied); +} +``` + +#### Example: Non-Promotion Across Different Interfaces + +```csharp +public interface IFoo {} +public interface IBar {} +public class DualImpl : IFoo, IBar {} + +public static IEnumerable DualRow() +{ + yield return new object?[] { new DualImpl() }; // supplies IFoo parameter only +} + +[Theory] +[MemberAutoNSubstituteData(nameof(DualRow))] +public void Different_Interface_Not_Promoted( + IFoo foo, + [Frozen] IBar bar, + UsesBar consumer) +{ + bar.Should().NotBeSameAs(foo); // separate instance + consumer.Bar.Should().BeSameAs(bar); // consumer wired to frozen IBar +} +``` + +Design Rationale: + +* Class data is usually fully positional—implicit promotion might hide mistakes. +* Member data often supplies only a prefix—promotion reduces duplication while staying explicit. +* Exact-type restriction avoids cross-interface bleed (e.g., dual implementations hijacking unrelated abstractions). + +### Auto Registration of Customizations + +Any `ICustomization` or `ISpecimenBuilder` decorated with `[AutoRegister]` is added automatically to the fixture created by `FixtureFactory.Create()`. + +Example: + +```csharp +[AutoRegister] +public class GuidCustomization : ICustomization +{ + public void Customize(IFixture fixture) => fixture.Register(() => Guid.NewGuid()); +} +``` + +### Helper Extensions + +| Helper | Purpose | +|--------|---------| +| `EquivalencyAssertionOptionsExtensions` | Adds convenience config (e.g., date precision) to FluentAssertions equivalency. | +| `SubstituteExtensions` | Inspect substitutes, wait for calls, retrieve arguments. | +| `TaskExtensions` | Await with timeouts. | +| `ObjectExtensions` | Access protected members via reflection helpers. | +| `FixtureFactory` | Central factory returning a consistently customized `IFixture`. | + +## Requirements + +| Aspect | Value | +|--------|-------| +| Target Frameworks | netstandard2.1, net8.0, net9.0 | +| Test Framework | xUnit v3 (must be referenced directly) | +| Mocking | NSubstitute (transitively used for interfaces/abstract classes) | +| Assertions | FluentAssertions (recommended) | + +## How to Contribute + +[Contribution Guidelines](https://atc-net.github.io/introduction/about-atc#how-to-contribute) [Coding Guidelines](https://atc-net.github.io/introduction/about-atc#coding-guidelines) + diff --git a/atc-coding-rules-updater.json b/atc-coding-rules-updater.json index 65e3cb3..6e57f36 100644 --- a/atc-coding-rules-updater.json +++ b/atc-coding-rules-updater.json @@ -1,5 +1,5 @@ { - "projectTarget": "DotNet8", + "projectTarget": "DotNet9", "useLatestMinorNugetVersion": true, "useTemporarySuppressions": false, "temporarySuppressionAsExcel": false, diff --git a/src/.editorconfig b/src/.editorconfig index 970bed4..65d0085 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -1,8 +1,8 @@ # ATC coding rules - https://github.com/atc-net/atc-coding-rules # Version: 1.0.0 -# Updated: 25-09-2023 +# Updated: 03-06-2024 # Location: src -# Distribution: DotNet8 +# Distribution: DotNet9 # Inspired by: https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options ########################################## diff --git a/src/Atc.Test/Atc.Test.csproj b/src/Atc.Test/Atc.Test.csproj index a235fa0..cff8962 100644 --- a/src/Atc.Test/Atc.Test.csproj +++ b/src/Atc.Test/Atc.Test.csproj @@ -1,7 +1,7 @@ - + - netstandard2.1;net8.0 + netstandard2.1;net8.0;net9.0 Atc.Test xunit;NSubstitute;AutoFixture;FluentAssertions Common tools for writing tests using XUnit, AutoFixture, NSubstitute and FluentAssertions. @@ -15,14 +15,13 @@ - + - - - + + \ No newline at end of file diff --git a/src/Atc.Test/AutoNSubstituteDataAttribute.cs b/src/Atc.Test/AutoNSubstituteDataAttribute.cs index dcdae00..5a16d3d 100644 --- a/src/Atc.Test/AutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/AutoNSubstituteDataAttribute.cs @@ -14,7 +14,7 @@ public sealed class AutoNSubstituteDataAttribute : AutoDataAttribute /// Initializes a new instance of the class. /// public AutoNSubstituteDataAttribute() - : base(FixtureFactory.Create) + : base(() => global::Atc.Test.FixtureFactory.Create()) { } } \ No newline at end of file diff --git a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs index 8122a7a..724722a 100644 --- a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs @@ -11,42 +11,78 @@ public sealed class ClassAutoNSubstituteDataAttribute : ClassDataAttribute public ClassAutoNSubstituteDataAttribute(Type @class) : base(@class) { + Class = @class; } - public override IEnumerable GetData(MethodInfo testMethod) + /// + /// Gets the class type that provides the enumerable data. + /// + public new Type Class { get; } + + /// + /// Combines class-provided data rows with AutoFixture generated specimens, honoring any parameters. + /// + /// The target test method. + /// xUnit disposal tracker (not directly used). + /// Augmented data rows with original supplied values followed by generated specimens. + /// + /// For each row: + /// 1. Create a fresh fixture. + /// 2. Inject supplied values that map to [Frozen] parameters (positional only). + /// 3. Generate remaining parameters via . + /// 4. Preserve original row metadata. + /// Unlike member attribute handling, no promotion step is required because class data enumeration typically aligns positions directly. + /// + public override async ValueTask> GetData( + MethodInfo testMethod, + DisposalTracker disposalTracker) { + var baseRows = await base.GetData(testMethod, disposalTracker).ConfigureAwait(false); var parameters = testMethod.GetParameters(); - var frozenValues = parameters - .Select((p, i) => (Index: i, Parameter: p, p.ParameterType)) - .Where(x => x.Parameter.GetCustomAttribute() != null) - .ToArray(); - var injectMethod = typeof(FixtureRegistrar) - .GetMethod( - nameof(FixtureRegistrar.Inject), - BindingFlags.Public | BindingFlags.Static); - var data = base.GetData(testMethod); - foreach (var values in data) + // Build injector with promotion disabled (class data relies purely on positional alignment). + var frozenInjector = FrozenParameterInjector.Build(parameters, enableExactTypePromotion: false); + + var augmented = new List(baseRows.Count); + foreach (var row in baseRows) { + var originalData = row.GetData(); var fixture = FixtureFactory.Create(); - foreach (var frozenValue in frozenValues) - { - if (values.Length > frozenValue.Index) - { - injectMethod? - .MakeGenericMethod(frozenValue.ParameterType) - .Invoke(null, [fixture, values[frozenValue.Index]]); - } - } - yield return values + // Inject frozen values if present in source data (positional only). + frozenInjector(originalData, fixture); + + var extendedData = originalData .Concat(parameters - .Skip(values.Length) + .Skip(originalData.Length) .Select(p => GetSpecimen(fixture, p))) .ToArray(); + + // Preserve metadata from original row where possible. + augmented.Add(new TheoryDataRow(extendedData) + { + Explicit = row.Explicit, + Label = row.Label, + Skip = row.Skip, + TestDisplayName = row.TestDisplayName, + Timeout = row.Timeout, + Traits = row.Traits ?? new Dictionary>(StringComparer.OrdinalIgnoreCase), + }); } + + return augmented; } + /// + /// Resolves a specimen for the given parameter applying parameter-level customizations prior to resolution. + /// + /// The per-row fixture instance. + /// The parameter to resolve. + /// The resolved specimen instance. + /// + /// Customizations are ordered so non-frozen behaviors apply before any potential freezing to ensure + /// a final form is captured when freezing occurs. + /// private static object GetSpecimen( IFixture fixture, ParameterInfo parameter) diff --git a/src/Atc.Test/FrozenParameterInjector.cs b/src/Atc.Test/FrozenParameterInjector.cs new file mode 100644 index 0000000..56608ba --- /dev/null +++ b/src/Atc.Test/FrozenParameterInjector.cs @@ -0,0 +1,96 @@ +namespace Atc.Test; + +internal static class FrozenParameterInjector +{ + private static readonly MethodInfo? InjectMethod = typeof(FixtureRegistrar).GetMethod( + nameof(FixtureRegistrar.Inject), + BindingFlags.Public | BindingFlags.Static); + + /// + /// Builds an injector delegate that performs positional frozen injections and (optionally) exact-type promotions. + /// + /// Ordered method parameters. + /// When true, a later frozen parameter whose index exceeds supplied row length + /// will re-use an earlier supplied value only if the earlier parameter type is exactly the same (no interface/base widening). + /// An injector delegate accepting (suppliedRowValues, fixture) that performs injections/promotions. + internal static Action Build( + ParameterInfo[] parameters, + bool enableExactTypePromotion) + { + var frozenParameters = parameters + .Select((p, i) => new FrozenDescriptor(i, p, p.GetCustomAttribute() != null)) + .Where(x => x.IsFrozen) + .ToArray(); + + if (frozenParameters.Length == 0) + { + return static (_, _) => { }; + } + + // Pre-compute mapping from type to earliest supplied parameter index for fast exact promotion lookup. + var earliestIndexByType = parameters + .Select((p, i) => (p.ParameterType, Index: i)) + .GroupBy(x => x.ParameterType) + .ToDictionary(g => g.Key, g => g.Min(x => x.Index)); + + return (suppliedData, fixture) => + { + // Phase 1: direct positional injection where row already supplies the frozen parameter value. + foreach (var frozen in frozenParameters) + { + if (suppliedData.Length > frozen.Index) + { + InjectGeneric(fixture, frozen.Parameter.ParameterType, suppliedData[frozen.Index]); + } + } + + if (!enableExactTypePromotion) + { + return; // Class data does not promote. + } + + // Phase 2: exact-type promotion only (no interface/base assignability) to avoid cross-interface bleed. + foreach (var frozen in frozenParameters) + { + if (suppliedData.Length > frozen.Index) + { + continue; // Already handled by direct positional reuse. + } + + if (!earliestIndexByType.TryGetValue(frozen.Parameter.ParameterType, out var earliestIndex)) + { + continue; + } + + if (earliestIndex >= suppliedData.Length) + { + continue; // Earliest instance not actually supplied in this row. + } + + // Reuse supplied instance at earliest index for this exact type. + var instance = suppliedData[earliestIndex]; + InjectGeneric(fixture, frozen.Parameter.ParameterType, instance); + } + }; + } + + private static void InjectGeneric( + IFixture fixture, + Type type, + object? instance) + => InjectMethod? + .MakeGenericMethod(type) + .Invoke(null, [fixture, instance]); + + private readonly struct FrozenDescriptor( + int index, + ParameterInfo parameter, + bool isFrozen) + { + public int Index { get; } = index; + + public ParameterInfo Parameter { get; } = parameter; + + public bool IsFrozen { get; } = isFrozen; + } +} diff --git a/src/Atc.Test/GlobalUsings.cs b/src/Atc.Test/GlobalUsings.cs index cd712bc..1d2a409 100644 --- a/src/Atc.Test/GlobalUsings.cs +++ b/src/Atc.Test/GlobalUsings.cs @@ -6,21 +6,17 @@ global using System.Text.Json; global using System.Text.RegularExpressions; global using System.Xml; - global using Atc.Test.Customizations; - global using AutoFixture; global using AutoFixture.AutoNSubstitute; global using AutoFixture.Kernel; -global using AutoFixture.Xunit2; - +global using AutoFixture.Xunit3; global using FluentAssertions; global using FluentAssertions.Equivalency; global using FluentAssertions.Primitives; - global using NSubstitute; global using NSubstitute.Core; global using NSubstitute.ReceivedExtensions; - global using Xunit; global using Xunit.Sdk; +global using Xunit.v3; \ No newline at end of file diff --git a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs index 8047494..f765635 100644 --- a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs @@ -1,49 +1,106 @@ namespace Atc.Test; /// -/// Provides a data source for a data theory, with the data coming from -/// one of the following sources and combined with auto-generated data -/// specimens generated by AutoFixture and NSubstitute. -/// -/// A static property -/// A static field -/// A static method (with parameters) -/// -/// The member must return something compatible with -/// IEnumerable<object[]> with the test data. +/// Data attribute combining semantics with AutoFixture + NSubstitute specimen generation. /// +/// +/// 1. The referenced member (field / property / method) must return a type assignable to IEnumerable<object?[]>.
+/// 2. Supplied row values are appended with generated specimens for any remaining test method parameters.
+/// 3. Parameters decorated with participate in a two-phase reuse model: +/// +/// Direct positional reuse: If the row already supplies a value at the frozen parameter's index, that instance is injected (frozen) into the fixture. +/// Promotion: If the frozen parameter appears later (no supplied value at its index yet) an earlier supplied argument whose runtime type is assignable to the frozen parameter type is promoted and injected. +/// +/// This mirrors the behavior of ClassAutoNSubstituteDataAttribute while extending it with the promotion scenario common in member data ordering. +///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -[DataDiscoverer("Xunit.Sdk.MemberDataDiscoverer", "xunit.core")] public sealed class MemberAutoNSubstituteDataAttribute : MemberDataAttributeBase { public MemberAutoNSubstituteDataAttribute(string memberName, params object[] parameters) : base(memberName, parameters) { + MemberName = memberName; + Parameters = parameters; } - protected override object[] ConvertDataItem(MethodInfo testMethod, object item) + /// + /// Gets the member name passed to the constructor. + /// + public new string MemberName { get; } + + /// + /// Gets the parameter values passed to the constructor (for method members). + /// + public object[] Parameters { get; } + + /// + /// Produces the final theory data rows by merging member-supplied data with AutoFixture generated specimens. + /// + /// The target test method whose parameters drive specimen generation. + /// xUnit disposal tracker (unused directly here but required by override). + /// Augmented data rows containing original values plus generated specimens. + /// + /// Processing for each source row: + /// 1. Create an isolated fixture. + /// 2. Apply frozen injection logic (positional + promotion) before resolving additional parameters. + /// 3. Generate remaining parameters using . + /// 4. Preserve original row metadata (label, skip, etc.). + /// + public override async ValueTask> GetData( + MethodInfo testMethod, + DisposalTracker disposalTracker) { - if (item is not object[] values) + // Retrieve the original member data rows (without any AutoFixture augmentation yet). + var baseRows = await base.GetData(testMethod, disposalTracker).ConfigureAwait(false); + var parameters = testMethod.GetParameters(); + var augmented = new List(baseRows.Count); + + // Pre-compute an injector tailored to the frozen parameters of this method (with exact-type promotion enabled). + var frozenInjector = FrozenParameterInjector.Build(parameters, enableExactTypePromotion: true); + + foreach (var row in baseRows) { - throw new ArgumentException( - $"Property {MemberName} on {MemberType.Name} yielded an item that is not an object[]", - nameof(item)); - } + var data = row.GetData(); // The raw supplied data (could be shorter than parameter list) + var fixture = FixtureFactory.Create(); // Fresh fixture per row for isolation + + // Apply frozen injections (positional + promotions) before generating remaining specimens. + frozenInjector(data, fixture); - var fixture = FixtureFactory.Create(); + var extendedData = data + .Concat(parameters + .Skip(data.Length) + .Select(p => GetSpecimen(fixture, p))) + .ToArray(); + + augmented.Add(new TheoryDataRow(extendedData) + { + Explicit = row.Explicit, + Label = row.Label, + Skip = row.Skip, + TestDisplayName = row.TestDisplayName, + Timeout = row.Timeout, + Traits = row.Traits ?? new Dictionary>(StringComparer.OrdinalIgnoreCase), + }); + } - return values - .Concat(testMethod - .GetParameters() - .Skip(values.Length) - .Select(p => GetSpecimen(fixture, p))) - .ToArray(); + return augmented; } + /// + /// Resolves a specimen for a single parameter after applying any parameter-level customizations. + /// + /// The fixture instance configured for the current data row. + /// The reflective description of the theory method parameter. + /// The resolved object instance to be injected into the theory invocation. + /// + /// Customizations are applied in a deterministic order with non-frozen customizations first, ensuring + /// that observes the final constructed form when freezing an instance. + /// private static object GetSpecimen( IFixture fixture, ParameterInfo parameter) { + // Gather parameter-level customization sources (e.g. [Frozen], [Greedy], etc.) var attributes = parameter .GetCustomAttributes() .OfType() @@ -51,11 +108,13 @@ private static object GetSpecimen( foreach (var attribute in attributes) { + // Each customization mutates the fixture before resolving the specimen. attribute .GetCustomization(parameter) .Customize(fixture); } + // Resolve the final specimen through AutoFixture's pipeline honoring prior customizations. return new SpecimenContext(fixture) .Resolve(parameter); } diff --git a/test/.editorconfig b/test/.editorconfig index 2e29b97..3504194 100644 --- a/test/.editorconfig +++ b/test/.editorconfig @@ -1,8 +1,8 @@ # ATC coding rules - https://github.com/atc-net/atc-coding-rules # Version: 1.0.0 -# Updated: 25-09-2023 +# Updated: 09-01-2025 # Location: test -# Distribution: DotNet8 +# Distribution: DotNet9 # Inspired by: https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options ########################################## @@ -24,6 +24,7 @@ dotnet_diagnostic.MA0004.severity = none # https://github.com/atc-net dotnet_diagnostic.MA0016.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/Meziantou/MA0016.md dotnet_diagnostic.MA0051.severity = none # Method Length + # Microsoft - Code Analysis # https://docs.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ dotnet_diagnostic.CA1068.severity = none # https://github.com/atc-net/atc-coding-rules/blob/main/documentation/CodeAnalyzersRules/MicrosoftCodeAnalysis/CA1068.md diff --git a/test/Atc.Test.Tests/Atc.Test.Tests.csproj b/test/Atc.Test.Tests/Atc.Test.Tests.csproj index 45ceaf4..f779fb9 100644 --- a/test/Atc.Test.Tests/Atc.Test.Tests.csproj +++ b/test/Atc.Test.Tests/Atc.Test.Tests.csproj @@ -1,22 +1,20 @@ - + - net8.0 + Exe + net9.0 false + + true + true + + true - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + diff --git a/test/Atc.Test.Tests/ClassAutoNSubstituteDataAttributeTests.cs b/test/Atc.Test.Tests/ClassAutoNSubstituteDataAttributeTests.cs index e7104a2..29d2ece 100644 --- a/test/Atc.Test.Tests/ClassAutoNSubstituteDataAttributeTests.cs +++ b/test/Atc.Test.Tests/ClassAutoNSubstituteDataAttributeTests.cs @@ -6,9 +6,9 @@ public class TestData : TheoryData { public TestData() { - AddRow(SampleEnum.One); - AddRow(SampleEnum.Two); - AddRow(SampleEnum.Three); + Add(SampleEnum.One); + Add(SampleEnum.Two); + Add(SampleEnum.Three); } } diff --git a/test/Atc.Test.Tests/GlobalUsings.cs b/test/Atc.Test.Tests/GlobalUsings.cs index 8203116..5c195ea 100644 --- a/test/Atc.Test.Tests/GlobalUsings.cs +++ b/test/Atc.Test.Tests/GlobalUsings.cs @@ -1 +1,2 @@ -global using Atc.Test.Tests.SampleTypes; \ No newline at end of file +global using Atc.Test.Tests.SampleTypes; +global using AutoFixture.Xunit3; \ No newline at end of file diff --git a/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs b/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs index 1417be9..700b0d6 100644 --- a/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs +++ b/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs @@ -6,7 +6,7 @@ public sealed class MemberAutoNSubstituteDataAttributeTests [ [SampleEnum.One], [SampleEnum.Two], - [SampleEnum.Three] + [SampleEnum.Three], ]; [Theory] @@ -50,4 +50,115 @@ public void MemberAutoNSubstituteData_Should_Call_For_Each_MemberData_Values( dependantType.Should().NotBeNull(); dependantType.Dependency.Should().Be(interfaceType); } + + [Theory] + [MemberAutoNSubstituteData(nameof(DataFrozenProvided))] + public void MemberData_SuppliesValueForFrozenParameter_ReusedForDependants( + [Frozen] ISampleInterface frozen, + SampleDependantClass dependant) + { + frozen.Should().NotBeNull(); + dependant.Should().NotBeNull(); + dependant.Dependency.Should().BeSameAs(frozen); + } + + [Theory] + [MemberAutoNSubstituteData(nameof(DataFrozenProvided))] + public void MemberData_SuppliesValueEarlier_ReusedByLaterFrozenParameter( + ISampleInterface provided, + [Frozen] ISampleInterface frozen, + SampleDependantClass dependant) + { + provided.Should().NotBeNull(); + frozen.Should().NotBeNull(); + dependant.Should().NotBeNull(); + frozen.Should().BeSameAs(provided); + dependant.Dependency.Should().BeSameAs(provided); + } + + public static IEnumerable DataFrozenProvided() + { + yield return [Substitute.For()]; + } + + // Negative promotion scenario + // We provide an earlier argument typed as IFirstInterface whose concrete instance also + // implements ISecondInterface. The later parameter is [Frozen] ISecondInterface. Desired + // invariant: the framework should NOT promote the earlier IFirstInterface value to satisfy + // the frozen ISecondInterface; a distinct instance should be generated/injected instead. + // Current promotion logic (Type.IsInstanceOfType) is broad and WILL incorrectly reuse it. + // This test therefore initially fails (red) and will pass after tightening promotion logic + // in the upcoming refactor (shared frozen helper with exact-type matching / index mapping). + [Theory] + [MemberAutoNSubstituteData(nameof(DualInterfaceProvidedData))] + public void MemberData_DifferentInterface_NotPromoted( + IFirstInterface first, + [Frozen] ISecondInterface second, + SecondInterfaceDependant dependant) + { + first.Should().NotBeNull(); + second.Should().NotBeNull(); + dependant.Should().NotBeNull(); + + // Guard: dependant wired to the frozen instance. + dependant.Dependency.Should().BeSameAs(second); + + // Specification: these should NOT be the same instance + second.Should().NotBeSameAs(first); + } + + public static IEnumerable DualInterfaceProvidedData() + { + yield return [new DualInterfaceImpl { Name = "n", Description = "d" }]; + } + + [Theory] + [AutoNSubstituteData] + public void Multiple_Frozen_Same_Type_Should_Reference_Same_Instance( + [Frozen] ISampleInterface first, + [Frozen] ISampleInterface second, + SampleDependantClass dependant) + { + first.Should().NotBeNull(); + second.Should().NotBeNull(); + first.Should().BeSameAs(second); + dependant.Dependency.Should().BeSameAs(first); + } + + [Theory] + [MemberAutoNSubstituteData(nameof(DataSingleProvidedInterface))] + public void Earlier_Single_Supplied_Value_Reused_For_Multiple_Later_Frozens( + ISampleInterface supplied, + [Frozen] ISampleInterface frozen1, + [Frozen] ISampleInterface frozen2, + SampleDependantClass dependant) + { + supplied.Should().NotBeNull(); + frozen1.Should().BeSameAs(supplied); + frozen2.Should().BeSameAs(supplied); + dependant.Dependency.Should().BeSameAs(supplied); + } + + public static IEnumerable DataSingleProvidedInterface() + { + yield return [Substitute.For()]; + } + + [Theory] + [MemberAutoNSubstituteData(nameof(DataMultipleSameTypeProvided))] + public void Promotion_Uses_Earliest_Supplied_Instance( + ISampleInterface firstProvided, + ISampleInterface secondProvided, + [Frozen] ISampleInterface frozen, + SampleDependantClass dependant) + { + firstProvided.Should().NotBeSameAs(secondProvided); // separate substitutes + frozen.Should().BeSameAs(firstProvided); // earliest should win + dependant.Dependency.Should().BeSameAs(firstProvided); + } + + public static IEnumerable DataMultipleSameTypeProvided() + { + yield return [Substitute.For(), Substitute.For()]; + } } \ No newline at end of file diff --git a/test/Atc.Test.Tests/SampleTypes/DualInterfaceImpl.cs b/test/Atc.Test.Tests/SampleTypes/DualInterfaceImpl.cs new file mode 100644 index 0000000..e58a5d1 --- /dev/null +++ b/test/Atc.Test.Tests/SampleTypes/DualInterfaceImpl.cs @@ -0,0 +1,11 @@ +namespace Atc.Test.Tests.SampleTypes; + +// Implements both interfaces so underlying instance satisfies each, allowing us to +// detect overly-broad promotion logic that reuses an earlier IFirstInterface value +// for a later [Frozen] ISecondInterface parameter. +public class DualInterfaceImpl : IFirstInterface, ISecondInterface +{ + public string? Name { get; set; } + + public string? Description { get; set; } +} diff --git a/test/Atc.Test.Tests/SampleTypes/IFirstInterface.cs b/test/Atc.Test.Tests/SampleTypes/IFirstInterface.cs new file mode 100644 index 0000000..e82997b --- /dev/null +++ b/test/Atc.Test.Tests/SampleTypes/IFirstInterface.cs @@ -0,0 +1,6 @@ +namespace Atc.Test.Tests.SampleTypes; + +public interface IFirstInterface +{ + string? Name { get; set; } +} diff --git a/test/Atc.Test.Tests/SampleTypes/ISecondInterface.cs b/test/Atc.Test.Tests/SampleTypes/ISecondInterface.cs new file mode 100644 index 0000000..3b13a67 --- /dev/null +++ b/test/Atc.Test.Tests/SampleTypes/ISecondInterface.cs @@ -0,0 +1,6 @@ +namespace Atc.Test.Tests.SampleTypes; + +public interface ISecondInterface +{ + string? Description { get; set; } +} diff --git a/test/Atc.Test.Tests/SampleTypes/SecondInterfaceDependant.cs b/test/Atc.Test.Tests/SampleTypes/SecondInterfaceDependant.cs new file mode 100644 index 0000000..48586a3 --- /dev/null +++ b/test/Atc.Test.Tests/SampleTypes/SecondInterfaceDependant.cs @@ -0,0 +1,11 @@ +namespace Atc.Test.Tests.SampleTypes; + +public class SecondInterfaceDependant +{ + public SecondInterfaceDependant(ISecondInterface dependency) + { + Dependency = dependency; + } + + public ISecondInterface Dependency { get; } +} diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 91d531f..a776542 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -16,7 +16,7 @@ - + diff --git a/version.json b/version.json index 3ef0001..ff60197 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.1", + "version": "2.0", "cloudBuild": { "buildNumber": { "enabled": true