From 8a2086386a1c43f1c10b51f78d03aa739b6e93fb Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 21:22:05 +0200 Subject: [PATCH 01/28] chore(deps): upgrade coding-rules and static code analyzers --- .editorconfig | 1 + Directory.Build.props | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.editorconfig b/.editorconfig index 077ee2a..ded94a7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -530,6 +530,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 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 From 931f3c6e703699dce10074d105e1c476abc46ddd Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 21:22:15 +0200 Subject: [PATCH 02/28] chore: migrate solution to new .slnx format --- Atc.Test.sln | 79 ---------------------------------------- Atc.Test.sln.DotSettings | 2 - Atc.Test.slnx | 25 +++++++++++++ 3 files changed, 25 insertions(+), 81 deletions(-) delete mode 100644 Atc.Test.sln delete mode 100644 Atc.Test.sln.DotSettings create mode 100644 Atc.Test.slnx 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + From 8d298d210d89d0e6a1edc012b856fe0efb4e940a Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 21:53:57 +0200 Subject: [PATCH 03/28] chore: add chatmodes, instructions and mcp configuration --- .github/chatmodes/GPT5-Beast.chatmode.md | 121 ++++++++++++++++++++ .github/instructions/csharp.instructions.md | 34 ++++++ .vscode/mcp.json | 31 +++++ 3 files changed, 186 insertions(+) create mode 100644 .github/chatmodes/GPT5-Beast.chatmode.md create mode 100644 .github/instructions/csharp.instructions.md create mode 100644 .vscode/mcp.json 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/.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 From 8034d6d1d3042b65e9c3198f327c59fd038c551e Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 21:54:48 +0200 Subject: [PATCH 04/28] feat: target .net9.0 and upgrade dependencies --- src/Atc.Test/Atc.Test.csproj | 4 ++-- test/Atc.Test.Tests/Atc.Test.Tests.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Atc.Test/Atc.Test.csproj b/src/Atc.Test/Atc.Test.csproj index a235fa0..bbaa63b 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. @@ -22,7 +22,7 @@ - + \ No newline at end of file diff --git a/test/Atc.Test.Tests/Atc.Test.Tests.csproj b/test/Atc.Test.Tests/Atc.Test.Tests.csproj index 45ceaf4..db95ffd 100644 --- a/test/Atc.Test.Tests/Atc.Test.Tests.csproj +++ b/test/Atc.Test.Tests/Atc.Test.Tests.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 false From 0f5e8fac83e16ef9d7361093d8ff4ebdf8897048 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 21:55:35 +0200 Subject: [PATCH 05/28] feat: upgrade dependencies to xunit v3 --- src/Atc.Test/Atc.Test.csproj | 7 +++---- src/Atc.Test/GlobalUsings.cs | 8 ++------ test/Atc.Test.Tests/Atc.Test.Tests.csproj | 13 +++---------- test/Atc.Test.Tests/GlobalUsings.cs | 3 ++- 4 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/Atc.Test/Atc.Test.csproj b/src/Atc.Test/Atc.Test.csproj index bbaa63b..cff8962 100644 --- a/src/Atc.Test/Atc.Test.csproj +++ b/src/Atc.Test/Atc.Test.csproj @@ -1,4 +1,4 @@ - + netstandard2.1;net8.0;net9.0 @@ -15,13 +15,12 @@ - + - - + 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/test/Atc.Test.Tests/Atc.Test.Tests.csproj b/test/Atc.Test.Tests/Atc.Test.Tests.csproj index db95ffd..07f1b6c 100644 --- a/test/Atc.Test.Tests/Atc.Test.Tests.csproj +++ b/test/Atc.Test.Tests/Atc.Test.Tests.csproj @@ -1,22 +1,15 @@ + Exe net9.0 false - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - + + 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 From 8e429e12fc5c106b19169cd5a1c6bb52f0df7909 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 21:55:50 +0200 Subject: [PATCH 06/28] test: enable MTP in test project --- test/Atc.Test.Tests/Atc.Test.Tests.csproj | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/Atc.Test.Tests/Atc.Test.Tests.csproj b/test/Atc.Test.Tests/Atc.Test.Tests.csproj index 07f1b6c..f779fb9 100644 --- a/test/Atc.Test.Tests/Atc.Test.Tests.csproj +++ b/test/Atc.Test.Tests/Atc.Test.Tests.csproj @@ -1,9 +1,14 @@ - + Exe net9.0 false + + true + true + + true From 1518f67e4db2de25088d3a67d105c09dec493c0d Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 21:56:02 +0200 Subject: [PATCH 07/28] chore: add trailing comma in MemberAutoNSubstituteDataAttributeTests --- test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs b/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs index 1417be9..204a269 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] From 5fddcf302b2ea559d0711f5f2651fc2118b73968 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 22:30:19 +0200 Subject: [PATCH 08/28] feat(test): adapt AutoNSubstituteDataAttribute to new AutoDataAttribute pattern Use protected AutoDataAttribute(Func) constructor with lambda. Fully qualify FixtureFactory to avoid shadowing with base property. Ensures custom fixture customizations still applied post-migration. --- src/Atc.Test/AutoNSubstituteDataAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b8d31ddd4f6d053da8f9e3934c05e11d28814807 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 22:30:57 +0200 Subject: [PATCH 09/28] feat(test): update ClassAutoNSubstituteDataAttribute for xUnit v3 async GetData Replace IEnumerable GetData override with ValueTask>. Augment each row with frozen parameter injection + generated specimens. Preserve row metadata; guard against null traits. Align with v3 TheoryDataRow / ITheoryDataRow contract. --- .../ClassAutoNSubstituteDataAttribute.cs | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs index 8122a7a..c72eab3 100644 --- a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs @@ -13,38 +13,56 @@ public ClassAutoNSubstituteDataAttribute(Type @class) { } - public override IEnumerable GetData(MethodInfo testMethod) + 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 injectMethod = typeof(FixtureRegistrar).GetMethod( + nameof(FixtureRegistrar.Inject), + BindingFlags.Public | BindingFlags.Static); - var data = base.GetData(testMethod); - foreach (var values in data) + var augmented = new List(baseRows.Count); + foreach (var row in baseRows) { + var originalData = row.GetData(); var fixture = FixtureFactory.Create(); + + // Inject frozen values if present in source data. foreach (var frozenValue in frozenValues) { - if (values.Length > frozenValue.Index) + if (originalData.Length > frozenValue.Index) { injectMethod? .MakeGenericMethod(frozenValue.ParameterType) - .Invoke(null, [fixture, values[frozenValue.Index]]); + .Invoke(null, [fixture, originalData[frozenValue.Index]]); } } - yield return values + 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; } private static object GetSpecimen( From eb9e4702890ccc5e19aa6b06d46e2d29280417c4 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 22:31:45 +0200 Subject: [PATCH 10/28] feat(test): migrate MemberAutoNSubstituteDataAttribute to xUnit v3 data pipeline Remove obsolete [DataDiscoverer] (reflection abstractions removed in xUnit v3). Replace deprecated ConvertDataItem override with async GetData(MethodInfo, DisposalTracker). Wrap base data rows into new TheoryDataRow instances and append AutoFixture-generated specimens for missing parameters. Preserve existing metadata (skip, explicit, traits, etc.). --- .../MemberAutoNSubstituteDataAttribute.cs | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs index 8047494..300c2a1 100644 --- a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs @@ -13,7 +13,6 @@ namespace Atc.Test; /// IEnumerable<object[]> with the test data. /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] -[DataDiscoverer("Xunit.Sdk.MemberDataDiscoverer", "xunit.core")] public sealed class MemberAutoNSubstituteDataAttribute : MemberDataAttributeBase { public MemberAutoNSubstituteDataAttribute(string memberName, params object[] parameters) @@ -21,23 +20,36 @@ public MemberAutoNSubstituteDataAttribute(string memberName, params object[] par { } - protected override object[] ConvertDataItem(MethodInfo testMethod, object item) + public override async ValueTask> GetData( + MethodInfo testMethod, + DisposalTracker disposalTracker) { - if (item is not object[] values) + var baseRows = await base.GetData(testMethod, disposalTracker).ConfigureAwait(false); + var parameters = testMethod.GetParameters(); + var augmented = new List(baseRows.Count); + + 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(); + var fixture = FixtureFactory.Create(); + var extendedData = data + .Concat(parameters + .Skip(data.Length) + .Select(p => GetSpecimen(fixture, p))) + .ToArray(); - var fixture = FixtureFactory.Create(); + 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; } private static object GetSpecimen( From f78878e44a627c3ab0ee8043e0316c8528244484 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 22:33:20 +0200 Subject: [PATCH 11/28] test: replace deprecated AddRow with Add in theory data --- .../ClassAutoNSubstituteDataAttributeTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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); } } From 814e6d3ba982cae26cdbc490ccb0a261a5ec0b01 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 22:54:42 +0200 Subject: [PATCH 12/28] chore: update coding rules to .net9.0 --- .editorconfig | 12 +++++++++--- atc-coding-rules-updater.json | 2 +- src/.editorconfig | 4 ++-- test/.editorconfig | 5 +++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.editorconfig b/.editorconfig index ded94a7..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 @@ -557,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/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/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 From faebc0f31d4492b1f53b0b532400bcaf3ed53aae Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 22:55:03 +0200 Subject: [PATCH 13/28] fix: expose constructor argument properties on custom data attributes to satisfy CA1019 --- src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs | 6 ++++++ src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs index c72eab3..f3ea3e9 100644 --- a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs @@ -11,8 +11,14 @@ public sealed class ClassAutoNSubstituteDataAttribute : ClassDataAttribute public ClassAutoNSubstituteDataAttribute(Type @class) : base(@class) { + Class = @class; } + /// + /// Gets the class type that provides the enumerable data. + /// + public new Type Class { get; } + public override async ValueTask> GetData( MethodInfo testMethod, DisposalTracker disposalTracker) diff --git a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs index 300c2a1..58009bc 100644 --- a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs @@ -18,8 +18,20 @@ public sealed class MemberAutoNSubstituteDataAttribute : MemberDataAttributeBase public MemberAutoNSubstituteDataAttribute(string memberName, params object[] parameters) : base(memberName, parameters) { + MemberName = memberName; + Parameters = parameters; } + /// + /// 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; } + public override async ValueTask> GetData( MethodInfo testMethod, DisposalTracker disposalTracker) From 6ac806409e74427c5142c7ddc290fdd061722eb8 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 23:01:10 +0200 Subject: [PATCH 14/28] feat: bump version to 2.0.x --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 25c8aab993888b191fc4710421d5e1f74187c85c Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 23:11:13 +0200 Subject: [PATCH 15/28] docs: update README for xUnit v3 (async data pipeline, usage examples, upgrade guidance) --- README.md | 88 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index eed8a85..a5964c2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,46 @@ -[![NuGet Version](https://img.shields.io/nuget/v/Atc.Test.svg?logo=nuget&style=for-the-badge)](https://www.nuget.org/packages/atc.test) - # ATC Test -Common tools for writing tests using XUnit, AutoFixture, NSubstitute and FluentAssertions. +![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. + +## Package References + +Add `Atc.Test` to the project containing your tests or to a shared test utilities project. + +Typical test project (excerpt): + +```xml + + + net9.0 + Exe + true + + + + + + + + + + + +``` -> **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. +`Atc.Test` depends on `xunit.v3.extensibility.core` for the extensibility APIs, but it intentionally does NOT bring in the `xunit.v3` meta-package for you. + +### Why you must still reference xUnit directly + +We do not make `xunit.v3` transitive because: + +- The `xunit.v3` meta-package pulls in runner-related assets that are not targeted for `netstandard2.1`; this causes NU1701 framework fallback warnings if we referenced it from the multi-targeted library. +- Consumers should control the exact xUnit version (pin or float) in their test project without the library forcing an upgrade cadence. +- Keeping test framework + runner packages at the application (test project) layer avoids unexpected breaking changes when updating `Atc.Test`. +- Separation of concerns: `Atc.Test` provides data attributes/utilities; the test project owns the choice of framework + runner configuration. + +If you need a different xUnit patch/minor version, just adjust the `` in your test project. ## Test Attributes @@ -18,8 +51,45 @@ Common tools for writing tests using XUnit, AutoFixture, NSubstitute and FluentA | `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.| +### Usage Examples + +```csharp +public class CalculatorTests +{ + [Theory] + [AutoNSubstituteData] + public void AutoData_Generates_Specimens(int a, int b, Calculator sut) + { + var result = sut.Add(a, b); + result.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 those satisfied by inline/member data) are populated using an AutoFixture `IFixture` customized with NSubstitute for interfaces/abstract classes. + > **Note:** -> NSubstitute is used when the type being created is abstract, or when the `[Substitute]` is applied. +> NSubstitute is used when the type being created is an interface or abstract class. ## Test Helpers @@ -33,7 +103,7 @@ Common tools for writing tests using XUnit, AutoFixture, NSubstitute and FluentA ## Extensibility -The default `Fixture` returned by the `FixtrueFactory.Create()` method is used for all the `Attributes` mentioned above. +The default `Fixture` returned by the `FixtureFactory.Create()` method is used for all the attributes mentioned above. To add customizations to this, you can add the `AutoRegisterAttribute` to any custom `ICustomization` or `ISpecimenBuilder` to have it automatically added to the Fixture. From 16cddb7c972e075d9edf077c808d4bd14f2fb3fe Mon Sep 17 00:00:00 2001 From: Per Kops Date: Thu, 11 Sep 2025 22:34:34 +0200 Subject: [PATCH 16/28] build(test): switch to AutoFixture.Xunit3 in test Directory.build.props --- README.md | 4 ---- test/Directory.Build.props | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index a5964c2..5ff479c 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,6 @@ Typical test project (excerpt): - - - - ``` 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 @@ - + From 4f9bf3a787d4ae62950d7a2b8d9f0f86d05d17b4 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 20:41:18 +0200 Subject: [PATCH 17/28] feat(test): add frozen reuse parity tests for MemberAutoNSubstituteDataAttribute --- ...MemberAutoNSubstituteDataAttributeTests.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs b/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs index 204a269..3650771 100644 --- a/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs +++ b/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs @@ -50,4 +50,34 @@ 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()]; + } } \ No newline at end of file From 2f7dd8be3200932915c3b520994dfaba37fe9185 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 20:42:27 +0200 Subject: [PATCH 18/28] feat: enable frozen value reuse in MemberAutoNSubstituteDataAttribute --- .../MemberAutoNSubstituteDataAttribute.cs | 85 ++++++++++++++++--- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs index 58009bc..d20d348 100644 --- a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs @@ -1,17 +1,18 @@ 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)] public sealed class MemberAutoNSubstituteDataAttribute : MemberDataAttributeBase { @@ -36,14 +37,22 @@ public override async ValueTask> GetData( MethodInfo testMethod, DisposalTracker disposalTracker) { + // 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. + var frozenInjector = BuildFrozenInjector(parameters); + foreach (var row in baseRows) { - var data = row.GetData(); - var fixture = FixtureFactory.Create(); + 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 extendedData = data .Concat(parameters .Skip(data.Length) @@ -64,10 +73,60 @@ public override async ValueTask> GetData( return augmented; } + private static Action BuildFrozenInjector(ParameterInfo[] parameters) + { + // Identify parameters decorated with [Frozen]; capture index + type for later injection/promotion. + var frozenParameters = parameters + .Select((p, i) => (Index: i, Type: p.ParameterType, Frozen: p.GetCustomAttribute())) + .Where(x => x.Frozen is not null) + .ToArray(); + + if (frozenParameters.Length == 0) + { + // Fast path: no frozen parameters -> no-op. + return static (_, _) => { }; + } + + var injectMethod = typeof(FixtureRegistrar).GetMethod( + nameof(FixtureRegistrar.Inject), + BindingFlags.Public | BindingFlags.Static); + + return (suppliedData, fixture) => + { + // Phase 1: Direct positional injections for frozen parameters already covered by supplied row data. + foreach (var frozen in frozenParameters) + { + if (suppliedData.Length > frozen.Index) + { + injectMethod? + .MakeGenericMethod(frozen.Type) + .Invoke(null, [fixture, suppliedData[frozen.Index]]); + } + } + + // Phase 2: Promotions – for frozen parameters whose index is beyond supplied data length, + // attempt to reuse an earlier compatible supplied argument (interface / base type friendly). + foreach (var frozen in frozenParameters) + { + if (suppliedData.Length <= frozen.Index) + { + var promoted = suppliedData.FirstOrDefault(d => d is not null && frozen.Type.IsInstanceOfType(d)); + if (promoted is not null) + { + injectMethod? + .MakeGenericMethod(frozen.Type) + .Invoke(null, [fixture, promoted]); + } + } + } + }; + } + private static object GetSpecimen( IFixture fixture, ParameterInfo parameter) { + // Gather parameter-level customization sources (e.g. [Frozen], [Greedy], etc.) var attributes = parameter .GetCustomAttributes() .OfType() @@ -75,11 +134,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); } From 30519447a8e500a700eaab0a993b70dd2243a786 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 21:35:27 +0200 Subject: [PATCH 19/28] docs(member-data): add XML docs and detailed frozen reuse explanation --- .../MemberAutoNSubstituteDataAttribute.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs index d20d348..eac00cf 100644 --- a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs @@ -33,6 +33,19 @@ public MemberAutoNSubstituteDataAttribute(string memberName, params object[] par /// 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) @@ -73,6 +86,18 @@ public override async ValueTask> GetData( return augmented; } + /// + /// Builds a delegate that performs two-phase frozen value handling for the supplied test method parameters. + /// + /// Ordered parameter list from the test method. + /// An action taking (suppliedRowValues, fixture) which injects any frozen instances. + /// + /// Phase 1 (Direct): For each parameter marked with , if the member row already + /// supplies a value at the same index, that instance is injected (frozen) into the fixture. + /// Phase 2 (Promotion): For frozen parameters whose index exceeds the supplied row length, the earliest + /// previously supplied compatible instance (assignable type) is promoted and injected. This enables scenarios + /// where the developer supplies a value earlier and later annotates a parameter of the same type with [Frozen]. + /// private static Action BuildFrozenInjector(ParameterInfo[] parameters) { // Identify parameters decorated with [Frozen]; capture index + type for later injection/promotion. @@ -122,6 +147,16 @@ public override async ValueTask> GetData( }; } + /// + /// 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) From 2a44894cc428a1a5dd5b1df7d2822a774aa7899e Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 21:35:43 +0200 Subject: [PATCH 20/28] docs(class-data): document GetData and specimen resolution with frozen handling --- .../ClassAutoNSubstituteDataAttribute.cs | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs index f3ea3e9..ed43117 100644 --- a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs @@ -19,6 +19,20 @@ public ClassAutoNSubstituteDataAttribute(Type @class) /// 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) @@ -33,22 +47,27 @@ public override async ValueTask> GetData( nameof(FixtureRegistrar.Inject), BindingFlags.Public | BindingFlags.Static); - var augmented = new List(baseRows.Count); - foreach (var row in baseRows) + void InjectFrozen(object?[] originalData, IFixture f) { - var originalData = row.GetData(); - var fixture = FixtureFactory.Create(); - - // Inject frozen values if present in source data. foreach (var frozenValue in frozenValues) { if (originalData.Length > frozenValue.Index) { injectMethod? .MakeGenericMethod(frozenValue.ParameterType) - .Invoke(null, [fixture, originalData[frozenValue.Index]]); + .Invoke(null, [f, originalData[frozenValue.Index]]); } } + } + + var augmented = new List(baseRows.Count); + foreach (var row in baseRows) + { + var originalData = row.GetData(); + var fixture = FixtureFactory.Create(); + + // Inject frozen values if present in source data (positional only for class data). + InjectFrozen(originalData, fixture); var extendedData = originalData .Concat(parameters @@ -71,6 +90,16 @@ public override async ValueTask> GetData( 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) From 5cc4434e2b3f27a611dcf5b7b487756e01621381 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 21:43:40 +0200 Subject: [PATCH 21/28] test(member-data): add failing negative promotion test across different interfaces --- ...MemberAutoNSubstituteDataAttributeTests.cs | 31 +++++++++++++++++++ .../SampleTypes/DualInterfaceImpl.cs | 11 +++++++ .../SampleTypes/IFirstInterface.cs | 6 ++++ .../SampleTypes/ISecondInterface.cs | 6 ++++ .../SampleTypes/SecondInterfaceDependant.cs | 11 +++++++ 5 files changed, 65 insertions(+) create mode 100644 test/Atc.Test.Tests/SampleTypes/DualInterfaceImpl.cs create mode 100644 test/Atc.Test.Tests/SampleTypes/IFirstInterface.cs create mode 100644 test/Atc.Test.Tests/SampleTypes/ISecondInterface.cs create mode 100644 test/Atc.Test.Tests/SampleTypes/SecondInterfaceDependant.cs diff --git a/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs b/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs index 3650771..f80893c 100644 --- a/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs +++ b/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs @@ -80,4 +80,35 @@ public void MemberData_SuppliesValueEarlier_ReusedByLaterFrozenParameter( { 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" }]; + } } \ 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; } +} From 71b2e09f20d587706f6d5e1fe1527078db1f3f40 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 21:54:21 +0200 Subject: [PATCH 22/28] refactor: extract FrozenParameterInjector and tighten exact-type promotion --- .../ClassAutoNSubstituteDataAttribute.cs | 25 +---- src/Atc.Test/FrozenParameterInjector.cs | 96 +++++++++++++++++++ .../MemberAutoNSubstituteDataAttribute.cs | 65 +------------ 3 files changed, 102 insertions(+), 84 deletions(-) create mode 100644 src/Atc.Test/FrozenParameterInjector.cs diff --git a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs index ed43117..724722a 100644 --- a/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/ClassAutoNSubstituteDataAttribute.cs @@ -39,26 +39,9 @@ public override async ValueTask> GetData( { 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); - void InjectFrozen(object?[] originalData, IFixture f) - { - foreach (var frozenValue in frozenValues) - { - if (originalData.Length > frozenValue.Index) - { - injectMethod? - .MakeGenericMethod(frozenValue.ParameterType) - .Invoke(null, [f, originalData[frozenValue.Index]]); - } - } - } + // 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) @@ -66,8 +49,8 @@ void InjectFrozen(object?[] originalData, IFixture f) var originalData = row.GetData(); var fixture = FixtureFactory.Create(); - // Inject frozen values if present in source data (positional only for class data). - InjectFrozen(originalData, fixture); + // Inject frozen values if present in source data (positional only). + frozenInjector(originalData, fixture); var extendedData = originalData .Concat(parameters 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/MemberAutoNSubstituteDataAttribute.cs b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs index eac00cf..f765635 100644 --- a/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs +++ b/src/Atc.Test/MemberAutoNSubstituteDataAttribute.cs @@ -55,8 +55,8 @@ public override async ValueTask> GetData( var parameters = testMethod.GetParameters(); var augmented = new List(baseRows.Count); - // Pre-compute an injector tailored to the frozen parameters of this method. - var frozenInjector = BuildFrozenInjector(parameters); + // 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) { @@ -86,67 +86,6 @@ public override async ValueTask> GetData( return augmented; } - /// - /// Builds a delegate that performs two-phase frozen value handling for the supplied test method parameters. - /// - /// Ordered parameter list from the test method. - /// An action taking (suppliedRowValues, fixture) which injects any frozen instances. - /// - /// Phase 1 (Direct): For each parameter marked with , if the member row already - /// supplies a value at the same index, that instance is injected (frozen) into the fixture. - /// Phase 2 (Promotion): For frozen parameters whose index exceeds the supplied row length, the earliest - /// previously supplied compatible instance (assignable type) is promoted and injected. This enables scenarios - /// where the developer supplies a value earlier and later annotates a parameter of the same type with [Frozen]. - /// - private static Action BuildFrozenInjector(ParameterInfo[] parameters) - { - // Identify parameters decorated with [Frozen]; capture index + type for later injection/promotion. - var frozenParameters = parameters - .Select((p, i) => (Index: i, Type: p.ParameterType, Frozen: p.GetCustomAttribute())) - .Where(x => x.Frozen is not null) - .ToArray(); - - if (frozenParameters.Length == 0) - { - // Fast path: no frozen parameters -> no-op. - return static (_, _) => { }; - } - - var injectMethod = typeof(FixtureRegistrar).GetMethod( - nameof(FixtureRegistrar.Inject), - BindingFlags.Public | BindingFlags.Static); - - return (suppliedData, fixture) => - { - // Phase 1: Direct positional injections for frozen parameters already covered by supplied row data. - foreach (var frozen in frozenParameters) - { - if (suppliedData.Length > frozen.Index) - { - injectMethod? - .MakeGenericMethod(frozen.Type) - .Invoke(null, [fixture, suppliedData[frozen.Index]]); - } - } - - // Phase 2: Promotions – for frozen parameters whose index is beyond supplied data length, - // attempt to reuse an earlier compatible supplied argument (interface / base type friendly). - foreach (var frozen in frozenParameters) - { - if (suppliedData.Length <= frozen.Index) - { - var promoted = suppliedData.FirstOrDefault(d => d is not null && frozen.Type.IsInstanceOfType(d)); - if (promoted is not null) - { - injectMethod? - .MakeGenericMethod(frozen.Type) - .Invoke(null, [fixture, promoted]); - } - } - } - }; - } - /// /// Resolves a specimen for a single parameter after applying any parameter-level customizations. /// From c2cee4814d88932761d6402ef2bf4f147c3ce353 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 22:04:20 +0200 Subject: [PATCH 23/28] docs(readme): add frozen reuse, promotion, and non-promotion examples --- README.md | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/README.md b/README.md index 5ff479c..8cfe1f4 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,79 @@ All remaining parameters (after those satisfied by inline/member data) are popul > **Note:** > NSubstitute is used when the type being created is an interface or abstract class. +### Frozen Reuse Scenarios + +When you decorate a parameter with `[Frozen]`, its resolved instance is reused for any other specimens needing that type. The `MemberAutoNSubstituteData` attribute supports an additional convenience: **exact-type promotion** of an earlier supplied value to a later `[Frozen]` parameter whose slot was not part of the supplied member row. + +| Scenario | Attribute | Behavior | +|----------|-----------|----------| +| Positional frozen reuse | `ClassAutoNSubstituteData` + `MemberAutoNSubstituteData` | If the data row supplies a value at the same parameter index as a `[Frozen]` parameter, that value is frozen and reused. | +| Exact-type promotion (member data only) | `MemberAutoNSubstituteData` | If a later `[Frozen] T` parameter has no supplied value (index beyond row length), we look for an earlier supplied value whose parameter type is exactly `T` and freeze it. | +| No interface/base promotion | Both | We do NOT promote across interface or base types—only exact parameter type matches. | + +#### 1. Positional Reuse + +```csharp +[Theory] +[InlineAutoNSubstituteData(42)] +public void Positional_Frozen_Reuses_Inline_Value( + [Frozen] int number, // inline supplies index 0 -> frozen + SomeConsumer consumer) // receives the same number if it depends on it +{ + consumer.NumberDependency.Should().Be(number); +} +``` + +#### 2. Exact-Type Promotion (Member Data Only) + +```csharp +public static IEnumerable ServiceRow() => new [] +{ + new object?[] { Substitute.For() } // supplies parameter 0 only +}; + +[Theory] +[MemberAutoNSubstituteData(nameof(ServiceRow))] +public void Promotion_Reuses_Earlier_Same_Type( + IMyService supplied, // index 0 supplied + [Frozen] IMyService frozenLater, // not supplied -> promoted reuse + NeedsService consumer) // receives frozen instance +{ + frozenLater.Should().BeSameAs(supplied); + consumer.Service.Should().BeSameAs(supplied); +} +``` + +#### 3. Non-Promotion Across Different Interfaces + +```csharp +public interface IFoo {} +public interface IBar {} +public class DualImpl : IFoo, IBar {} + +public static IEnumerable DualRow() => new [] +{ + new object?[] { new DualImpl() } // supplies IFoo parameter only +}; + +[Theory] +[MemberAutoNSubstituteData(nameof(DualRow))] +public void Different_Interface_Not_Promoted( + IFoo foo, // supplied DualImpl + [Frozen] IBar bar, // exact-type mismatch (IBar vs IFoo) -> NOT reused + UsesBar consumer) +{ + bar.Should().NotBeSameAs(foo); // separate instance + consumer.Bar.Should().BeSameAs(bar); // consumer wired to frozen IBar +} +``` + +Design Rationale: + +- Class data is typically authored with full positional intent—implicit promotion could hide mistakes. +- Member data commonly supplies only a prefix, so exact-type promotion avoids boilerplate duplication while staying predictable. +- Restricting to exact type (no interface/base assignability) prevents accidental cross-interface freezes (e.g., a dual-implemented object hijacking a different abstraction). + ## Test Helpers | Name | Description | From a160e4aeae24804b70cee123e698c44e3d8e68de Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 22:15:22 +0200 Subject: [PATCH 24/28] docs(readme): clarify xunit v3-only compatibility and unsupported scenarios --- README.md | 206 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 130 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index 8cfe1f4..f81ba35 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,40 @@ -# ATC Test +# Introduction ![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. -## Package References +## Table of Content -Add `Atc.Test` to the project containing your tests or to a shared test utilities project. +- [Introduction](#introduction) +- [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) -Typical test project (excerpt): +## Features + +- 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 @@ -25,29 +51,47 @@ Typical test project (excerpt): ``` -`Atc.Test` depends on `xunit.v3.extensibility.core` for the extensibility APIs, but it intentionally does NOT bring in the `xunit.v3` meta-package for you. +### Why xUnit Must Be Referenced Directly -### Why you must still reference xUnit directly +`Atc.Test` depends on `xunit.v3.extensibility.core` (the extensibility surface) but intentionally does **not** bring in the `xunit.v3` meta-package: -We do not make `xunit.v3` transitive because: +- 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. -- The `xunit.v3` meta-package pulls in runner-related assets that are not targeted for `netstandard2.1`; this causes NU1701 framework fallback warnings if we referenced it from the multi-targeted library. -- Consumers should control the exact xUnit version (pin or float) in their test project without the library forcing an upgrade cadence. -- Keeping test framework + runner packages at the application (test project) layer avoids unexpected breaking changes when updating `Atc.Test`. -- Separation of concerns: `Atc.Test` provides data attributes/utilities; the test project owns the choice of framework + runner configuration. +If you want a different xUnit patch/minor version, change the `` line—no changes to `Atc.Test` required. -If you need a different xUnit patch/minor version, just adjust the `` in your test project. +#### xUnit v3 Only (Incompatible With v2) +`Atc.Test` relies on xUnit v3 extensibility APIs: -## Test Attributes +- Async data attribute signature: `ValueTask> GetData(...)`. +- `ITheoryDataRow` & metadata (Label, Explicit, Timeout) preservation. +- `DisposalTracker` parameter passed to data attributes. -| 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.| +These do not exist in xUnit v2. Attempting to use a v2 framework or runner will result in discovery failures or compile errors. -### Usage Examples +| 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 @@ -55,17 +99,12 @@ public class CalculatorTests [Theory] [AutoNSubstituteData] public void AutoData_Generates_Specimens(int a, int b, Calculator sut) - { - var result = sut.Add(a, b); - result.Should().Be(a + b); - } + => 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); - } + => sut.Add(a, b).Should().Be(5); public static IEnumerable MemberSource() { @@ -76,110 +115,125 @@ public class CalculatorTests [Theory] [MemberAutoNSubstituteData(nameof(MemberSource))] public void MemberAutoData_Augments_Member_Data(int a, int b, Calculator sut) - { - sut.Add(a, b).Should().Be(a + b); - } + => sut.Add(a, b).Should().Be(a + b); } ``` -All remaining parameters (after those satisfied by inline/member data) are populated using an AutoFixture `IFixture` customized with NSubstitute for interfaces/abstract classes. +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 when the type being created is an interface or abstract class. +> **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 any other specimens needing that type. The `MemberAutoNSubstituteData` attribute supports an additional convenience: **exact-type promotion** of an earlier supplied value to a later `[Frozen]` parameter whose slot was not part of the supplied member row. +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 the data row supplies a value at the same parameter index as a `[Frozen]` parameter, that value is frozen and reused. | -| Exact-type promotion (member data only) | `MemberAutoNSubstituteData` | If a later `[Frozen] T` parameter has no supplied value (index beyond row length), we look for an earlier supplied value whose parameter type is exactly `T` and freeze it. | -| No interface/base promotion | Both | We do NOT promote across interface or base types—only exact parameter type matches. | +| 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). | -#### 1. Positional Reuse +#### Example: Positional Reuse ```csharp [Theory] [InlineAutoNSubstituteData(42)] public void Positional_Frozen_Reuses_Inline_Value( - [Frozen] int number, // inline supplies index 0 -> frozen - SomeConsumer consumer) // receives the same number if it depends on it + [Frozen] int number, + SomeConsumer consumer) { - consumer.NumberDependency.Should().Be(number); + consumer.NumberDependency.Should().Be(number); } ``` -#### 2. Exact-Type Promotion (Member Data Only) +#### Example: Exact-Type Promotion (Member Data) ```csharp -public static IEnumerable ServiceRow() => new [] +public static IEnumerable ServiceRow() { - new object?[] { Substitute.For() } // supplies parameter 0 only -}; + yield return new object?[] { Substitute.For() }; // supplies parameter 0 only +} [Theory] [MemberAutoNSubstituteData(nameof(ServiceRow))] public void Promotion_Reuses_Earlier_Same_Type( - IMyService supplied, // index 0 supplied - [Frozen] IMyService frozenLater, // not supplied -> promoted reuse - NeedsService consumer) // receives frozen instance + IMyService supplied, + [Frozen] IMyService frozenLater, + NeedsService consumer) { - frozenLater.Should().BeSameAs(supplied); - consumer.Service.Should().BeSameAs(supplied); + frozenLater.Should().BeSameAs(supplied); + consumer.Service.Should().BeSameAs(supplied); } ``` -#### 3. Non-Promotion Across Different Interfaces +#### Example: Non-Promotion Across Different Interfaces ```csharp public interface IFoo {} public interface IBar {} public class DualImpl : IFoo, IBar {} -public static IEnumerable DualRow() => new [] +public static IEnumerable DualRow() { - new object?[] { new DualImpl() } // supplies IFoo parameter only -}; + yield return new object?[] { new DualImpl() }; // supplies IFoo parameter only +} [Theory] [MemberAutoNSubstituteData(nameof(DualRow))] public void Different_Interface_Not_Promoted( - IFoo foo, // supplied DualImpl - [Frozen] IBar bar, // exact-type mismatch (IBar vs IFoo) -> NOT reused - UsesBar consumer) + IFoo foo, + [Frozen] IBar bar, + UsesBar consumer) { - bar.Should().NotBeSameAs(foo); // separate instance - consumer.Bar.Should().BeSameAs(bar); // consumer wired to frozen IBar + bar.Should().NotBeSameAs(foo); // separate instance + consumer.Bar.Should().BeSameAs(bar); // consumer wired to frozen IBar } ``` Design Rationale: -- Class data is typically authored with full positional intent—implicit promotion could hide mistakes. -- Member data commonly supplies only a prefix, so exact-type promotion avoids boilerplate duplication while staying predictable. -- Restricting to exact type (no interface/base assignability) prevents accidental cross-interface freezes (e.g., a dual-implemented object hijacking a different abstraction). +- 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). -## Test Helpers +### Auto Registration of Customizations -| 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. | +Any `ICustomization` or `ISpecimenBuilder` decorated with `[AutoRegister]` is added automatically to the fixture created by `FixtureFactory.Create()`. -## Extensibility +Example: -The default `Fixture` returned by the `FixtureFactory.Create()` method is used for all the attributes mentioned above. +```csharp +[AutoRegister] +public class GuidCustomization : ICustomization +{ + public void Customize(IFixture fixture) => fixture.Register(() => Guid.NewGuid()); +} +``` + +### Helper Extensions -To add customizations to this, you can add the `AutoRegisterAttribute` to any custom `ICustomization` or `ISpecimenBuilder` to have it automatically added to the Fixture. +| 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`. | -See [`CancellationTokenGenerator`](src/Atc.Test/Customizations/Generators/CancellationTokenGenerator.cs) for an example on how to do this. +## Requirements -## How to contribute +| 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) | -[Contribution Guidelines](https://atc-net.github.io/introduction/about-atc#how-to-contribute) +## 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) + From f2c47e73727e3766039110b0c54f67911ac9b577 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 22:26:24 +0200 Subject: [PATCH 25/28] test(member-data): add multi-[Frozen] same-type and earliest-wins promotion tests --- ...MemberAutoNSubstituteDataAttributeTests.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs b/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs index f80893c..700b0d6 100644 --- a/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs +++ b/test/Atc.Test.Tests/MemberAutoNSubstituteDataAttributeTests.cs @@ -111,4 +111,54 @@ public void MemberData_DifferentInterface_NotPromoted( { 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 From 8f28408dbab4204eee5cfdd1f918e6a4fa25b767 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Fri, 12 Sep 2025 23:17:11 +0200 Subject: [PATCH 26/28] docs(readme): add value proposition (Why Atc.Test) and adjust wording --- README.md | 91 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index f81ba35..1e6393f 100644 --- a/README.md +++ b/README.md @@ -4,31 +4,62 @@ `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. +## Why Atc.Test + +> You can “just wire everything manually” with plain xUnit and hand‑rolled mocks—so why use this instead? + +| 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 | + +### When It Delivers the Most Value + +* 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. + +### When Bare xUnit (+ manual mocks) May Be Enough + +* 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. + +### Summary + +`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. + ## Table of Content -- [Introduction](#introduction) -- [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) +* [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) ## Features -- 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. +* 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 @@ -55,10 +86,10 @@ Add `Atc.Test` to your test project along with explicit references to xUnit and `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. +* 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. @@ -66,9 +97,9 @@ If you want a different xUnit patch/minor version, change the `> GetData(...)`. -- `ITheoryDataRow` & metadata (Label, Explicit, Timeout) preservation. -- `DisposalTracker` parameter passed to data attributes. +* 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. @@ -195,9 +226,9 @@ public void Different_Interface_Not_Promoted( 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). +* 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 From 7cd6c3feb51cdd1b579b7f681e6fcf070a660922 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Sun, 14 Sep 2025 11:27:40 +0200 Subject: [PATCH 27/28] ci: upgrade pipelines to .NET 9 --- .github/workflows/post-integration.yml | 4 ++-- .github/workflows/pre-integration.yml | 8 ++++---- .github/workflows/release.yml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/post-integration.yml b/.github/workflows/post-integration.yml index c99fb26..e6d3aa9 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 + - name: ⚙️ Setup dotnet 9.0.x uses: actions/setup-dotnet@v4 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..d10b365 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 + - name: ⚙️ Setup dotnet 9.0.x uses: actions/setup-dotnet@v4 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 + - name: ⚙️ Setup dotnet 9.0.x uses: actions/setup-dotnet@v4 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..cd55d3c 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 + - name: ⚙️ Setup dotnet 9.0.x uses: actions/setup-dotnet@v4 with: - dotnet-version: '8.0.x' + dotnet-version: '9.0.x' - name: 🧹 Clean run: dotnet clean -c Release && dotnet nuget locals all --clear From 94fe8d574790e47b24c62387eedba146bc15cc85 Mon Sep 17 00:00:00 2001 From: Per Kops Date: Mon, 15 Sep 2025 10:28:25 +0200 Subject: [PATCH 28/28] chore: upgrade actions/setup-dotnet to v5 --- .github/workflows/post-integration.yml | 2 +- .github/workflows/pre-integration.yml | 4 ++-- .github/workflows/release.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/post-integration.yml b/.github/workflows/post-integration.yml index e6d3aa9..b938d2f 100644 --- a/.github/workflows/post-integration.yml +++ b/.github/workflows/post-integration.yml @@ -31,7 +31,7 @@ jobs: setAllVars: true - name: ⚙️ Setup dotnet 9.0.x - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/pre-integration.yml b/.github/workflows/pre-integration.yml index d10b365..2bc89a9 100644 --- a/.github/workflows/pre-integration.yml +++ b/.github/workflows/pre-integration.yml @@ -20,7 +20,7 @@ jobs: fetch-depth: 0 - name: ⚙️ Setup dotnet 9.0.x - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '9.0.x' @@ -44,7 +44,7 @@ jobs: fetch-depth: 0 - name: ⚙️ Setup dotnet 9.0.x - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cd55d3c..493295e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: setAllVars: true - name: ⚙️ Setup dotnet 9.0.x - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: dotnet-version: '9.0.x'