Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
543e79d
feat: add TUnit.Assertions.Should package
thomhurst Apr 28, 2026
5b89e90
fix: address PR #5785 review feedback
thomhurst Apr 28, 2026
32bdd5d
feat: discover [GenerateAssertion]/[AssertionFrom]/hand-written exten…
thomhurst Apr 28, 2026
b4f2494
ci: wire TUnit.Assertions.Should into pipeline as beta package
thomhurst Apr 28, 2026
148f72f
refactor: address /simplify review findings
thomhurst Apr 28, 2026
584bfb4
feat: source-generate ShouldCollectionSource instance methods
thomhurst Apr 28, 2026
d121ad1
feat(analyzers): extend existing analyzers to recognise Should syntax
thomhurst Apr 28, 2026
0d59c81
refactor(analyzers): align Should-aware analyzer code with existing p…
thomhurst Apr 28, 2026
9f0460e
docs: add Should syntax page
thomhurst Apr 28, 2026
2830fed
fix: address PR review — CI sln, generator tests, ShouldNameAttribute
thomhurst Apr 28, 2026
8f88cc4
fix: address PR round 5 — docs example + simplify NameConjugator
thomhurst Apr 28, 2026
dd7931a
chore: round 6 cleanup — drop unused packages, kill empty if block
thomhurst Apr 28, 2026
2273360
fix(should-gen): handle -es endings in name conjugator
thomhurst Apr 28, 2026
4c54f08
test(should-gen): add Verify snapshots for generator output
thomhurst Apr 28, 2026
33a903e
fix(should): render generic exception type names without backtick man…
thomhurst Apr 28, 2026
ce5dc38
docs(should): clarify analyzer extension match, Clear() coverage, Doe…
thomhurst Apr 29, 2026
a3cba8e
perf(should-gen): cache per-reference walk by MetadataReference
thomhurst Apr 29, 2026
f8f0fa6
fix(analyzer-tests): load netstandard2.0 builds to avoid CS1705 silen…
thomhurst Apr 29, 2026
6bd1516
fix(should-gen): swap reference cache to ConditionalWeakTable to avoi…
thomhurst Apr 29, 2026
8a726e9
ci(pipeline): wire Should test modules into the release gate
thomhurst Apr 29, 2026
10b0a15
fix(assertions): IsAll -> IsFullRange; propagate Obsolete/EditorBrows…
thomhurst Apr 29, 2026
4769882
refactor(analyzer-tests): centralize GetCompatibleDllPath; document a…
thomhurst Apr 29, 2026
4f9f779
refactor(should): drop ExpressionBuilder.Clear coupling; centralize g…
thomhurst Apr 29, 2026
46ba85a
refactor: drop redundant GetCompatibleDllPath wrappers; document roun…
thomhurst Apr 29, 2026
de03ec8
fix(should): address review edge cases
thomhurst Apr 29, 2026
8d5b321
fix(should): handle z-ending verbs
thomhurst Apr 29, 2026
4fa90f1
fix(should): address review blockers
thomhurst Apr 29, 2026
d5be784
test(should): cover assertion failures
thomhurst Apr 29, 2026
5ad8a66
test(should): cover failing entry shapes
thomhurst Apr 29, 2026
37df1c8
fix(ci): align analyzer test references
thomhurst Apr 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ See the [documentation](https://tunit.dev/docs/getting-started/attributes) for m
| `TUnit.Core` | Shared test library components without an execution engine |
| `TUnit.Engine` | Execution engine for test projects |
| `TUnit.Assertions` | Standalone assertions — works with other test frameworks too |
| `TUnit.Assertions.Should` | Optional FluentAssertions-style `value.Should().BeEqualTo(...)` syntax over `TUnit.Assertions` (beta) |
| `TUnit.Playwright` | Playwright integration with automatic browser lifecycle management |

## Migrating from xUnit, NUnit, or MSTest?
Expand Down
63 changes: 63 additions & 0 deletions SharedTestHelpers/AnalyzerTestCompatibility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System;
using System.IO;
using System.Reflection;

namespace TUnit.Tests.Shared;

/// <summary>
/// Shared resolution helper for the analyzer-test framework's reference-assembly mismatch.
/// </summary>
/// <remarks>
/// <para>
/// The Microsoft.CodeAnalysis.Testing 1.1.2 framework targets net9.0 reference assemblies. Loading
/// TUnit's net10.0 builds via <c>typeof(...).Assembly.Location</c> raises CS1705 (referenced
/// assembly has a higher System.Runtime version), which the verifier suppresses via
/// <c>CompilerDiagnostics = None</c>. The suppression silently breaks symbol resolution for every
/// extension method (IsEqualTo, Throws, etc.) in the test compilation — analyzers find nothing
/// to flag and tests report "expected 1 diagnostic, actual 0" with no compiler errors visible.
/// </para>
/// <para>
/// The fix is to load the netstandard2.0 builds (compatible with both Net90 ref assemblies and the
/// test runtime). Each analyzer test csproj copies the relevant netstandard2.0 build into the test
/// bin via a <c>&lt;None Include="..." Link="X.netstandard2.0.dll" CopyToOutputDirectory="..."&gt;</c>
/// item; this helper resolves that copy and fails fast if it is missing. Falling back to the
/// runtime assembly path would silently reintroduce the CS1705/symbol-resolution failure this
/// helper exists to avoid.
/// </para>
/// <para>
/// Linked into multiple test projects via <c>&lt;Compile Include="..\SharedTestHelpers\..."&gt;</c>;
/// see <c>TUnit.Assertions.Should.SourceGenerator</c> for the same pattern with
/// <c>CovarianceHelper</c>/<c>EquatableArray</c>.
/// </para>
/// </remarks>
internal static class AnalyzerTestCompatibility
{
public static string GetCompatibleDllPath(string assemblyName, Assembly fallback)
{
var ns20Path = Path.Combine(AppContext.BaseDirectory, $"{assemblyName}.netstandard2.0.dll");
if (!File.Exists(ns20Path))
{
throw new FileNotFoundException(
$"netstandard2.0 build of {assemblyName} not found at '{ns20Path}'. " +
"Run 'dotnet build' before running analyzer tests.",
ns20Path);
}

return ns20Path;
}

public static string GetSystemTextJson9DllPath()
{
const string fileName = "System.Text.Json.9.0.dll";
var path = Path.Combine(AppContext.BaseDirectory, fileName);
if (!File.Exists(path))
{
throw new FileNotFoundException(
$"System.Text.Json 9.0 reference not found at '{path}'. " +
"Run 'dotnet build' before running analyzer tests.",
path);
}

return path;
}
}
14 changes: 10 additions & 4 deletions TUnit.Analyzers.Tests/AnalyzerTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ public static CSharpAnalyzerTest<TAnalyzer, DefaultVerifier> CreateAnalyzerTest<
csTest.TestState.AdditionalReferences
.AddRange(
[
MetadataReference.CreateFromFile(typeof(TUnitAttribute).Assembly.Location),
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly)),
#if NET8_0
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetSystemTextJson9DllPath()),
#endif
MetadataReference.CreateFromFile(typeof(CircuitState).Assembly.Location),
MetadataReference.CreateFromFile(typeof(ProjectReferenceEnum).Assembly.Location)
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.TestProject.Library", typeof(ProjectReferenceEnum).Assembly))
]
);

Expand Down Expand Up @@ -145,9 +148,12 @@ public static CSharpSuppressorTest<TSuppressor, DefaultVerifier> CreateSuppresso

test.TestState.AdditionalReferences
.AddRange([
MetadataReference.CreateFromFile(typeof(TUnitAttribute).Assembly.Location),
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly)),
#if NET8_0
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetSystemTextJson9DllPath()),
#endif
MetadataReference.CreateFromFile(typeof(CircuitState).Assembly.Location),
MetadataReference.CreateFromFile(typeof(ProjectReferenceEnum).Assembly.Location)
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.TestProject.Library", typeof(ProjectReferenceEnum).Assembly))
]);

return test;
Expand Down
27 changes: 27 additions & 0 deletions TUnit.Analyzers.Tests/TUnit.Analyzers.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<PackageReference Include="NUnit" />
<PackageReference Include="xunit.v3.extensibility.core" />
<PackageReference Include="xunit.v3.assert" />
<PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net8.0'" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TUnit.Analyzers.CodeFixers\TUnit.Analyzers.CodeFixers.csproj" />
Expand All @@ -22,6 +23,32 @@
<ProjectReference Include="..\TUnit.Assertions\TUnit.Assertions.csproj" />
</ItemGroup>

<!-- Copy netstandard2.0 builds into the test bin so the analyzer-test framework's
Net90 reference assemblies don't trigger CS1705 against the net10.0 builds otherwise
loaded via typeof(...).Assembly.Location. CS1705 is suppressed silently and the resulting
symbol-resolution failures cause analyzers to find nothing to flag. -->
<ItemGroup>
<None Include="..\TUnit.Core\bin\$(Configuration)\netstandard2.0\TUnit.Core.dll"
Link="TUnit.Core.netstandard2.0.dll"
CopyToOutputDirectory="PreserveNewest"
Visible="false" />
<None Include="..\TUnit.Assertions\bin\$(Configuration)\netstandard2.0\TUnit.Assertions.dll"
Link="TUnit.Assertions.netstandard2.0.dll"
CopyToOutputDirectory="PreserveNewest"
Visible="false" />
<None Include="..\TUnit.TestProject.Library\bin\$(Configuration)\netstandard2.0\TUnit.TestProject.Library.dll"
Link="TUnit.TestProject.Library.netstandard2.0.dll"
CopyToOutputDirectory="PreserveNewest"
Visible="false" />
<None Include="$(PkgSystem_Text_Json)\lib\net8.0\System.Text.Json.dll"
Condition="'$(TargetFramework)' == 'net8.0'"
Link="System.Text.Json.9.0.dll"
CopyToOutputDirectory="PreserveNewest"
Visible="false" />
<Compile Include="..\SharedTestHelpers\AnalyzerTestCompatibility.cs"
Link="Shared\AnalyzerTestCompatibility.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" VersionOverride="9.0.0" PrivateAssets="all" GeneratePathProperty="true" />
<Reference Include="$(PkgMicrosoft_CodeAnalysis_NetAnalyzers)\analyzers\dotnet\cs\Microsoft.CodeAnalysis.NetAnalyzers.dll" Analyzer="false" />
Expand Down
10 changes: 7 additions & 3 deletions TUnit.Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,21 @@ public static Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, param
/// <inheritdoc cref="AnalyzerVerifier{TAnalyzer, TTest, TVerifier}.VerifyAnalyzerAsync(string, DiagnosticResult[])"/>
public static async Task VerifyAnalyzerAsync([StringSyntax("c#")] string source, Action<Test> configureTest, params DiagnosticResult[] expected)
{
// Microsoft.Bcl.AsyncInterfaces is required by TUnit.Core.netstandard2.0.dll's typeforward
// of IAsyncEnumerable; without it, tests that use IAsyncEnumerable in their source see
// CS0012/CS0508 alongside the expected analyzer diagnostic.
var test = new Test
{
TestCode = source,
ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
ReferenceAssemblies = ReferenceAssemblies.Net.Net90
.AddPackages([new PackageIdentity("Microsoft.Bcl.AsyncInterfaces", "9.0.0")]),
TestState =
{
AdditionalReferences =
{
typeof(TUnitAttribute).Assembly.Location,
TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly),
typeof(CircuitState).Assembly.Location,
typeof(ProjectReferenceEnum).Assembly.Location,
TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.TestProject.Library", typeof(ProjectReferenceEnum).Assembly),
},
},
};
Expand Down
4 changes: 2 additions & 2 deletions TUnit.Analyzers.Tests/Verifiers/CSharpCodeFixVerifier`2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ params DiagnosticResult[] expected
{
AdditionalReferences =
{
typeof(TUnitAttribute).Assembly.Location,
TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly),
},
}
};
Expand Down Expand Up @@ -97,7 +97,7 @@ Action<Test> configureTest
{
AdditionalReferences =
{
typeof(TUnitAttribute).Assembly.Location,
TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly),
},
},
CodeActionValidationMode = CodeActionValidationMode.SemanticStructure,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,16 @@
<ProjectReference Include="..\TUnit.Core\TUnit.Core.csproj" />
</ItemGroup>

<!-- Copy netstandard2.0 build of TUnit.Core into the test bin so Net90 ref assemblies don't
trigger CS1705 against the test process's net10.0 build. -->
<ItemGroup>
<None Include="..\TUnit.Core\bin\$(Configuration)\netstandard2.0\TUnit.Core.dll"
Link="TUnit.Core.netstandard2.0.dll"
CopyToOutputDirectory="PreserveNewest"
Visible="false" />
<Compile Include="..\SharedTestHelpers\AnalyzerTestCompatibility.cs"
Link="Shared\AnalyzerTestCompatibility.cs" />
</ItemGroup>

<Import Project="..\TestProject.targets" />
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ public static async Task VerifyAnalyzerAsync([StringSyntax("c#")] string source,
var test = new Test
{
TestCode = source,
ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
ReferenceAssemblies = ReferenceAssemblies.Net.Net90
.AddPackages([new PackageIdentity("Microsoft.Bcl.AsyncInterfaces", "9.0.0")]),
TestState =
{
AdditionalReferences =
{
typeof(TUnit.Core.TUnitAttribute).Assembly.Location,
TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnit.Core.TUnitAttribute).Assembly),
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@
<ProjectReference Include="..\TUnit.Assertions.Analyzers.CodeFixers\TUnit.Assertions.Analyzers.CodeFixers.csproj" />
<ProjectReference Include="..\TUnit.Assertions.Analyzers\TUnit.Assertions.Analyzers.csproj" />
</ItemGroup>

<!-- Copy netstandard2.0 builds of TUnit.Core / TUnit.Assertions into the test bin so the
analyzer-test framework's net9.0 reference assemblies don't trigger CS1705 against the
net10.0 builds otherwise loaded via typeof(...).Assembly.Location. CS1705 is suppressed
silently by CompilerDiagnostics.None and the resulting symbol-resolution failures
appear as analyzers/codefixers seeing zero diagnostics. -->
<ItemGroup>
<None Include="..\TUnit.Core\bin\$(Configuration)\netstandard2.0\TUnit.Core.dll"
Link="TUnit.Core.netstandard2.0.dll"
CopyToOutputDirectory="PreserveNewest"
Visible="false" />
<None Include="..\TUnit.Assertions\bin\$(Configuration)\netstandard2.0\TUnit.Assertions.dll"
Link="TUnit.Assertions.netstandard2.0.dll"
CopyToOutputDirectory="PreserveNewest"
Visible="false" />
<Compile Include="..\SharedTestHelpers\AnalyzerTestCompatibility.cs"
Link="Shared\AnalyzerTestCompatibility.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeFix.Testing" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ params DiagnosticResult[] expected
{
AdditionalReferences =
{
typeof(TUnitAttribute).Assembly.Location,
typeof(Assert).Assembly.Location,
TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly),
TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly),
},
},
};
Expand Down Expand Up @@ -102,8 +102,8 @@ public static async Task VerifyCodeFixAsync(
{
AdditionalReferences =
{
typeof(TUnitAttribute).Assembly.Location,
typeof(Assert).Assembly.Location,
TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly),
TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly),
},
},
CodeActionValidationMode = CodeActionValidationMode.SemanticStructure,
Expand Down
20 changes: 13 additions & 7 deletions TUnit.Assertions.Analyzers.Tests/AnalyzerTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ public static CSharpAnalyzerTest<TAnalyzer, DefaultVerifier> CreateAnalyzerTest<
csTest.TestState.AdditionalReferences
.AddRange(
[
MetadataReference.CreateFromFile(typeof(TUnitAttribute).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Assert).Assembly.Location),
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly)),
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly)),
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions.Should", typeof(TUnit.Assertions.Should.ShouldExtensions).Assembly)),
#if NET8_0
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetSystemTextJson9DllPath()),
#endif
]
);

Expand Down Expand Up @@ -139,8 +143,12 @@ public static CSharpSuppressorTest<TSuppressor, DefaultVerifier> CreateSuppresso

test.TestState.AdditionalReferences
.AddRange([
MetadataReference.CreateFromFile(typeof(TUnitAttribute).Assembly.Location),
MetadataReference.CreateFromFile(typeof(Assert).Assembly.Location),
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Core", typeof(TUnitAttribute).Assembly)),
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions", typeof(Assert).Assembly)),
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetCompatibleDllPath("TUnit.Assertions.Should", typeof(TUnit.Assertions.Should.ShouldExtensions).Assembly)),
#if NET8_0
MetadataReference.CreateFromFile(TUnit.Tests.Shared.AnalyzerTestCompatibility.GetSystemTextJson9DllPath()),
#endif
]);

return test;
Expand All @@ -152,9 +160,7 @@ private static ReferenceAssemblies GetReferenceAssemblies()
return ReferenceAssemblies.NetFramework.Net472.Default;
#elif NET8_0
return ReferenceAssemblies.Net.Net80;
#elif NET9_0
return ReferenceAssemblies.Net.Net90;
#elif NET10_0_OR_GREATER
#elif NET9_0_OR_GREATER
return ReferenceAssemblies.Net.Net90;
#else
return ReferenceAssemblies.Net.Net80; // Default fallback
Expand Down
Loading
Loading