diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 281043f636..c86cbf234d 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,261 +1,989 @@ -# GitHub Copilot Instructions for TUnit - -## IMPORTANT: Read This First - -When assisting with TUnit development: -1. **ALWAYS** maintain behavioral parity between source-generated and reflection modes -2. **ALWAYS** run `dotnet test TUnit.PublicAPI` when modifying public APIs in TUnit.Core, TUnit.Engine, or TUnit.Playwright -3. **ALWAYS** run source generator tests when changing source generator output -4. **NEVER** use VSTest APIs - use Microsoft.Testing.Platform instead -5. **PREFER** performance over convenience - this framework may be used by millions -6. **FOLLOW** modern C# patterns and the coding standards outlined below - -## Repository Overview - -TUnit is a modern .NET testing framework designed as an alternative to xUnit, NUnit, and MSTest. It leverages the newer Microsoft.Testing.Platform instead of the legacy VSTest framework, providing improved performance and modern .NET capabilities. - -## Key Architecture Concepts - -### Dual Execution Modes -TUnit operates in two distinct execution modes that must maintain behavioral parity: - -1. **Source Generated Mode**: Uses compile-time source generation for optimal performance -2. **Reflection Mode**: Uses runtime reflection for dynamic scenarios - -**Critical Rule**: Both modes must produce identical end-user behavior. Any feature or bug fix must be implemented consistently across both execution paths. - -### Core Components -- **TUnit.Core**: Core abstractions and interfaces -- **TUnit.Engine**: Test discovery and execution engine -- **TUnit.Core.SourceGenerator**: Compile-time test generation -- **TUnit.Assertions**: Fluent assertion library -- **TUnit.Analyzers**: Roslyn analyzers for compile-time validation - -## Coding Standards and Best Practices - -### .NET Modern Syntax -- Use collection initializers: `List list = []` instead of `List list = new()` -- Leverage pattern matching, records, and modern C# features -- Use file-scoped namespaces where appropriate -- Prefer `var` for local variables when type is obvious - -### Code Formatting -- Always use braces for control structures, even single-line statements: - ```csharp - if (condition) - { - DoSomething(); - } - ``` -- Maintain consistent spacing between methods and logical code blocks -- Use expression-bodied members for simple properties and methods -- Follow standard .NET naming conventions (PascalCase for public members, _camelCase for private fields) -- Don't pollute code with unnecessary comments; use meaningful names instead - -### Performance Considerations -- **Critical**: TUnit may be used by millions of developers - performance is paramount -- Avoid unnecessary allocations in hot paths -- Use `ValueTask` over `Task` for potentially synchronous operations -- Leverage object pooling for frequently created objects -- Consider memory usage patterns, especially in test discovery and execution - -### Architecture Principles -- **Single Responsibility Principle**: Classes should have one reason to change -- **Avoid Over-engineering**: Prefer simple, maintainable solutions -- **Composition over Inheritance**: Use dependency injection and composition patterns -- **Immutability**: Prefer immutable data structures where possible - -## Testing Framework Specifics - -### Microsoft.Testing.Platform Integration -- Use Microsoft.Testing.Platform APIs instead of VSTest -- Test filtering syntax: `dotnet test -- --treenode-filter /Assembly/Namespace/ClassName/TestName` -- Understand the platform's execution model and lifecycle hooks - -### Data Generation and Attributes -- Support multiple data source attributes: `[Arguments]`, `[MethodDataSource]`, `[ClassDataSource]`, etc. -- Reuse existing data generation logic instead of duplicating code -- Maintain consistency between reflection and source-generated approaches - -### Test Discovery and Execution -- Test discovery should be fast and efficient -- Support dynamic test generation while maintaining type safety -- Handle edge cases gracefully (generic types, inheritance, etc.) - -## Common Patterns and Conventions - -### Error Handling -- Use specific exception types with meaningful messages -- Provide contextual information in error messages for debugging -- Handle reflection failures gracefully with fallback mechanisms - -### Async/Await -- Use `ValueTask` for performance-critical async operations -- Properly handle `CancellationToken` throughout the pipeline -- Avoid async void except for event handlers - -### Reflection Usage -- Use `[UnconditionalSuppressMessage]` attributes appropriately for AOT/trimming -- Cache reflection results where possible for performance -- Provide both reflection and source-generated code paths - -## Code Review Guidelines - -### When Adding New Features -1. Implement in both source-generated and reflection modes -2. Add corresponding analyzer rules if applicable -3. Include comprehensive tests covering edge cases -4. Verify performance impact with benchmarks if relevant -5. Update documentation and ensure API consistency - -### When Fixing Bugs -1. Identify if the issue affects one or both execution modes -2. Write a failing test that reproduces the issue -3. Fix the bug in all affected code paths -4. Verify the fix doesn't introduce performance regressions - -### Code Quality Checklist -- [ ] No unused using statements -- [ ] Proper null handling (nullable reference types) -- [ ] Appropriate access modifiers -- [ ] XML documentation for public APIs -- [ ] No magic strings or numbers (use constants) -- [ ] Proper disposal of resources - -## Testing and Validation +# TUnit Development Guide for AI Assistants + +## Table of Contents +- [Critical Rules - READ FIRST](#critical-rules---read-first) +- [Quick Reference](#quick-reference) +- [Project Overview](#project-overview) +- [Architecture](#architecture) +- [Development Workflow](#development-workflow) +- [Code Style Standards](#code-style-standards) +- [Testing Guidelines](#testing-guidelines) +- [Performance Requirements](#performance-requirements) +- [Common Patterns](#common-patterns) +- [Troubleshooting](#troubleshooting) + +--- + +## Critical Rules - READ FIRST + +### The Five Commandments + +1. **DUAL-MODE IMPLEMENTATION IS MANDATORY** + - Every feature MUST work identically in both execution modes: + - **Source-Generated Mode**: Compile-time code generation via `TUnit.Core.SourceGenerator` + - **Reflection Mode**: Runtime discovery via `TUnit.Engine` + - Test both modes explicitly. Never assume parity without verification. + - If you only implement in one mode, the feature is incomplete and MUST NOT be merged. + +2. **SNAPSHOT TESTS ARE NON-NEGOTIABLE** + - After ANY change to source generator output: + ```bash + dotnet test TUnit.Core.SourceGenerator.Tests + # Review .received.txt files, then: + for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done # Linux/macOS + for %f in (*.received.txt) do move /Y "%f" "%~nf.verified.txt" # Windows + ``` + - After ANY public API change (TUnit.Core, TUnit.Engine, TUnit.Assertions): + ```bash + dotnet test TUnit.PublicAPI + # Review and accept snapshots as above + ``` + - Commit ALL `.verified.txt` files. These are the source of truth. -### Test Categories -- Unit tests for individual components -- Integration tests for cross-component functionality -- Performance benchmarks for critical paths -- Analyzer tests for compile-time validation -- Source generator snapshot tests -- Public API snapshot tests +3. **NEVER USE VSTest APIs** + - This project uses **Microsoft.Testing.Platform** exclusively + - VSTest is legacy and incompatible with TUnit's architecture + - If you see `Microsoft.VisualStudio.TestPlatform`, it's wrong + +4. **PERFORMANCE IS A FEATURE** + - TUnit is used by millions of tests daily + - Every allocation in discovery/execution hot paths matters + - Profile before and after for any changes in critical paths + - Use `ValueTask`, object pooling, and cached reflection + +5. **AOT/TRIMMING COMPATIBILITY IS REQUIRED** + - All code must work with Native AOT and IL trimming + - Use `[DynamicallyAccessedMembers]` and `[UnconditionalSuppressMessage]` appropriately + - Test changes with AOT-compiled projects when touching reflection + +--- + +## Quick Reference + +### Most Common Commands +```bash +# Run all tests +dotnet test + +# Test source generator + accept snapshots +dotnet test TUnit.Core.SourceGenerator.Tests +for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done + +# Test public API + accept snapshots +dotnet test TUnit.PublicAPI +for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done + +# Run specific test by tree node filter +dotnet test -- --treenode-filter "/Assembly/Namespace/ClassName/TestName" + +# Run tests excluding performance tests +dotnet test -- --treenode-filter "/*/*/*/*[Category!=Performance]" + +# Build in release mode +dotnet build -c Release + +# Test AOT compilation +dotnet publish -c Release -p:PublishAot=true +``` + +### Snapshot Workflow Quick Ref +``` +┌─────────────────────────────────────────────────────────┐ +│ 1. Make change to source generator or public API │ +│ 2. Run relevant test: dotnet test [Project] │ +│ 3. If snapshots differ, review .received.txt files │ +│ 4. If changes are correct, rename to .verified.txt │ +│ 5. Commit .verified.txt files │ +│ 6. NEVER commit .received.txt files │ +└─────────────────────────────────────────────────────────┘ +``` + +### Test Filter Syntax +```bash +# Single test +--treenode-filter "/TUnit.TestProject/Namespace/ClassName/TestMethodName" + +# All tests in a class +--treenode-filter "/*/*/ClassName/*" + +# Multiple patterns (OR logic) +--treenode-filter "Pattern1|Pattern2" + +# Exclude by category +--treenode-filter "/*/*/*/*[Category!=Performance]" +``` + +--- + +## Project Overview + +### What is TUnit? + +TUnit is a **modern .NET testing framework** that prioritizes: +- **Performance**: Source-generated tests, parallel by default +- **Modern .NET**: Native AOT, trimming, latest C# features +- **Microsoft.Testing.Platform**: Not VSTest (legacy) +- **Developer Experience**: Fluent assertions, minimal boilerplate + +### Key Differentiators +- **Compile-time test discovery** via source generators +- **Parallel execution** by default with dependency management +- **Dual execution modes** (source-gen + reflection) for flexibility +- **Built-in assertions** with detailed failure messages +- **Property-based testing** support +- **Dynamic test variants** for data-driven scenarios + +--- + +## Architecture -### Source Generator Changes +### Execution Modes -**CRITICAL**: When modifying what the source generator emits: +TUnit has two execution paths that **MUST** behave identically: -1. **Run the source generator tests**: +``` +┌─────────────────────────────────────────────────────────────────┐ +│ USER TEST CODE │ +│ [Test] public void MyTest() { ... } │ +└────────────┬────────────────────────────────┬───────────────────┘ + │ │ + ▼ ▼ + ┌────────────────────┐ ┌─────────────────────┐ + │ SOURCE-GENERATED │ │ REFLECTION MODE │ + │ MODE │ │ │ + │ │ │ │ + │ TUnit.Core. │ │ TUnit.Engine │ + │ SourceGenerator │ │ │ + │ │ │ │ + │ Generates code at │ │ Discovers tests at │ + │ compile time │ │ runtime via │ + │ │ │ reflection │ + └─────────┬──────────┘ └──────────┬──────────┘ + │ │ + │ │ + └───────────────┬───────────────┘ + ▼ + ┌──────────────────┐ + │ TUnit.Engine │ + │ (Execution) │ + └──────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Microsoft.Testing│ + │ .Platform │ + └──────────────────┘ +``` + +### Core Projects + +| Project | Purpose | Notes | +|---------|---------|-------| +| **TUnit.Core** | Abstractions, interfaces, attributes | Public API surface | +| **TUnit.Engine** | Test discovery & execution (reflection) | Runtime path | +| **TUnit.Core.SourceGenerator** | Compile-time test generation | Compile-time path | +| **TUnit.Assertions** | Fluent assertion library | Separate from core | +| **TUnit.Assertions.SourceGenerator** | Generates custom assertions | Extensibility | +| **TUnit.Analyzers** | Roslyn analyzers & code fixes | Compile-time safety | +| **TUnit.PropertyTesting** | Property-based testing support | New feature | +| **TUnit.Playwright** | Playwright integration | Browser testing | + +### Roslyn Version Projects +- `*.Roslyn414`, `*.Roslyn44`, `*.Roslyn47`: Multi-targeting for different Roslyn versions +- Ensures compatibility across VS versions and .NET SDK versions + +### Test Projects +- **TUnit.TestProject**: Integration tests (uses TUnit to test itself) +- **TUnit.Engine.Tests**: Engine-specific tests +- **TUnit.Assertions.Tests**: Assertion library tests +- **TUnit.Core.SourceGenerator.Tests**: Source generator snapshot tests +- **TUnit.PublicAPI**: Public API snapshot tests (prevents breaking changes) + +--- + +## Development Workflow + +### Adding a New Feature + +#### Step-by-Step Process + +1. **Design Phase** + ``` + ┌─────────────────────────────────────────────────────┐ + │ Ask yourself: │ + │ • Does this require dual-mode implementation? │ + │ • Will this affect public API? │ + │ • Does this need an analyzer rule? │ + │ • What's the performance impact? │ + │ • Is this AOT/trimming compatible? │ + └─────────────────────────────────────────────────────┘ + ``` + +2. **Implementation** + - **Write tests FIRST** (TDD approach) + - Implement in `TUnit.Core` (if new abstractions needed) + - Implement in `TUnit.Core.SourceGenerator` (source-gen path) + - Implement in `TUnit.Engine` (reflection path) + - Add analyzer rule if misuse is possible + +3. **Verification** ```bash - dotnet test TUnit.Core.SourceGenerator.Tests + # Run all tests + dotnet test + + # If source generator changed, accept snapshots + cd TUnit.Core.SourceGenerator.Tests + dotnet test + # Review .received.txt files + for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done + + # If public API changed, accept snapshots + cd TUnit.PublicAPI + dotnet test + # Review .received.txt files + for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done ``` -2. **Accept any changed snapshots**: - - The test will generate `.received.txt` files showing the new generated output - - Review these files to ensure your changes are intentional and correct - - Convert the received files to verified files: - ```bash - # Windows - for %f in (*.received.txt) do move /Y "%f" "%~nf.verified.txt" - - # Linux/macOS - for file in *.received.txt; do mv "$file" "${file%.received.txt}.verified.txt"; done - ``` - - Or manually rename each `.received.txt` file to `.verified.txt` +4. **Performance Check** + ```bash + # Run benchmarks (if touching hot paths) + cd TUnit.Performance.Tests + dotnet run -c Release --framework net9.0 + ``` -3. **Commit the updated snapshots**: - - Include the updated `.verified.txt` files in your commit - - These snapshots ensure source generation consistency +5. **AOT Verification** (if touching reflection) + ```bash + cd TUnit.TestProject + dotnet publish -c Release -p:PublishAot=true --use-current-runtime + ``` + +### Fixing a Bug + +#### Step-by-Step Process -### Public API Changes +1. **Reproduce** + - Write a failing test that demonstrates the bug + - Identify which execution mode(s) are affected -**CRITICAL**: When modifying any public API in TUnit.Core, TUnit.Engine, or TUnit.Playwright (adding, removing, or changing public methods, properties, or types): +2. **Fix** + - Fix in source generator (if affected) + - Fix in reflection engine (if affected) + - Ensure both modes now pass the test -1. **Run the Public API test**: +3. **Verify No Regression** ```bash - dotnet test TUnit.PublicAPI + # Run full test suite + dotnet test + + # Check performance hasn't regressed + # (if fix is in hot path) ``` -2. **Update the API snapshots**: - - The test will generate `.received.txt` files showing the current API surface - - Review these files to ensure your changes are intentional - - Convert the received files to verified files: - ```bash - # Windows - for %f in (*.received.txt) do move /Y "%f" "%~nf.verified.txt" - - # Linux/macOS - for file in *.received.txt; do mv "$file" "${file%.received.txt}.verified.txt"; done - ``` - - Or manually rename each `.received.txt` file to `.verified.txt` +4. **Accept Snapshots** (if applicable) + - Follow snapshot workflow above + +--- + +## Code Style Standards + +### Modern C# - Required Syntax + +```csharp +// ✅ CORRECT: Use collection expressions (C# 12+) +List items = []; +string[] array = ["a", "b", "c"]; +Dictionary dict = []; + +// ❌ WRONG: Don't use old initialization syntax +List items = new List(); +string[] array = new string[] { "a", "b", "c" }; + +// ✅ CORRECT: Use var when type is obvious +var testName = GetTestName(); +var results = ExecuteTests(); + +// ❌ WRONG: Explicit types for obvious cases +string testName = GetTestName(); +List results = ExecuteTests(); + +// ✅ CORRECT: Always use braces, even for single lines +if (condition) +{ + DoSomething(); +} + +// ❌ WRONG: No braces +if (condition) + DoSomething(); + +// ✅ CORRECT: File-scoped namespaces +namespace TUnit.Core.Features; + +public class MyClass { } + +// ❌ WRONG: Traditional namespace blocks (unless multiple namespaces in file) +namespace TUnit.Core.Features +{ + public class MyClass { } +} + +// ✅ CORRECT: Pattern matching +if (obj is TestContext context) +{ + ProcessContext(context); +} + +// ✅ CORRECT: Switch expressions +var result = status switch +{ + TestStatus.Passed => "✓", + TestStatus.Failed => "✗", + TestStatus.Skipped => "⊘", + _ => "?" +}; + +// ✅ CORRECT: Target-typed new +TestContext context = new(testName, metadata); + +// ✅ CORRECT: Record types for immutable data +public record TestMetadata(string Name, string FilePath, int LineNumber); + +// ✅ CORRECT: Required properties (C# 11+) +public required string TestName { get; init; } + +// ✅ CORRECT: Raw string literals for multi-line strings +string code = """ + public void TestMethod() + { + Assert.That(value).IsEqualTo(expected); + } + """; +``` -3. **Commit the updated snapshots**: - - Include the updated `.verified.txt` files in your commit - - These snapshots track the public API surface and prevent accidental breaking changes +### Naming Conventions -### Compatibility Testing -- Test against multiple .NET versions (.NET 6, 8, 9+) -- Verify AOT and trimming compatibility -- Test source generation in various project configurations +```csharp +// Public members: PascalCase +public string TestName { get; } +public void ExecuteTest() { } +public const int MaxRetries = 3; -## Common Gotchas and Pitfalls +// Private fields: _camelCase +private readonly ITestExecutor _executor; +private string _cachedResult; -1. **Execution Mode Inconsistency**: Always verify behavior matches between modes -2. **Performance Regressions**: Profile code changes in test discovery and execution -3. **AOT/Trimming Issues**: Be careful with reflection usage and dynamic code -4. **Thread Safety**: Ensure thread-safe patterns in concurrent test execution -5. **Memory Leaks**: Properly dispose of resources and avoid circular references +// Local variables: camelCase +var testContext = new TestContext(); +int retryCount = 0; -## Dependencies and Third-Party Libraries +// Type parameters: T prefix for single, descriptive for multiple +public class Repository { } +public class Converter { } -- Minimize external dependencies for core functionality -- Use Microsoft.Extensions.* packages for common functionality -- Prefer .NET BCL types over third-party alternatives -- Keep analyzer dependencies minimal to avoid version conflicts +// Interfaces: I prefix +public interface ITestExecutor { } -## Documentation Standards +// Async methods: Async suffix +public async Task ExecuteTestAsync(CancellationToken ct) { } +``` -- Use triple-slash comments for public APIs -- Include code examples in documentation -- Document performance characteristics for critical APIs -- Maintain README files for major components +### Async/Await Patterns + +```csharp +// ✅ CORRECT: Use ValueTask for potentially sync operations +public ValueTask ExecuteAsync(CancellationToken ct) +{ + if (IsCached) + { + return new ValueTask(cachedResult); + } + + return ExecuteAsyncCore(ct); +} + +// ✅ CORRECT: Always accept CancellationToken +public async Task RunTestAsync(CancellationToken cancellationToken) +{ + await PrepareAsync(cancellationToken); + return await ExecuteAsync(cancellationToken); +} + +// ✅ CORRECT: ConfigureAwait(false) in library code +var result = await ExecuteAsync().ConfigureAwait(false); + +// ❌ WRONG: NEVER block on async code +var result = ExecuteAsync().Result; // DEADLOCK RISK +var result = ExecuteAsync().GetAwaiter().GetResult(); // DEADLOCK RISK +``` -## Questions to Ask When Making Changes +### Nullable Reference Types -1. Does this change affect both execution modes? -2. Is this the most performant approach? -3. Does this follow established patterns in the codebase? -4. Are there any breaking changes or compatibility concerns? -5. How will this behave under high load or with many tests? -6. Does this require new analyzer rules or diagnostics? +```csharp +// ✅ CORRECT: Explicit nullability annotations +public string? TryGetTestName(TestContext context) +{ + return context.Metadata?.Name; +} -## IDE and Tooling +// ✅ CORRECT: Null-forgiving operator when you know it's safe +var testName = context.Metadata!.Name; -- Use EditorConfig settings for consistent formatting -- Leverage Roslyn analyzers for code quality -- Run performance benchmarks for critical changes -- Use the project's specific MSBuild properties and targets +// ✅ CORRECT: Null-coalescing +var name = context.Metadata?.Name ?? "UnknownTest"; -## Critical Reminders for Every Change +// ✅ CORRECT: Required non-nullable properties +public required string TestName { get; init; } +``` -### Before Submitting Any Code: -1. **Test Both Modes**: Verify your changes work identically in both source-generated and reflection modes -2. **Check Source Generator**: If changing source generator output, run `dotnet test TUnit.Core.SourceGenerator.Tests` and accept snapshots -3. **Check Public API**: If modifying TUnit.Core, TUnit.Engine, or TUnit.Playwright public APIs, run `dotnet test TUnit.PublicAPI` and accept snapshots -4. **Performance Impact**: Consider if your change affects test discovery or execution performance -5. **Breaking Changes**: Ensure no unintentional breaking changes to the public API -6. **Documentation**: Update relevant documentation for any public API changes +### Performance-Critical Code + +```csharp +// ✅ CORRECT: Object pooling for frequently allocated objects +private static readonly ObjectPool StringBuilderPool = + ObjectPool.Create(); + +public string BuildMessage() +{ + var builder = StringBuilderPool.Get(); + try + { + builder.Append("Test: "); + builder.Append(TestName); + return builder.ToString(); + } + finally + { + builder.Clear(); + StringBuilderPool.Return(builder); + } +} + +// ✅ CORRECT: Span for stack-allocated buffers +Span buffer = stackalloc char[256]; + +// ✅ CORRECT: Avoid allocations in hot paths +// Cache reflection results +private static readonly MethodInfo ExecuteMethod = + typeof(TestRunner).GetMethod(nameof(Execute))!; + +// ✅ CORRECT: Use static readonly for constant data +private static readonly string[] ReservedNames = ["Test", "Setup", "Cleanup"]; +``` + +### Anti-Patterns to Avoid + +```csharp +// ❌ WRONG: Catching generic exceptions without re-throwing +try { } +catch (Exception) { } // Swallows all errors + +// ✅ CORRECT: Catch specific exceptions or re-throw +try { } +catch (InvalidOperationException ex) +{ + Log(ex); + throw; +} + +// ❌ WRONG: Using Task.Run in library code (pushes threading choice to consumers) +public Task DoWorkAsync() => Task.Run(() => DoWork()); + +// ✅ CORRECT: Properly async all the way +public async Task DoWorkAsync() => await ActualAsyncWork(); + +// ❌ WRONG: Unnecessary LINQ in hot paths +var count = tests.Where(t => t.IsPassed).Count(); + +// ✅ CORRECT: Direct iteration +int count = 0; +foreach (var test in tests) +{ + if (test.IsPassed) count++; +} + +// ❌ WRONG: String concatenation in loops +string result = ""; +foreach (var item in items) +{ + result += item; +} + +// ✅ CORRECT: StringBuilder or collection expressions +var builder = new StringBuilder(); +foreach (var item in items) +{ + builder.Append(item); +} +``` + +--- + +## Testing Guidelines + +### Test Categories + +1. **Unit Tests** (`TUnit.Core.Tests`, `TUnit.UnitTests`) + - Test individual components in isolation + - Fast execution, no external dependencies + - Mock dependencies + +2. **Integration Tests** (`TUnit.TestProject`, `TUnit.Engine.Tests`) + - Test interactions between components + - Use TUnit to test itself (dogfooding) + - Verify dual-mode parity + +3. **Snapshot Tests** (`TUnit.Core.SourceGenerator.Tests`, `TUnit.PublicAPI`) + - Verify source generator output + - Track public API surface + - Prevent unintended breaking changes + +4. **Performance Tests** (`TUnit.Performance.Tests`) + - Benchmark critical paths + - Compare against other frameworks + - Track performance regressions + +### Writing Tests + +```csharp +// ✅ CORRECT: Descriptive test names +[Test] +public async Task ExecuteTest_WhenTestPasses_ReturnsPassedStatus() +{ + // Arrange + var test = CreatePassingTest(); + + // Act + var result = await test.ExecuteAsync(); + + // Assert + await Assert.That(result.Status).IsEqualTo(TestStatus.Passed); +} + +// ✅ CORRECT: Test both execution modes explicitly +[Test] +[Arguments(ExecutionMode.SourceGenerated)] +[Arguments(ExecutionMode.Reflection)] +public async Task MyFeature_WorksInBothModes(ExecutionMode mode) +{ + // Test implementation +} + +// ✅ CORRECT: Use Categories for grouping +[Test] +[Category("Performance")] +public void MyPerformanceTest() { } + +// Run without performance tests: +// dotnet test -- --treenode-filter "/*/*/*/*[Category!=Performance]" +``` -### Quick Commands Reference: +### Snapshot Testing + +```csharp +// In TUnit.Core.SourceGenerator.Tests + +[Test] +public Task GeneratesCorrectCode_ForSimpleTest() +{ + string source = """ + using TUnit.Core; + + public class MyTests + { + [Test] + public void SimpleTest() { } + } + """; + + return VerifySourceGenerator(source); +} + +// This will: +// 1. Run the source generator +// 2. Capture the generated code +// 3. Compare to MyTestName.verified.txt +// 4. Create MyTestName.received.txt if different +// 5. Fail test if difference found +``` + +**Accepting Snapshots:** ```bash -# Run all tests -dotnet test +# After verifying .received.txt files are correct: +cd TUnit.Core.SourceGenerator.Tests +for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done -# Run source generator tests -dotnet test TUnit.Core.SourceGenerator.Tests +# Commit the .verified.txt files +git add *.verified.txt +git commit -m "Update source generator snapshots" +``` -# Run public API tests -dotnet test TUnit.PublicAPI +--- -# Convert received to verified files (Windows) -for %f in (*.received.txt) do move /Y "%f" "%~nf.verified.txt" +## Performance Requirements -# Convert received to verified files (Linux/macOS) -for file in *.received.txt; do mv "$file" "${file%.received.txt}.verified.txt"; done +### Performance Budget -# Run specific test -dotnet test -- --treenode-filter "/Assembly/Namespace/ClassName/TestName" +| Operation | Target | Critical | +|-----------|--------|----------| +| Test discovery | < 100ms per 1000 tests | Hot path | +| Test execution overhead | < 1ms per test | Hot path | +| Source generation | < 1s per 1000 tests | Compile-time | +| Memory per test | < 1KB average | At scale | + +### Hot Paths (Optimize Aggressively) + +1. **Test Discovery** + - Source generator: Generating test registration code + - Reflection engine: Scanning assemblies for test attributes + +2. **Test Execution** + - Test invocation + - Assertion evaluation + - Result collection + +3. **Data Generation** + - Argument expansion + - Data source evaluation + +### Performance Checklist + +``` +┌─────────────────────────────────────────────────────────┐ +│ Before committing changes to hot paths: │ +│ □ Profiled with BenchmarkDotNet │ +│ □ No new allocations in tight loops │ +│ □ Reflection results cached │ +│ □ String operations minimized │ +│ □ LINQ avoided in hot paths (use loops) │ +│ □ ValueTask used for potentially sync operations │ +│ □ Compared before/after performance │ +└─────────────────────────────────────────────────────────┘ ``` -Remember: TUnit aims to be a fast, modern, and reliable testing framework. Every change should contribute to these goals while maintaining the simplicity and developer experience that makes testing enjoyable. +### Performance Patterns + +```csharp +// ✅ CORRECT: Cache reflection results +private static readonly Dictionary TestMethodCache = new(); + +public MethodInfo[] GetTestMethods(Type type) +{ + if (!TestMethodCache.TryGetValue(type, out var methods)) + { + methods = type.GetMethods() + .Where(m => m.GetCustomAttribute() != null) + .ToArray(); + TestMethodCache[type] = methods; + } + return methods; +} + +// ✅ CORRECT: Use spans to avoid allocations +public void ProcessTestName(ReadOnlySpan name) +{ + // Work with span, no string allocation +} + +// ✅ CORRECT: ArrayPool for temporary buffers +var buffer = ArrayPool.Shared.Rent(size); +try +{ + // Use buffer +} +finally +{ + ArrayPool.Shared.Return(buffer); +} +``` + +--- + +## Common Patterns + +### Implementing Dual-Mode Features + +#### Pattern: New Test Lifecycle Hook + +```csharp +// 1. Define in TUnit.Core (abstraction) +namespace TUnit.Core; + +[AttributeUsage(AttributeTargets.Method)] +public class BeforeAllTestsAttribute : Attribute +{ +} + +// 2. Implement in TUnit.Core.SourceGenerator +// Generates code like: +/* +await MyTestClass.GlobalSetup(); +*/ + +// 3. Implement in TUnit.Engine (reflection) +public class ReflectionTestDiscoverer +{ + private async Task DiscoverHooksAsync(Type testClass) + { + var hookMethods = testClass.GetMethods() + .Where(m => m.GetCustomAttribute() != null); + + foreach (var method in hookMethods) + { + RegisterHook(method); + } + } +} + +// 4. Write tests for BOTH modes +[Test] +[Arguments(ExecutionMode.SourceGenerated)] +[Arguments(ExecutionMode.Reflection)] +public async Task BeforeAllTestsHook_ExecutesOnce(ExecutionMode mode) +{ + // Test implementation +} +``` + +### Adding Analyzer Rules + +```csharp +// TUnit.Analyzers/Rules/TestMethodMustBePublic.cs + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class TestMethodMustBePublicAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "TUNIT0001"; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + title: "Test method must be public", + messageFormat: "Test method '{0}' must be public", + category: "Design", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + } + + private void AnalyzeMethod(SymbolAnalysisContext context) + { + var method = (IMethodSymbol)context.Symbol; + + if (method.GetAttributes().Any(a => a.AttributeClass?.Name == "TestAttribute")) + { + if (method.DeclaredAccessibility != Accessibility.Public) + { + context.ReportDiagnostic(Diagnostic.Create( + Rule, + method.Locations[0], + method.Name)); + } + } + } +} +``` + +### Adding Assertions + +```csharp +// TUnit.Assertions/Extensions/NumericAssertions.cs + +public static class NumericAssertions +{ + public static InvokableValueAssertionBuilder IsPositive( + this IValueSource valueSource) + where TActual : IComparable + { + return valueSource.RegisterAssertion( + new DelegateAssertion( + (value, _, _) => + { + if (value.CompareTo(default!) <= 0) + { + return AssertionResult.Failed($"Expected positive value but was {value}"); + } + return AssertionResult.Passed; + }, + (actual, expected) => $"{actual} is positive")); + } +} + +// Usage: +await Assert.That(value).IsPositive(); +``` + +--- + +## Troubleshooting + +### Snapshot Tests Failing + +**Problem**: `TUnit.Core.SourceGenerator.Tests` or `TUnit.PublicAPI` failing with "Snapshots don't match" + +**Solution**: +```bash +# 1. Review the .received.txt files to see what changed +cd TUnit.Core.SourceGenerator.Tests # or TUnit.PublicAPI +ls *.received.txt + +# 2. If changes are intentional (you modified the generator or public API): +for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done + +# 3. Commit the updated .verified.txt files +git add *.verified.txt +git commit -m "Update snapshots after [your change]" + +# 4. NEVER commit .received.txt files +git status # Ensure no .received.txt files are staged +``` + +### Tests Pass Locally But Fail in CI + +**Common Causes**: +1. **Snapshot mismatch**: Forgot to commit `.verified.txt` files +2. **Platform differences**: Line ending issues (CRLF vs LF) +3. **Timing issues**: Race conditions in parallel tests +4. **Environment differences**: Missing dependencies + +**Solution**: +```bash +# Check for uncommitted snapshots +git status | grep verified.txt + +# Check line endings +git config core.autocrlf # Should be consistent + +# Run tests with same parallelization as CI +dotnet test --parallel +``` + +### Dual-Mode Behavior Differs + +**Problem**: Test passes in source-generated mode but fails in reflection mode (or vice versa) + +**Diagnostic Process**: +```bash +# 1. Run test in specific mode +dotnet test -- --treenode-filter "/*/*/*/YourTest*" + +# 2. Check generated code +# Look in obj/Debug/net9.0/generated/TUnit.Core.SourceGenerator/ + +# 3. Debug reflection path +# Set breakpoint in TUnit.Engine code + +# 4. Common issues: +# - Attribute not checked in reflection path +# - Different data expansion logic +# - Missing hook invocation +# - Incorrect test metadata +``` + +**Solution**: Implement missing logic in the other execution mode + +### AOT Compilation Fails + +**Problem**: `dotnet publish -p:PublishAot=true` fails + +**Common Causes**: +1. Dynamic code generation (not supported in AOT) +2. Reflection without proper annotations +3. Missing `[DynamicallyAccessedMembers]` attributes + +**Solution**: +```csharp +// ✅ CORRECT: Annotate methods that use reflection +public void DiscoverTests( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + Type testClass) +{ + var methods = testClass.GetMethods(); // Safe - annotated +} + +// ✅ CORRECT: Suppress warnings when you know it's safe +[UnconditionalSuppressMessage("Trimming", "IL2070", + Justification = "Test methods are preserved by source generator")] +public void InvokeTestMethod(MethodInfo method) { } +``` + +### Performance Regression + +**Problem**: Tests run slower after changes + +**Diagnostic**: +```bash +# Run performance benchmarks +cd TUnit.Performance.Tests +dotnet run -c Release --framework net9.0 + +# Profile with dotnet-trace +dotnet trace collect -- dotnet test + +# Analyze with PerfView or similar +``` + +**Common Causes**: +- Added LINQ in hot path (use loops instead) +- Missing caching of reflection results +- Unnecessary allocations (use object pooling) +- Synchronous blocking on async code + +--- + +## Pre-Commit Checklist + +Before committing ANY code, verify: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ □ All tests pass: dotnet test │ +│ □ If source generator changed: │ +│ • Ran TUnit.Core.SourceGenerator.Tests │ +│ • Reviewed and accepted snapshots (.verified.txt) │ +│ • Committed .verified.txt files │ +│ □ If public API changed: │ +│ • Ran TUnit.PublicAPI tests │ +│ • Reviewed and accepted snapshots │ +│ • Committed .verified.txt files │ +│ □ If dual-mode feature: │ +│ • Implemented in BOTH source-gen and reflection │ +│ • Tested both modes explicitly │ +│ • Verified identical behavior │ +│ □ If performance-critical: │ +│ • Profiled before and after │ +│ • No performance regression │ +│ • Minimized allocations │ +│ □ If touching reflection: │ +│ • Tested with AOT: dotnet publish -p:PublishAot=true │ +│ • Added proper DynamicallyAccessedMembers annotations │ +│ □ Code follows style guide │ +│ □ No breaking changes (or documented if unavoidable) │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Additional Resources + +- **Documentation**: https://tunit.dev +- **Contributing Guide**: `.github/CONTRIBUTING.md` +- **Issues**: https://github.com/thomhurst/TUnit/issues +- **Discussions**: https://github.com/thomhurst/TUnit/discussions + +--- + +## Philosophy + +TUnit aims to be: **fast, modern, reliable, and enjoyable to use**. + +Every change should advance these goals: +- **Fast**: Optimize for performance. Millions of tests depend on it. +- **Modern**: Leverage latest .NET features. Support AOT, trimming, latest C#. +- **Reliable**: Dual-mode parity. Comprehensive tests. No breaking changes without major version bump. +- **Enjoyable**: Great error messages. Intuitive API. Minimal boilerplate. + +When in doubt, ask: "Does this make TUnit faster, more modern, more reliable, or more enjoyable to use?" + +If the answer is no, reconsider the change. diff --git a/CLAUDE.md b/CLAUDE.md index fa287251a1..fd4913434d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,115 +1,677 @@ # TUnit Development Guide for LLM Agents -## MANDATORY RULES - ALWAYS FOLLOW -1. **Dual-mode implementation required**: ALL changes must work identically in both source-generated and reflection modes -2. **Snapshot tests are critical**: After ANY change to source generator or public APIs, you MUST run and accept snapshots -3. **Never use VSTest**: This project uses Microsoft.Testing.Platform exclusively -4. **Performance first**: Optimize for speed - this framework is used by millions - -## When You Make Changes - -### If you modify source generator code: -1. Run: `dotnet test TUnit.Core.SourceGenerator.Tests` -2. If snapshots differ, rename ALL `*.received.txt` files to `*.verified.txt` -3. Commit the updated `.verified.txt` files - -### If you modify public APIs: -1. Run: `dotnet test TUnit.PublicAPI` -2. If snapshots differ, rename ALL `*.received.txt` files to `*.verified.txt` -3. Commit the updated `.verified.txt` files - -### If you add a feature: -1. Implement in BOTH `TUnit.Core.SourceGenerator` AND `TUnit.Engine` (reflection path) -2. Verify identical behavior in both modes -3. Add tests covering both execution paths -4. Consider if an analyzer rule would help prevent misuse -5. Test performance impact - -### If you fix a bug: -1. Write a failing test first -2. Fix in BOTH execution modes (source-gen and reflection) -3. Verify no performance regression - -## Project Structure -- `TUnit.Core`: Abstractions, interfaces, attributes -- `TUnit.Engine`: Test discovery and execution (reflection mode) -- `TUnit.Core.SourceGenerator`: Compile-time code generation (source-gen mode) -- `TUnit.Assertions`: Fluent assertion library -- `TUnit.Analyzers`: Roslyn analyzers for compile-time validation - -## Code Style (REQUIRED) -```csharp -// Modern C# syntax - always use -List list = []; // Collection expressions -var result = GetValue(); // var for obvious types +> **Purpose**: This file contains essential instructions for AI assistants (Claude, Copilot, etc.) working on TUnit. +> **Audience**: LLM agents, AI coding assistants +> **Optimization**: Structured for rapid parsing, clear decision trees, explicit requirements -// Always use braces -if (condition) +--- + +## 🚨 MANDATORY RULES - ALWAYS FOLLOW + +### Rule 1: Dual-Mode Implementation (CRITICAL) + +**REQUIREMENT**: ALL changes must work identically in both execution modes. + +``` +User Test Code + │ + ├─► SOURCE-GENERATED MODE (TUnit.Core.SourceGenerator) + │ └─► Compile-time code generation + │ + └─► REFLECTION MODE (TUnit.Engine) + └─► Runtime test discovery + + Both modes MUST produce identical behavior +``` + +**Implementation Checklist**: +- [ ] Feature implemented in `TUnit.Core.SourceGenerator` (source-gen path) +- [ ] Feature implemented in `TUnit.Engine` (reflection path) +- [ ] Tests written for both modes +- [ ] Verified identical behavior in both modes + +**If you implement only ONE mode, the feature is INCOMPLETE and MUST NOT be committed.** + +--- + +### Rule 2: Snapshot Testing (NON-NEGOTIABLE) + +**TRIGGER CONDITIONS**: +1. ANY change to source generator output +2. ANY change to public APIs (TUnit.Core, TUnit.Engine, TUnit.Assertions) + +**WORKFLOW**: +```bash +# Source Generator Changes: +dotnet test TUnit.Core.SourceGenerator.Tests +# Review .received.txt files +for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done # Linux/macOS +for %f in (*.received.txt) do move /Y "%f" "%~nf.verified.txt" # Windows + +# Public API Changes: +dotnet test TUnit.PublicAPI +# Review .received.txt files +for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done # Linux/macOS +for %f in (*.received.txt) do move /Y "%f" "%~nf.verified.txt" # Windows + +# Commit .verified.txt files (NEVER commit .received.txt) +git add *.verified.txt +git commit -m "Update snapshots: [reason]" +``` + +**REMEMBER**: Snapshots are the source of truth. Failing to update them breaks CI. + +--- + +### Rule 3: No VSTest (ABSOLUTE) + +- ✅ **USE**: `Microsoft.Testing.Platform` +- ❌ **NEVER**: `Microsoft.VisualStudio.TestPlatform` (VSTest - legacy, incompatible) + +If you see VSTest references in new code, **STOP** and use Microsoft.Testing.Platform instead. + +--- + +### Rule 4: Performance First + +**Context**: TUnit processes millions of tests daily. Performance is not optional. + +**Requirements**: +- Minimize allocations in hot paths (test discovery, execution) +- Cache reflection results +- Use `ValueTask` for potentially-sync operations +- Profile before/after for changes in critical paths +- Use object pooling for frequent allocations + +**Hot Paths** (profile these): +1. Test discovery (source generation + reflection scanning) +2. Test execution (invocation, assertions, result collection) +3. Data generation (argument expansion, data sources) + +--- + +### Rule 5: AOT/Trimming Compatibility + +**Requirement**: All code must work with Native AOT and IL trimming. + +**Guidelines**: +```csharp +// ✅ CORRECT: Annotate reflection usage +public void DiscoverTests( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] + Type testClass) { - DoSomething(); + var methods = testClass.GetMethods(); } -// Naming -public string PublicField; // PascalCase -private string _privateField; // _camelCase +// ✅ CORRECT: Suppress warnings when safe +[UnconditionalSuppressMessage("Trimming", "IL2070", + Justification = "Test methods preserved by source generator")] +public void InvokeTest(MethodInfo method) { } +``` -// Async -async ValueTask DoWorkAsync(CancellationToken cancellationToken) // ValueTask when possibly sync +**Verification**: +```bash +cd TUnit.TestProject +dotnet publish -c Release -p:PublishAot=true --use-current-runtime ``` -## Performance Guidelines -- Minimize allocations in hot paths (discovery/execution) -- Use object pooling for frequent allocations -- Cache reflection results -- Benchmark critical paths before/after changes +--- + +## 📋 Quick Reference Card + +### Most Common Commands -## Common Commands ```bash -# Test everything +# Run all tests dotnet test -# Test source generator specifically +# Test source generator + accept snapshots dotnet test TUnit.Core.SourceGenerator.Tests +for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done -# Test public API surface +# Test public API + accept snapshots dotnet test TUnit.PublicAPI +for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done -# Accept snapshots (Windows - use this after verifying diffs are correct) -for %f in (*.received.txt) do move /Y "%f" "%~nf.verified.txt" - -# Run specific test by filter +# Run specific test dotnet test -- --treenode-filter "/Assembly/Namespace/ClassName/TestName" + +# Exclude performance tests +dotnet test -- --treenode-filter "/*/*/*/*[Category!=Performance]" + +# Build release +dotnet build -c Release + +# Test AOT +dotnet publish -c Release -p:PublishAot=true +``` + +### Test Filter Syntax + +```bash +# Single test +--treenode-filter "/TUnit.TestProject/Namespace/ClassName/TestMethodName" + +# All tests in a class +--treenode-filter "/*/*/ClassName/*" + +# Multiple patterns (OR) +--treenode-filter "Pattern1|Pattern2" + +# Exclude by category +--treenode-filter "/*/*/*/*[Category!=Performance]" +``` + +--- + +## 🏗️ Project Structure + +### Core Projects + +| Project | Purpose | Key Responsibility | +|---------|---------|-------------------| +| `TUnit.Core` | Abstractions, attributes, interfaces | Public API surface | +| `TUnit.Engine` | Test discovery & execution | **Reflection mode** | +| `TUnit.Core.SourceGenerator` | Compile-time test generation | **Source-gen mode** | +| `TUnit.Assertions` | Fluent assertion library | Separate from core | +| `TUnit.Assertions.SourceGenerator` | Custom assertion generation | Extensibility | +| `TUnit.Analyzers` | Roslyn analyzers & code fixes | Compile-time safety | +| `TUnit.PropertyTesting` | Property-based testing | New feature | +| `TUnit.Playwright` | Browser testing integration | Playwright wrapper | + +### Test Projects + +| Project | Purpose | +|---------|---------| +| `TUnit.TestProject` | Integration tests (dogfooding) | +| `TUnit.Engine.Tests` | Engine-specific tests | +| `TUnit.Assertions.Tests` | Assertion library tests | +| `TUnit.Core.SourceGenerator.Tests` | **Snapshot tests for source generator** | +| `TUnit.PublicAPI` | **Snapshot tests for public API** | + +### Roslyn Version Projects + +- `*.Roslyn414`, `*.Roslyn44`, `*.Roslyn47`: Multi-targeting for Roslyn API versions +- Ensures compatibility across Visual Studio and .NET SDK versions + +--- + +## 💻 Code Style (REQUIRED) + +### Modern C# Syntax (Mandatory) + +```csharp +// ✅ Collection expressions (C# 12+) +List items = []; +string[] array = ["a", "b", "c"]; + +// ❌ WRONG: Old syntax +List items = new List(); + +// ✅ var for obvious types +var testName = GetTestName(); + +// ✅ Always use braces +if (condition) +{ + DoSomething(); +} + +// ❌ WRONG: No braces +if (condition) + DoSomething(); + +// ✅ File-scoped namespaces +namespace TUnit.Core.Features; + +public class MyClass { } + +// ✅ Pattern matching +if (obj is TestContext context) +{ + ProcessContext(context); +} + +// ✅ Switch expressions +var result = status switch +{ + TestStatus.Passed => "✓", + TestStatus.Failed => "✗", + _ => "?" +}; + +// ✅ Raw string literals +string code = """ + public void Test() { } + """; +``` + +### Naming Conventions + +```csharp +// Public: PascalCase +public string TestName { get; } + +// Private fields: _camelCase +private readonly IExecutor _executor; + +// Local: camelCase +var testContext = new TestContext(); + +// Async: Async suffix +public async Task ExecuteTestAsync(CancellationToken ct) { } +``` + +### Async Patterns + +```csharp +// ✅ ValueTask for potentially-sync operations +public ValueTask ExecuteAsync(CancellationToken ct) +{ + if (IsCached) + return new ValueTask(cachedResult); + + return ExecuteAsyncCore(ct); +} + +// ✅ Always accept CancellationToken +public async Task RunAsync(CancellationToken cancellationToken) { } + +// ❌ NEVER block on async +var result = ExecuteAsync().Result; // DEADLOCK RISK +var result = ExecuteAsync().GetAwaiter().GetResult(); // DEADLOCK RISK +``` + +### Performance Patterns + +```csharp +// ✅ Cache reflection results +private static readonly Dictionary TestMethodCache = new(); + +// ✅ Object pooling +private static readonly ObjectPool StringBuilderPool = + ObjectPool.Create(); + +// ✅ Span to avoid allocations +public void ProcessTestName(ReadOnlySpan name) { } + +// ✅ ArrayPool for temporary buffers +var buffer = ArrayPool.Shared.Rent(size); +try { /* use buffer */ } +finally { ArrayPool.Shared.Return(buffer); } +``` + +### Anti-Patterns (NEVER DO THIS) + +```csharp +// ❌ Catching all exceptions without re-throw +try { } catch (Exception) { } // Swallows errors + +// ❌ LINQ in hot paths +var count = tests.Where(t => t.IsPassed).Count(); + +// ✅ Use loops instead +int count = 0; +foreach (var test in tests) + if (test.IsPassed) count++; + +// ❌ String concatenation in loops +string result = ""; +foreach (var item in items) result += item; + +// ✅ StringBuilder +var builder = new StringBuilder(); +foreach (var item in items) builder.Append(item); +``` + +--- + +## 🔄 Development Workflows + +### Adding a New Feature + +**Decision Tree**: +``` +Is this a new feature? + ├─► YES + │ ├─► Does it require dual-mode implementation? + │ │ ├─► YES: Implement in BOTH source-gen AND reflection + │ │ └─► NO: Still verify both modes aren't affected + │ │ + │ ├─► Does it change public API? + │ │ └─► YES: Run TUnit.PublicAPI tests + accept snapshots + │ │ + │ ├─► Does it change source generator output? + │ │ └─► YES: Run TUnit.Core.SourceGenerator.Tests + accept snapshots + │ │ + │ ├─► Does it touch hot paths? + │ │ └─► YES: Profile before/after, benchmark + │ │ + │ └─► Does it use reflection? + │ └─► YES: Test with AOT (dotnet publish -p:PublishAot=true) + │ + └─► NO: (Continue to bug fix workflow) +``` + +**Step-by-Step**: +1. **Write tests FIRST** (TDD) +2. Implement in `TUnit.Core` (if new abstractions needed) +3. Implement in `TUnit.Core.SourceGenerator` (source-gen path) +4. Implement in `TUnit.Engine` (reflection path) +5. Add analyzer rule (if misuse is possible) +6. Run all tests: `dotnet test` +7. Accept snapshots if needed (see Rule 2) +8. Benchmark if touching hot paths +9. Test AOT if using reflection + +### Fixing a Bug + +**Step-by-Step**: +1. **Write failing test** that reproduces the bug +2. Identify affected execution mode(s) +3. Fix in source generator (if affected) +4. Fix in reflection engine (if affected) +5. Verify both modes pass the test +6. Run full test suite: `dotnet test` +7. Accept snapshots if applicable +8. Check for performance regression (if in hot path) + +--- + +## 🎯 Common Patterns + +### Implementing Dual-Mode Feature + +```csharp +// 1. Define abstraction in TUnit.Core +[AttributeUsage(AttributeTargets.Method)] +public class BeforeAllTestsAttribute : Attribute { } + +// 2. Implement in TUnit.Core.SourceGenerator +// Generated code: +// await MyTestClass.GlobalSetup(); + +// 3. Implement in TUnit.Engine (reflection) +public class ReflectionTestDiscoverer +{ + private async Task DiscoverHooksAsync(Type testClass) + { + var hookMethods = testClass.GetMethods() + .Where(m => m.GetCustomAttribute() != null); + + foreach (var method in hookMethods) + RegisterHook(method); + } +} + +// 4. Test BOTH modes +[Test] +[Arguments(ExecutionMode.SourceGenerated)] +[Arguments(ExecutionMode.Reflection)] +public async Task BeforeAllTestsHook_ExecutesOnce(ExecutionMode mode) { } +``` + +### Adding Analyzer Rule + +```csharp +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class TestMethodMustBePublicAnalyzer : DiagnosticAnalyzer +{ + public const string DiagnosticId = "TUNIT0001"; + + private static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + title: "Test method must be public", + messageFormat: "Test method '{0}' must be public", + category: "Design", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + } + + private void AnalyzeMethod(SymbolAnalysisContext context) + { + var method = (IMethodSymbol)context.Symbol; + + if (method.GetAttributes().Any(a => a.AttributeClass?.Name == "TestAttribute")) + { + if (method.DeclaredAccessibility != Accessibility.Public) + { + context.ReportDiagnostic(Diagnostic.Create( + Rule, method.Locations[0], method.Name)); + } + } + } +} +``` + +### Adding Assertion + +```csharp +public static class NumericAssertions +{ + public static InvokableValueAssertionBuilder IsPositive( + this IValueSource valueSource) + where TActual : IComparable + { + return valueSource.RegisterAssertion( + new DelegateAssertion( + (value, _, _) => + { + if (value.CompareTo(default!) <= 0) + return AssertionResult.Failed($"Expected positive value but was {value}"); + return AssertionResult.Passed; + }, + (actual, expected) => $"{actual} is positive")); + } +} + +// Usage: +await Assert.That(value).IsPositive(); +``` + +--- + +## 🐛 Troubleshooting + +### Snapshot Tests Failing + +**Symptom**: `TUnit.Core.SourceGenerator.Tests` or `TUnit.PublicAPI` failing + +**Solution**: +```bash +# 1. Review .received.txt files +cd TUnit.Core.SourceGenerator.Tests # or TUnit.PublicAPI +ls *.received.txt + +# 2. If changes are intentional: +for f in *.received.txt; do mv "$f" "${f%.received.txt}.verified.txt"; done + +# 3. Commit .verified.txt files +git add *.verified.txt +git commit -m "Update snapshots: [reason]" + +# 4. NEVER commit .received.txt +git status # Verify no .received.txt staged +``` + +### Tests Pass Locally, Fail in CI + +**Common Causes**: +1. Forgot to commit `.verified.txt` files +2. Line ending differences (CRLF vs LF) +3. Race conditions in parallel tests +4. Missing dependencies + +**Solution**: +```bash +# Check for uncommitted snapshots +git status | grep verified.txt + +# Check line endings +git config core.autocrlf + +# Run with same parallelization as CI +dotnet test --parallel +``` + +### Dual-Mode Behavior Differs + +**Diagnostic**: +```bash +# Check generated code +# obj/Debug/net9.0/generated/TUnit.Core.SourceGenerator/ + +# Set breakpoint in TUnit.Engine for reflection path + +# Common issues: +# - Attribute not checked in reflection +# - Different data expansion logic +# - Missing hook invocation +``` + +**Solution**: Implement missing logic in other execution mode + +### AOT Compilation Fails + +**Common Causes**: +1. Dynamic code generation (not AOT-compatible) +2. Reflection without proper annotations +3. Missing `[DynamicallyAccessedMembers]` + +**Solution**: Add proper annotations (see Rule 5 above) + +### Performance Regression + +**Diagnostic**: +```bash +# Benchmark +cd TUnit.Performance.Tests +dotnet run -c Release --framework net9.0 + +# Profile +dotnet trace collect -- dotnet test ``` -## AOT/Trimming Compatibility -- Use `[UnconditionalSuppressMessage]` for known-safe reflection -- Test with trimming/AOT enabled projects -- Avoid dynamic code generation at runtime (use source generators instead) - -## Threading and Safety -- All test execution must be thread-safe -- Use proper synchronization for shared state -- Dispose resources correctly (implement IDisposable/IAsyncDisposable) - -## Critical Mistakes to Avoid -1. ❌ Implementing a feature only in source-gen mode (must do BOTH) -2. ❌ Breaking change to public API without major version bump -3. ❌ Forgetting to accept snapshots after intentional generator changes -4. ❌ Performance regression in discovery or execution -5. ❌ Using reflection in ways incompatible with AOT/trimming - -## Verification Checklist -Before completing any task, verify: -- [ ] Works in both source-generated and reflection modes -- [ ] Snapshots accepted if generator/API changed -- [ ] Tests added and passing -- [ ] No performance regression -- [ ] AOT/trimming compatible -- [ ] Thread-safe if touching concurrent code - -## Target Frameworks -- .NET Standard 2.0 (library compatibility) -- .NET 6, 8, 9+ (current support) - -## Philosophy -TUnit aims to be: **fast, modern, reliable, and enjoyable to use**. Every change should advance these goals. \ No newline at end of file +**Common Causes**: +- LINQ in hot path → use loops +- Missing reflection cache +- Unnecessary allocations → use object pooling +- Blocking on async + +--- + +## ✅ Pre-Commit Checklist + +**Before committing, verify ALL items**: + +``` +┌─────────────────────────────────────────────────────────┐ +│ □ All tests pass: dotnet test │ +│ │ +│ □ If source generator changed: │ +│ • Ran TUnit.Core.SourceGenerator.Tests │ +│ • Reviewed .received.txt files │ +│ • Accepted snapshots (.verified.txt) │ +│ • Committed .verified.txt files │ +│ │ +│ □ If public API changed: │ +│ • Ran TUnit.PublicAPI tests │ +│ • Reviewed .received.txt files │ +│ • Accepted snapshots (.verified.txt) │ +│ • Committed .verified.txt files │ +│ │ +│ □ If dual-mode feature: │ +│ • Implemented in BOTH source-gen AND reflection │ +│ • Tested both modes explicitly │ +│ • Verified identical behavior │ +│ │ +│ □ If performance-critical: │ +│ • Profiled before/after │ +│ • No performance regression │ +│ • Minimized allocations │ +│ │ +│ □ If touching reflection: │ +│ • Tested AOT: dotnet publish -p:PublishAot=true │ +│ • Added DynamicallyAccessedMembers annotations │ +│ │ +│ □ Code follows style guide │ +│ □ No breaking changes (or major version bump) │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 🎓 Philosophy + +**TUnit Core Principles**: + +1. **Fast**: Performance is not optional. Millions of tests depend on it. +2. **Modern**: Latest .NET features, AOT support, C# 12+ syntax. +3. **Reliable**: Dual-mode parity, comprehensive tests, API stability. +4. **Enjoyable**: Great error messages, intuitive API, minimal boilerplate. + +**Decision Framework**: + +When considering any change, ask: + +> "Does this make TUnit faster, more modern, more reliable, or more enjoyable to use?" + +If the answer is **NO** → reconsider the change. + +--- + +## 📚 Additional Resources + +- **Documentation**: https://tunit.dev +- **Contributing**: `.github/CONTRIBUTING.md` +- **Issues**: https://github.com/thomhurst/TUnit/issues +- **Discussions**: https://github.com/thomhurst/TUnit/discussions +- **Detailed Guide**: `.github/copilot-instructions.md` + +--- + +## 🤖 LLM-Specific Notes + +**For AI Assistants**: + +1. **Always check Rule 1-5 first** before making changes +2. **Use Quick Reference Card** for common commands +3. **Follow Decision Trees** in Development Workflows section +4. **Consult Common Patterns** for implementation templates +5. **Run Pre-Commit Checklist** before suggesting changes to commit + +**High-Level Decision Process**: +``` +User Request + │ + ├─► Identify category: feature, bug, refactor + │ + ├─► Check if dual-mode implementation needed + │ + ├─► Check if snapshots need updating + │ + ├─► Implement with required style/patterns + │ + ├─► Run tests + accept snapshots + │ + └─► Verify pre-commit checklist +``` + +**Common Mistakes to Avoid**: +1. ❌ Implementing only one execution mode +2. ❌ Forgetting to update snapshots +3. ❌ Using old C# syntax +4. ❌ Adding allocations in hot paths +5. ❌ Using VSTest APIs +6. ❌ Blocking on async code +7. ❌ Committing .received.txt files + +--- + +**Last Updated**: 2025-01-28 +**Version**: 2.0 (LLM-optimized) diff --git a/Examples/CleanArchitectureExample.cs b/Examples/CleanArchitectureExample.cs deleted file mode 100644 index a940c75322..0000000000 --- a/Examples/CleanArchitectureExample.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System; -using System.Collections.Generic; -using TUnit.Core; -using TUnit.Assertions; - -namespace TUnit.Examples; - -/// -/// Example demonstrating TUnit's clean architecture where: -/// - Source generator only emits TestMetadata (data structures) -/// - TestBuilder handles all complex runtime logic -/// -public class CleanArchitectureExample -{ - /// - /// Simple test - discovered by TestMetadataGenerator at compile time - /// - [Test] - public async Task SimpleTest() - { - // The source generator found this test and created TestMetadata for it - // TestBuilder expands it into a TestDefinition at runtime - var result = 2 + 2; - await Assert.That(result).IsEqualTo(4); - } - - /// - /// Data-driven test with inline arguments - /// - [Test] - [Arguments(1, 1, 2)] - [Arguments(2, 3, 5)] - [Arguments(10, -5, 5)] - public async Task ParameterizedTest(int a, int b, int expected) - { - // Source generator creates TestMetadata with InlineDataSourceProvider - // TestBuilder enumerates the data source and creates 3 test instances - var sum = a + b; - await Assert.That(sum).IsEqualTo(expected); - } - - /// - /// Data-driven test with method data source - /// - [Test] - [MethodDataSource(nameof(GetTestCases))] - public async Task MethodDataSourceTest(string input, int expectedLength) - { - // Source generator creates TestMetadata with MethodDataSourceProvider - // TestBuilder invokes GetTestCases() at runtime and creates test instances - await Assert.That(input.Length).IsEqualTo(expectedLength); - } - - public static IEnumerable<(string, int)> GetTestCases() - { - yield return ("hello", 5); - yield return ("world", 5); - yield return ("TUnit rocks!", 12); - } - - /// - /// Test with repeat functionality - /// - [Test] - [Repeat(3)] - public async Task RepeatedTest() - { - // Source generator sets RepeatCount = 3 in TestMetadata - // TestBuilder creates 3 instances of this test - var random = new Random(); - var value = random.Next(1, 100); - await Assert.That(value).IsGreaterThan(0).And.IsLessThanOrEqualTo(100); - } -} - -/// -/// Example with class-level data source and constructor injection -/// -[ClassDataSource(typeof(UserTestData))] -public class UserServiceTests -{ - private readonly User _testUser; - private readonly UserService _service; - - public UserServiceTests(User testUser) - { - // Source generator creates TestMetadata with ClassDataSources - // TestBuilder enumerates UserTestData and injects each user - _testUser = testUser; - _service = new UserService(); - } - - [Test] - public async Task ValidateUser_ShouldPass() - { - var isValid = _service.Validate(_testUser); - await Assert.That(isValid).IsTrue(); - } - - [Test] - public async Task GetUserAge_ShouldBePositive() - { - var age = _service.CalculateAge(_testUser); - await Assert.That(age).IsGreaterThanOrEqualTo(0); - } -} - -/// -/// Example with property injection -/// -public class PropertyInjectionExample -{ - [Arguments("Development")] - [Arguments("Staging")] - [Arguments("Production")] - public string Environment { get; set; } = ""; - - [Test] - public async Task EnvironmentSpecificTest() - { - // Source generator creates TestMetadata with PropertyDataSources - // TestBuilder injects the property value before test execution - await Assert.That(Environment).IsNotNullOrEmpty(); - - var config = new ConfigService(Environment); - var connectionString = config.GetConnectionString(); - - await Assert.That(connectionString).Contains(Environment); - } -} - -/// -/// Complex example showing how TestBuilder handles tuple unwrapping -/// -public class TupleUnwrappingExample -{ - [Test] - [MethodDataSource(nameof(GetComplexTestData))] - public async Task ComplexDataTest(int id, string name, DateTime birthDate, bool isActive) - { - // Source generator sees the method returns tuples - // TestBuilder unwraps the tuple into individual parameters at runtime - await Assert.That(id).IsGreaterThan(0); - await Assert.That(name).IsNotNullOrEmpty(); - await Assert.That(birthDate).IsLessThan(DateTime.Now); - await Assert.That(isActive).IsNotNull(); - } - - public static IEnumerable<(int, string, DateTime, bool)> GetComplexTestData() - { - yield return (1, "Alice", new DateTime(1990, 1, 1), true); - yield return (2, "Bob", new DateTime(1985, 6, 15), false); - yield return (3, "Charlie", new DateTime(2000, 12, 31), true); - } -} - -// Supporting classes for examples -public class User -{ - public string Name { get; set; } = ""; - public DateTime BirthDate { get; set; } -} - -public class UserService -{ - public bool Validate(User user) => !string.IsNullOrEmpty(user.Name); - public int CalculateAge(User user) => DateTime.Now.Year - user.BirthDate.Year; -} - -public class UserTestData : IEnumerable -{ - public IEnumerator GetEnumerator() - { - yield return new User { Name = "Test User 1", BirthDate = new DateTime(1990, 1, 1) }; - yield return new User { Name = "Test User 2", BirthDate = new DateTime(2000, 6, 15) }; - } - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); -} - -public class ConfigService -{ - private readonly string _environment; - - public ConfigService(string environment) => _environment = environment; - - public string GetConnectionString() => $"Server=db.{_environment.ToLower()}.example.com;Database=MyApp"; -} - -/// -/// Summary of the clean architecture: -/// -/// 1. Source Generation Phase (Compile Time): -/// - TestMetadataGenerator scans for [Test] attributes -/// - Emits only TestMetadata data structures -/// - No complex logic or execution code generated -/// -/// 2. Runtime Phase: -/// - TestSourceRegistrar registers TestMetadata -/// - TestBuilder expands metadata into executable tests -/// - Handles all complex logic: -/// * Data source enumeration -/// * Tuple unwrapping -/// * Property injection -/// * Constructor parameter resolution -/// * Test instance creation -/// -/// Benefits: -/// - Simpler source generator (easier to maintain) -/// - Better debugging (step through actual code, not generated strings) -/// - Improved performance (expression compilation, caching) -/// - Clear separation of concerns -/// \ No newline at end of file diff --git a/Examples/GenericTestExamples.cs b/Examples/GenericTestExamples.cs deleted file mode 100644 index 5ac9696f2e..0000000000 --- a/Examples/GenericTestExamples.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using TUnit.Core; -using TUnit.Assertions; - -namespace TUnit.Examples; - -/// -/// Examples demonstrating generic test resolution for AOT scenarios -/// -public class GenericTestExamples -{ - /// - /// Generic test class that needs explicit type instantiation for AOT - /// - [GenerateGenericTest(typeof(int))] - [GenerateGenericTest(typeof(string))] - [GenerateGenericTest(typeof(DateTime))] - public class GenericRepositoryTests where T : IComparable - { - private readonly List _items = new(); - - [Test] - public async Task CanAddAndRetrieveItem(T item) - { - // Arrange & Act - _items.Add(item); - - // Assert - await Assert.That(_items).Contains(item); - await Assert.That(_items.Count).IsEqualTo(1); - } - - [Test] - public void CanSortItems() - { - // This test will be generated for int, string, and DateTime - _items.Sort(); - - // Verify sorting doesn't throw - Assert.That(_items).IsNotNull(); - } - } - - /// - /// Generic method with explicit type generation - /// - public class GenericMethodTests - { - [Test] - [GenerateGenericTest(typeof(int), typeof(string))] - [GenerateGenericTest(typeof(double), typeof(decimal))] - public async Task GenericSwap(T1 first, T2 second) - { - // Simple test to verify generic method generation - var tuple = (first, second); - var swapped = (tuple.Item2, tuple.Item1); - - await Assert.That(swapped.Item1).IsEqualTo(second); - await Assert.That(swapped.Item2).IsEqualTo(first); - } - - [Test] - [GenerateGenericTest(typeof(List))] - [GenerateGenericTest(typeof(Dictionary))] - public void ComplexGenericTypes() where T : new() - { - var instance = new T(); - Assert.That(instance).IsNotNull(); - } - } - - /// - /// Example with generic constraints - /// - public interface IEntity - { - int Id { get; } - } - - public class User : IEntity - { - public int Id { get; set; } - public string Name { get; set; } = ""; - } - - public class Product : IEntity - { - public int Id { get; set; } - public decimal Price { get; set; } - } - - [GenerateGenericTest(typeof(User))] - [GenerateGenericTest(typeof(Product))] - public class EntityServiceTests where TEntity : IEntity, new() - { - private readonly List _entities = new(); - - [Test] - public async Task CanCreateEntity() - { - // Arrange - var entity = new TEntity(); - - // Act - _entities.Add(entity); - - // Assert - await Assert.That(_entities).HasCount(1); - await Assert.That(_entities[0]).IsNotNull(); - } - - [Test] - public void EntityHasValidId() - { - var entity = new TEntity(); - - // Default ID should be 0 - Assert.That(entity.Id).IsEqualTo(0); - } - } - - /// - /// Nested generic types example - /// - [GenerateGenericTest(typeof(string), typeof(int))] - [GenerateGenericTest(typeof(DateTime), typeof(bool))] - public class NestedGenericTests - where TKey : IComparable - { - private readonly Dictionary> _data = new(); - - [Test] - public async Task CanStoreNestedData(TKey key, TValue value) - { - // Arrange - if (!_data.ContainsKey(key)) - { - _data[key] = new List(); - } - - // Act - _data[key].Add(value); - - // Assert - await Assert.That(_data[key]).Contains(value); - } - } - - /// - /// Example showing that without [GenerateGenericTest], - /// generic tests won't work in AOT mode - /// - public class NonAotGenericTest - { - [Test] - public void ThisWontWorkInAot() - { - // This test class has no [GenerateGenericTest] attribute - // So it won't be instantiated in AOT mode - // The source generator will skip it or generate a warning - var typeName = typeof(T).Name; - Assert.That(typeName).IsNotNull(); - } - } -} \ No newline at end of file diff --git a/Examples/ReflectionFreeExamples.cs b/Examples/ReflectionFreeExamples.cs deleted file mode 100644 index 9d1749810c..0000000000 --- a/Examples/ReflectionFreeExamples.cs +++ /dev/null @@ -1,279 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using TUnit.Core; -using TUnit.Assertions; - -namespace TUnit.Examples; - -/// -/// Examples demonstrating reflection-free test patterns in TUnit -/// -public class ReflectionFreeExamples -{ - /// - /// Basic test - no reflection needed, delegates are pre-compiled - /// - [Test] - public async Task BasicAsyncTest() - { - await Task.Delay(10); - await Assert.That(1 + 1).IsEqualTo(2); - } - - /// - /// Synchronous test with compile-time delegate generation - /// - [Test] - public void SynchronousTest() - { - var result = PerformCalculation(5, 3); - Assert.That(result).IsEqualTo(8); - } - - /// - /// Parameterized test with static data - fully AOT compatible - /// - [Test] - [Arguments(1, 1, 2)] - [Arguments(2, 3, 5)] - [Arguments(5, 8, 13)] - public void ParameterizedTest(int a, int b, int expected) - { - var result = a + b; - Assert.That(result).IsEqualTo(expected); - } - - /// - /// Test with compile-time resolved data source - /// - [Test] - [MethodDataSource(typeof(TestDataProviders), nameof(TestDataProviders.GetMathTestCases))] - public async Task DataDrivenTest(int input, int expected) - { - var result = input * 2; - await Assert.That(result).IsEqualTo(expected); - } - - /// - /// Test with async data source - AOT friendly with pre-compiled factory - /// - [Test] - [MethodDataSource(typeof(TestDataProviders), nameof(TestDataProviders.GetAsyncTestData))] - public async Task AsyncDataSourceTest(string input, bool expectedValid) - { - var isValid = IsValidInput(input); - await Assert.That(isValid).IsEqualTo(expectedValid); - } - - /// - /// Test with property data source - no reflection at runtime - /// - [Test] - [PropertyDataSource(typeof(TestDataProviders), nameof(TestDataProviders.StaticTestData))] - public void PropertyDataSourceTest(TestCase testCase) - { - var result = ProcessTestCase(testCase); - Assert.That(result).IsNotNull(); - } - - /// - /// Test with complex argument types - fully type-safe at compile time - /// - [Test] - [Arguments(new[] { 1, 2, 3 }, 6)] - [Arguments(new[] { 5, 10, 15 }, 30)] - public void ComplexArgumentTest(int[] numbers, int expectedSum) - { - var sum = 0; - foreach (var num in numbers) - { - sum += num; - } - Assert.That(sum).IsEqualTo(expectedSum); - } - - /// - /// Test with class-level data source for multiple test methods - /// - [ClassDataSource] - public class UserTests(string username, int userId) - { - [Test] - public void ValidateUsername() - { - Assert.That(username).IsNotNull() - .And.IsNotEmpty() - .And.HasLengthGreaterThan(3); - } - - [Test] - public async Task ValidateUserId() - { - await Assert.That(userId).IsGreaterThan(0); - } - } - - /// - /// Test with custom timeout - no reflection needed for attribute processing - /// - [Test] - [Timeout(5000)] - public async Task TimeoutTest() - { - await Task.Delay(100); - await Assert.That(true).IsTrue(); - } - - /// - /// Test with categories for filtering - compile-time metadata - /// - [Test] - [Category("Unit")] - [Category("Fast")] - public void CategorizedTest() - { - var result = QuickCalculation(); - Assert.That(result).IsGreaterThan(0); - } - - /// - /// Test with retry logic - handled through pre-compiled metadata - /// - [Test] - [Retry(3)] - public void RetryableTest() - { - var random = new Random(); - var value = random.Next(0, 10); - - // This might fail sometimes, but retry logic will handle it - Assert.That(value).IsGreaterThan(5); - } - - /// - /// Test with dependencies - resolved at compile time - /// - [Test] - [DependsOn(nameof(BasicAsyncTest))] - public async Task DependentTest() - { - // This test runs after BasicAsyncTest completes - await Task.Delay(10); - await Assert.That(GetDependentValue()).IsEqualTo(42); - } - - #region Helper Methods - - private int PerformCalculation(int a, int b) => a + b; - - private bool IsValidInput(string input) => !string.IsNullOrWhiteSpace(input); - - private object ProcessTestCase(TestCase testCase) => new { testCase.Name, testCase.Value }; - - private int QuickCalculation() => 42; - - private int GetDependentValue() => 42; - - #endregion -} - -/// -/// Data providers for reflection-free tests -/// All methods are resolved at compile time and stored as factories -/// -public static class TestDataProviders -{ - /// - /// Static data source method - pre-compiled into factory - /// - public static IEnumerable GetMathTestCases() - { - yield return new object[] { 1, 2 }; - yield return new object[] { 5, 10 }; - yield return new object[] { 10, 20 }; - } - - /// - /// Async data source - AOT compatible through factory pattern - /// - public static async Task> GetAsyncTestData() - { - await Task.Delay(1); // Simulate async operation - - return new[] - { - new object[] { "valid", true }, - new object[] { "", false }, - new object[] { "test", true }, - new object[] { null!, false } - }; - } - - /// - /// Property data source - accessed without reflection at runtime - /// - public static IEnumerable StaticTestData { get; } = new[] - { - new TestCase { Name = "Test1", Value = 100 }, - new TestCase { Name = "Test2", Value = 200 }, - new TestCase { Name = "Test3", Value = 300 } - }; -} - -/// -/// Test data class for property data source -/// -public class TestCase -{ - public required string Name { get; init; } - public required int Value { get; init; } -} - -/// -/// Class data source implementation -/// -public class UserTestData : IClassDataSource -{ - public IEnumerable GetData() - { - yield return new object[] { "alice", 1 }; - yield return new object[] { "bob", 2 }; - yield return new object[] { "charlie", 3 }; - } -} - -/// -/// Example showing generic test class handling in AOT mode -/// Generic parameters must be resolved at compile time -/// -public class GenericTestExample where T : IComparable -{ - private readonly T _value; - - public GenericTestExample(T value) - { - _value = value; - } - - [Test] - public void GenericTest() - { - Assert.That(_value).IsNotNull(); - } -} - -/// -/// Concrete instantiations for AOT compilation -/// -[InheritsTests(typeof(GenericTestExample))] -public class IntGenericTests : GenericTestExample -{ - public IntGenericTests() : base(42) { } -} - -[InheritsTests(typeof(GenericTestExample))] -public class StringGenericTests : GenericTestExample -{ - public StringGenericTests() : base("test") { } -} \ No newline at end of file diff --git a/Examples/UnifiedTestBuilderExample.cs b/Examples/UnifiedTestBuilderExample.cs deleted file mode 100644 index 49b12783ee..0000000000 --- a/Examples/UnifiedTestBuilderExample.cs +++ /dev/null @@ -1,313 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using TUnit.Core; -using TUnit.Engine; -using TUnit.Engine.Building; -using TUnit.Engine.Framework; - -namespace TUnit.Examples; - -/// -/// Example showing how to use the unified test builder architecture -/// -public class UnifiedTestBuilderExample -{ - /// - /// Example 1: Using the unified test builder in AOT mode (default) - /// - public static async Task ExampleAotMode() - { - // Setup services - var services = new ServiceCollection(); - - // Register test invoker and hook invoker (normally done by the framework) - services.AddSingleton(); - services.AddSingleton(); - - // Create a test metadata source (normally from source generation) - var metadataSource = new SourceGeneratedTestMetadataSource(() => GetSampleTestMetadata()); - - // Register the unified test builder for AOT mode - services.AddUnifiedTestBuilderAot(metadataSource); - - // Build service provider - var serviceProvider = services.BuildServiceProvider(); - - // Get the test discovery service - var discoveryService = serviceProvider.GetRequiredService(); - - // Discover all tests - var tests = await discoveryService.DiscoverTests(); - - Console.WriteLine($"Discovered {tests.Count()} tests in AOT mode"); - foreach (var test in tests) - { - Console.WriteLine($" - {test.DisplayName} [{test.TestId}]"); - } - } - - /// - /// Example 2: Using the unified test builder in reflection mode - /// - public static async Task ExampleReflectionMode() - { - // Setup services - var services = new ServiceCollection(); - - // Register test invoker and hook invoker - services.AddSingleton(); - services.AddSingleton(); - - // Get assemblies to scan - var assemblies = new[] { typeof(SampleTestClass).Assembly }; - - // Register the unified test builder for reflection mode - services.AddUnifiedTestBuilderReflection(assemblies); - - // Build service provider - var serviceProvider = services.BuildServiceProvider(); - - // Get the test discovery service - var discoveryService = serviceProvider.GetRequiredService(); - - // Discover all tests - var tests = await discoveryService.DiscoverTests(); - - Console.WriteLine($"Discovered {tests.Count()} tests in reflection mode"); - foreach (var test in tests) - { - Console.WriteLine($" - {test.DisplayName} [{test.TestId}]"); - } - } - - /// - /// Example 3: Using the pipeline directly - /// - public static async Task ExampleDirectPipeline() - { - // Create metadata source - var metadataSource = new SourceGeneratedTestMetadataSource(() => GetSampleTestMetadata()); - - // Create test and hook invokers - var testInvoker = new DefaultTestInvoker(); - var hookInvoker = new DefaultHookInvoker(); - - // Create the pipeline for AOT mode - var pipeline = UnifiedTestBuilderPipelineFactory.CreateAotPipeline( - metadataSource, - testInvoker, - hookInvoker); - - // Build all tests - var tests = await pipeline.BuildTestsAsync(); - - Console.WriteLine($"Built {tests.Count()} tests using direct pipeline"); - foreach (var test in tests) - { - Console.WriteLine($" - {test.DisplayName}"); - Console.WriteLine($" ID: {test.TestId}"); - Console.WriteLine($" Can run in parallel: {test.Metadata.CanRunInParallel}"); - } - } - - /// - /// Example 4: Data-driven tests with the new factory pattern - /// - public static async Task ExampleDataDrivenTests() - { - // Create test metadata with data sources - var metadata = new TestMetadata - { - TestId = "ExampleTests.DataDrivenTest", - TestName = "DataDrivenTest", - TestClassType = typeof(SampleTestClass), - TestMethodName = "TestWithData", - Categories = new[] { "DataDriven" }, - IsSkipped = false, - CanRunInParallel = true, - DependsOn = Array.Empty(), - - // Static data source with factory pattern - DataSources = new TestDataSource[] - { - new StaticTestDataSource( - new object?[][] - { - new object?[] { 1, 2, 3 }, - new object?[] { 4, 5, 9 }, - new object?[] { 10, 20, 30 } - }) - }, - - // Class-level data for constructor - ClassDataSources = new TestDataSource[] - { - new StaticTestDataSource( - new object?[][] - { - new object?[] { "TestContext1" }, - new object?[] { "TestContext2" } - }) - }, - - // Property data sources (single values) - PropertyDataSources = new PropertyDataSource[] - { - new PropertyDataSource - { - PropertyName = "TestProperty", - PropertyType = typeof(string), - DataSource = new StaticTestDataSource( - new object?[][] - { - new object?[] { "PropertyValue1" }, - new object?[] { "PropertyValue2" } - }) - } - }, - - ParameterCount = 3, - ParameterTypes = new[] { typeof(int), typeof(int), typeof(int) }, - Hooks = new TestHooks(), - InstanceFactory = args => new SampleTestClass((string)args[0]), - TestInvoker = async (instance, args) => - { - var method = instance.GetType().GetMethod("TestWithData"); - await Task.Run(() => method!.Invoke(instance, args)); - } - }; - - // Create a simple metadata source - var metadataSource = new SourceGeneratedTestMetadataSource(() => new[] { metadata }); - - // Create the pipeline - var pipeline = UnifiedTestBuilderPipelineFactory.CreateAotPipeline( - metadataSource, - new DefaultTestInvoker(), - new DefaultHookInvoker()); - - // Build tests - this will expand all data combinations - var tests = await pipeline.BuildTestsAsync(); - - Console.WriteLine($"Expanded to {tests.Count()} test variations:"); - foreach (var test in tests) - { - Console.WriteLine($" - {test.DisplayName}"); - - // The key feature: each test gets fresh data instances - var instance1 = await test.CreateInstance(); - var instance2 = await test.CreateInstance(); - - Console.WriteLine($" Instance 1: {instance1.GetHashCode()}"); - Console.WriteLine($" Instance 2: {instance2.GetHashCode()}"); - Console.WriteLine($" Instances are different: {!ReferenceEquals(instance1, instance2)}"); - } - } - - // Sample test metadata for examples - private static IEnumerable GetSampleTestMetadata() - { - return new[] - { - new TestMetadata - { - TestId = "SampleTests.Test1", - TestName = "Test1", - TestClassType = typeof(SampleTestClass), - TestMethodName = "SimpleTest", - Categories = Array.Empty(), - IsSkipped = false, - CanRunInParallel = true, - DependsOn = Array.Empty(), - DataSources = Array.Empty(), - ClassDataSources = Array.Empty(), - PropertyDataSources = Array.Empty(), - ParameterCount = 0, - ParameterTypes = Array.Empty(), - Hooks = new TestHooks() - }, - new TestMetadata - { - TestId = "SampleTests.Test2", - TestName = "Test2", - TestClassType = typeof(SampleTestClass), - TestMethodName = "TestWithTimeout", - Categories = new[] { "Integration" }, - IsSkipped = false, - TimeoutMs = 5000, - RetryCount = 2, - CanRunInParallel = false, - DependsOn = new[] { "SampleTests.Test1" }, - DataSources = Array.Empty(), - ClassDataSources = Array.Empty(), - PropertyDataSources = Array.Empty(), - ParameterCount = 0, - ParameterTypes = Array.Empty(), - Hooks = new TestHooks() - } - }; - } -} - -// Sample test class for examples -public class SampleTestClass -{ - private readonly string _context; - - public SampleTestClass() : this("default") { } - - public SampleTestClass(string context) - { - _context = context; - } - - public string TestProperty { get; set; } = ""; - - [Test] - public void SimpleTest() - { - Console.WriteLine($"Running SimpleTest in context: {_context}"); - } - - [Test] - [Timeout(5000)] - [Retry(2)] - [NotInParallel] - [DependsOn("SimpleTest")] - [Category("Integration")] - public async Task TestWithTimeout() - { - Console.WriteLine($"Running TestWithTimeout in context: {_context}"); - await Task.Delay(100); - } - - [Test] - [Arguments(1, 2, 3)] - [Arguments(4, 5, 9)] - public void TestWithData(int a, int b, int expected) - { - Console.WriteLine($"Testing {a} + {b} = {expected} in context: {_context}, property: {TestProperty}"); - } -} - -// Placeholder implementations for the example -public class DefaultTestInvoker : ITestInvoker -{ - public Task InvokeTestMethod(object instance, MethodInfo method, object?[] arguments) - { - var result = method.Invoke(instance, arguments); - if (result is Task task) - return task; - return Task.CompletedTask; - } -} - -public class DefaultHookInvoker : IHookInvoker -{ - public Task InvokeHookAsync(object? instance, MethodInfo method, HookContext context) - { - var result = method.Invoke(instance, new object[] { context }); - if (result is Task task) - return task; - return Task.CompletedTask; - } -} \ No newline at end of file diff --git a/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs index 1120d2c753..cd465c1b56 100644 --- a/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs +++ b/TUnit.Assertions.SourceGenerator/Generators/AssertionMethodGenerator.cs @@ -12,15 +12,7 @@ public sealed class AssertionMethodGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { - // Handle non-generic CreateAssertionAttribute (deprecated) - var nonGenericCreateAttributeData = context.SyntaxProvider - .ForAttributeWithMetadataName( - "TUnit.Assertions.Attributes.CreateAssertionAttribute", - predicate: (node, _) => true, - transform: (ctx, _) => GetCreateAssertionAttributeData(ctx)) - .Where(x => x != null); - - // Handle non-generic AssertionFromAttribute (new) + // Handle non-generic AssertionFromAttribute var nonGenericAssertionFromData = context.SyntaxProvider .ForAttributeWithMetadataName( "TUnit.Assertions.Attributes.AssertionFromAttribute", @@ -28,15 +20,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) transform: (ctx, _) => GetCreateAssertionAttributeData(ctx)) .Where(x => x != null); - // Handle generic CreateAssertionAttribute (deprecated) - var genericCreateAttributeData = context.SyntaxProvider - .CreateSyntaxProvider( - predicate: (node, _) => node is ClassDeclarationSyntax, - transform: (ctx, _) => GetGenericCreateAssertionAttributeData(ctx, "CreateAssertionAttribute")) - .Where(x => x != null) - .SelectMany((x, _) => x!.ToImmutableArray()); - - // Handle generic AssertionFromAttribute (new) + // Handle generic AssertionFromAttribute var genericAssertionFromData = context.SyntaxProvider .CreateSyntaxProvider( predicate: (node, _) => node is ClassDeclarationSyntax, @@ -45,16 +29,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .SelectMany((x, _) => x!.ToImmutableArray()); // Combine all sources - var allAttributeData = nonGenericCreateAttributeData.Collect() - .Combine(nonGenericAssertionFromData.Collect()) - .Combine(genericCreateAttributeData.Collect()) + var allAttributeData = nonGenericAssertionFromData.Collect() .Combine(genericAssertionFromData.Collect()) .Select((data, _) => { var result = new List(); - result.AddRange(data.Left.Left.Left.Where(x => x != null).SelectMany(x => x!)); - result.AddRange(data.Left.Left.Right.Where(x => x != null).SelectMany(x => x!)); - result.AddRange(data.Left.Right); + result.AddRange(data.Left.Where(x => x != null).SelectMany(x => x!)); result.AddRange(data.Right); return result.AsEnumerable(); }); diff --git a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs index d32840697b..750caa385b 100644 --- a/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs +++ b/TUnit.Assertions.SourceGenerator/Generators/MethodAssertionGenerator.cs @@ -18,53 +18,105 @@ namespace TUnit.Assertions.SourceGenerator.Generators; [Generator] public sealed class MethodAssertionGenerator : IIncrementalGenerator { + private static readonly DiagnosticDescriptor MethodMustBeStaticRule = new DiagnosticDescriptor( + id: "TUNITGEN001", + title: "Method must be static", + messageFormat: "Method '{0}' decorated with [GenerateAssertion] must be static", + category: "TUnit.Assertions.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Methods decorated with [GenerateAssertion] must be static to be used in generated assertions."); + + private static readonly DiagnosticDescriptor MethodMustHaveParametersRule = new DiagnosticDescriptor( + id: "TUNITGEN002", + title: "Method must have at least one parameter", + messageFormat: "Method '{0}' decorated with [GenerateAssertion] must have at least one parameter (the value to assert)", + category: "TUnit.Assertions.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Methods decorated with [GenerateAssertion] must have at least one parameter representing the value being asserted."); + + private static readonly DiagnosticDescriptor UnsupportedReturnTypeRule = new DiagnosticDescriptor( + id: "TUNITGEN003", + title: "Unsupported return type", + messageFormat: "Method '{0}' decorated with [GenerateAssertion] has unsupported return type '{1}'. Supported types are: bool, AssertionResult, Task, Task", + category: "TUnit.Assertions.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Methods decorated with [GenerateAssertion] must return bool, AssertionResult, Task, or Task."); + public void Initialize(IncrementalGeneratorInitializationContext context) { // Find all methods decorated with [GenerateAssertion] - var assertionMethods = context.SyntaxProvider + var assertionMethodsOrDiagnostics = context.SyntaxProvider .ForAttributeWithMetadataName( "TUnit.Assertions.Attributes.GenerateAssertionAttribute", predicate: static (node, _) => node is MethodDeclarationSyntax, - transform: static (ctx, ct) => GetAssertionMethodData(ctx, ct)) - .Where(static x => x != null) - .Select(static (x, _) => x!); + transform: (ctx, ct) => GetAssertionMethodData(ctx, ct)); + + // Split into methods and diagnostics + var methods = assertionMethodsOrDiagnostics + .Where(x => x.Data != null) + .Select((x, _) => x.Data!); + + var diagnostics = assertionMethodsOrDiagnostics + .Where(x => x.Diagnostic != null) + .Select((x, _) => x.Diagnostic!); + + // Report diagnostics + context.RegisterSourceOutput(diagnostics, static (context, diagnostic) => + { + context.ReportDiagnostic(diagnostic); + }); // Generate assertion classes and extension methods - context.RegisterSourceOutput(assertionMethods.Collect(), static (context, methods) => + context.RegisterSourceOutput(methods.Collect(), static (context, methods) => { GenerateAssertions(context, methods); }); } - private static AssertionMethodData? GetAssertionMethodData( + private static (AssertionMethodData? Data, Diagnostic? Diagnostic) GetAssertionMethodData( GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) { if (context.TargetSymbol is not IMethodSymbol methodSymbol) { - return null; + return (null, null); } + var location = context.TargetNode.GetLocation(); + // Validate method is static if (!methodSymbol.IsStatic) { - // TODO: Report diagnostic - method must be static - return null; + var diagnostic = Diagnostic.Create( + MethodMustBeStaticRule, + location, + methodSymbol.Name); + return (null, diagnostic); } // Validate method has at least one parameter if (methodSymbol.Parameters.Length == 0) { - // TODO: Report diagnostic - method must have at least one parameter - return null; + var diagnostic = Diagnostic.Create( + MethodMustHaveParametersRule, + location, + methodSymbol.Name); + return (null, diagnostic); } // Get return type info var returnTypeInfo = AnalyzeReturnType(methodSymbol.ReturnType); if (returnTypeInfo == null) { - // TODO: Report diagnostic - unsupported return type - return null; + var diagnostic = Diagnostic.Create( + UnsupportedReturnTypeRule, + location, + methodSymbol.Name, + methodSymbol.ReturnType.ToDisplayString()); + return (null, diagnostic); } // First parameter is the target type (what becomes IAssertionSource) @@ -90,7 +142,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } } - return new AssertionMethodData( + var data = new AssertionMethodData( methodSymbol, targetType, additionalParameters, @@ -98,6 +150,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) isExtensionMethod, customExpectation ); + + return (data, null); } private static ReturnTypeInfo? AnalyzeReturnType(ITypeSymbol returnType) diff --git a/TUnit.Assertions/Assertions/PropertyAssertion.cs b/TUnit.Assertions/Assertions/PropertyAssertion.cs index e7b92dccec..8112b3db53 100644 --- a/TUnit.Assertions/Assertions/PropertyAssertion.cs +++ b/TUnit.Assertions/Assertions/PropertyAssertion.cs @@ -35,7 +35,6 @@ public PropertyAssertionResult IsEqualTo(TProperty expected) { _parentContext.ExpressionBuilder.Append($".IsEqualTo({expected})"); - // Create assertion on the property and wrap it with type erasure var assertion = new Conditions.EqualsAssertion(_propertyContext, expected); var erasedAssertion = new Conditions.TypeErasedAssertion(assertion); @@ -132,10 +131,8 @@ public TypeOfAssertion IsTypeOf() private async Task ExecuteAsync() { - // Execute the property assertion await _propertyAssertion.AssertAsync(); - // Return the parent object value var (parentValue, _) = await Context.GetAsync(); return parentValue; } diff --git a/TUnit.Assertions/Attributes/CreateAssertionAttribute.cs b/TUnit.Assertions/Attributes/CreateAssertionAttribute.cs deleted file mode 100644 index 01fd9c22eb..0000000000 --- a/TUnit.Assertions/Attributes/CreateAssertionAttribute.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace TUnit.Assertions.Attributes; - -/// -/// DEPRECATED: Use instead. -/// This attribute has been renamed for better clarity. -/// -[Obsolete("Use AssertionFromAttribute instead. This attribute will be removed in a future version.")] -[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] -public class CreateAssertionAttribute : Attribute -{ - public CreateAssertionAttribute(Type targetType, string methodName) - { - TargetType = targetType; - MethodName = methodName; - } - - /// - /// Constructor for methods on a different type than the target type. - /// - /// The type of the first parameter (what becomes IValueSource<T>) - /// The type that contains the static method - /// The name of the static method - /// The types of assertions to generate - public CreateAssertionAttribute(Type targetType, Type containingType, string methodName) - { - TargetType = targetType; - ContainingType = containingType; - MethodName = methodName; - } - - public Type TargetType { get; } - public Type? ContainingType { get; } - public string MethodName { get; } - - - /// - /// Optional custom name for the generated assertion method. If not specified, the name will be derived from the target method name. - /// - public string? CustomName { get; set; } - - /// - /// When true, inverts the logic of the assertion. This is used for creating negative assertions (e.g., DoesNotContain from Contains). - /// The generated assertion will negate the result of the target method. - /// - public bool NegateLogic { get; set; } - - /// - /// Indicates if this method requires generic type parameter handling (e.g., Enum.IsDefined(Type, object) where Type becomes typeof(T)). - /// - public bool RequiresGenericTypeParameter { get; set; } - - /// - /// When true, treats the method as an instance method even if it's static (useful for extension methods). - /// When false (default), the generator will automatically determine based on the method's actual signature. - /// - public bool TreatAsInstance { get; set; } -} diff --git a/TUnit.Assertions/Attributes/CreateAssertionAttribute`1.cs b/TUnit.Assertions/Attributes/CreateAssertionAttribute`1.cs deleted file mode 100644 index 62d2f1cf07..0000000000 --- a/TUnit.Assertions/Attributes/CreateAssertionAttribute`1.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; - -namespace TUnit.Assertions.Attributes; - -/// -/// DEPRECATED: Use instead. -/// This attribute has been renamed for better clarity. -/// -/// The target type for the assertion -[Obsolete("Use AssertionFromAttribute instead. This attribute will be removed in a future version.")] -[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] -public class CreateAssertionAttribute : Attribute -{ - public CreateAssertionAttribute(string methodName) - { - TargetType = typeof(TTarget); - MethodName = methodName; - } - - /// - /// Constructor for methods on a different type than the target type. - /// - /// The type that contains the static method - /// The name of the static method - public CreateAssertionAttribute(Type containingType, string methodName) - { - TargetType = typeof(TTarget); - ContainingType = containingType; - MethodName = methodName; - } - - public Type TargetType { get; } - public Type? ContainingType { get; } - public string MethodName { get; } - - /// - /// Optional custom name for the generated assertion method. If not specified, the name will be derived from the target method name. - /// - public string? CustomName { get; set; } - - /// - /// When true, inverts the logic of the assertion. This is used for creating negative assertions (e.g., DoesNotContain from Contains). - /// The generated assertion will negate the result of the target method. - /// - public bool NegateLogic { get; set; } - - /// - /// Indicates if this method requires generic type parameter handling (e.g., Enum.IsDefined(Type, object) where Type becomes typeof(T)). - /// - public bool RequiresGenericTypeParameter { get; set; } - - /// - /// When true, treats the method as an instance method even if it's static (useful for extension methods). - /// When false (default), the generator will automatically determine based on the method's actual signature. - /// - public bool TreatAsInstance { get; set; } -} \ No newline at end of file diff --git a/TUnit.Assertions/Chaining/OrAssertion.cs b/TUnit.Assertions/Chaining/OrAssertion.cs index a462ca989e..47aa2e87c2 100644 --- a/TUnit.Assertions/Chaining/OrAssertion.cs +++ b/TUnit.Assertions/Chaining/OrAssertion.cs @@ -75,7 +75,6 @@ public OrAssertion( // Both failed - build combined message var secondException = currentScope.GetLastException(); - // Remove both individual exceptions and add combined one currentScope.RemoveLastExceptions(2); var combinedExpectation = BuildCombinedExpectation(); diff --git a/TUnit.Assertions/Conditions/MemberAssertion.cs b/TUnit.Assertions/Conditions/MemberAssertion.cs index ce51314c54..991c6147b5 100644 --- a/TUnit.Assertions/Conditions/MemberAssertion.cs +++ b/TUnit.Assertions/Conditions/MemberAssertion.cs @@ -64,10 +64,8 @@ public static implicit operator Assertion(MemberAssertionResult ExecuteAsync() { - // Execute the member assertion await _memberAssertion.AssertAsync(); - // Return the parent object value var (parentValue, _) = await _parentContext.GetAsync(); return parentValue; } @@ -89,17 +87,14 @@ public MemberExecutionWrapper(AssertionContext parentContext, Assertion public override async Task AssertAsync() { - // Execute the member assertion await _memberAssertion.AssertAsync(); - // Return the parent object value for further chaining var (parentValue, _) = await Context.GetAsync(); return parentValue; } protected override async Task CheckAsync(EvaluationMetadata metadata) { - // Execute the member assertion await _memberAssertion.AssertAsync(); return AssertionResult.Passed; } diff --git a/TUnit.Assertions/Core/Assertion.cs b/TUnit.Assertions/Core/Assertion.cs index 7efd8f0019..a79e213396 100644 --- a/TUnit.Assertions/Core/Assertion.cs +++ b/TUnit.Assertions/Core/Assertion.cs @@ -52,7 +52,6 @@ protected Assertion(AssertionContext context) var (previous, combiner) = context.ConsumePendingLink(); if (previous != null) { - // Create wrapper based on combiner type _wrappedExecution = combiner == CombinerType.And ? new Chaining.AndAssertion(previous, this) : new Chaining.OrAssertion(previous, this); diff --git a/TUnit.Assertions/Extensions/AssertionExtensions.cs b/TUnit.Assertions/Extensions/AssertionExtensions.cs index fdcb2916f3..0349e62fc8 100644 --- a/TUnit.Assertions/Extensions/AssertionExtensions.cs +++ b/TUnit.Assertions/Extensions/AssertionExtensions.cs @@ -208,7 +208,6 @@ public static MemberAssertionResult Member combinedAssertion = combinerType == CombinerType.And ? new CombinedAndAssertion(parentContext, pendingAssertion, erasedAssertion) : new CombinedOrAssertion(parentContext, pendingAssertion, erasedAssertion); @@ -261,7 +260,6 @@ public static MemberAssertionResult Member( // If there was a pending link, wrap both assertions together if (pendingAssertion != null && combinerType != null) { - // Create a combined wrapper that executes the pending assertion first (or together for Or) Assertion combinedAssertion = combinerType == CombinerType.And ? new CombinedAndAssertion(parentContext, pendingAssertion, erasedAssertion) : new CombinedOrAssertion(parentContext, pendingAssertion, erasedAssertion); @@ -369,7 +367,6 @@ public static MemberAssertionResult Member combinedAssertion = combinerType == CombinerType.And ? new CombinedAndAssertion(parentContext, pendingAssertion, erasedAssertion) : new CombinedOrAssertion(parentContext, pendingAssertion, erasedAssertion); @@ -422,7 +419,6 @@ public static MemberAssertionResult Member( // If there was a pending link, wrap both assertions together if (pendingAssertion != null && combinerType != null) { - // Create a combined wrapper that executes the pending assertion first (or together for Or) Assertion combinedAssertion = combinerType == CombinerType.And ? new CombinedAndAssertion(parentContext, pendingAssertion, erasedAssertion) : new CombinedOrAssertion(parentContext, pendingAssertion, erasedAssertion); @@ -530,7 +526,6 @@ public static MemberAssertionResult Member combinedAssertion = combinerType == CombinerType.And ? new CombinedAndAssertion(parentContext, pendingAssertion, erasedAssertion) : new CombinedOrAssertion(parentContext, pendingAssertion, erasedAssertion); @@ -582,7 +577,6 @@ public static MemberAssertionResult Member( // If there was a pending link, wrap both assertions together if (pendingAssertion != null && combinerType != null) { - // Create a combined wrapper that executes the pending assertion first (or together for Or) Assertion combinedAssertion = combinerType == CombinerType.And ? new CombinedAndAssertion(parentContext, pendingAssertion, erasedAssertion) : new CombinedOrAssertion(parentContext, pendingAssertion, erasedAssertion); diff --git a/TUnit.Assertions/Sources/AsyncDelegateAssertion.cs b/TUnit.Assertions/Sources/AsyncDelegateAssertion.cs index 6147ea3e3f..17b03c5b48 100644 --- a/TUnit.Assertions/Sources/AsyncDelegateAssertion.cs +++ b/TUnit.Assertions/Sources/AsyncDelegateAssertion.cs @@ -40,13 +40,11 @@ public AsyncDelegateAssertion(Func action, string? expression) }); Context = new AssertionContext(evaluationContext, expressionBuilder); - // Create a TaskContext for Task-specific assertions // DO NOT await the task here - we want to check its state synchronously var taskExpressionBuilder = new StringBuilder(); taskExpressionBuilder.Append(expressionBuilder.ToString()); var taskEvaluationContext = new EvaluationContext(() => { - // Return the task object itself without awaiting it // This allows IsCompleted, IsCanceled, IsFaulted, etc. to check task properties synchronously var task = action(); return Task.FromResult<(Task?, Exception?)>((task, null)); @@ -177,7 +175,6 @@ public ThrowsExactlyAssertion ThrowsExactly() where TExc Context.ExpressionBuilder.Append($".Throws({exceptionType.Name})"); // Delegate to the generic Throws() and add runtime type checking var assertion = Throws(); - // Return the assertion with runtime type filtering applied return assertion.WithExceptionType(exceptionType); } diff --git a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs index 43a2a55606..1a71704c50 100644 --- a/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs +++ b/TUnit.Core.SourceGenerator/Generators/TestMetadataGenerator.cs @@ -2708,9 +2708,6 @@ private static void GenerateGenericTestWithConcreteTypes( var constraintsValid = ValidateClassTypeConstraints(testMethod.TypeSymbol, classTypes) && ValidateTypeConstraints(testMethod.MethodSymbol, methodTypes); - // TODO: Fix ValidateTypeConstraints method - temporarily skip validation - constraintsValid = true; - if (!constraintsValid) { continue; @@ -2808,9 +2805,6 @@ private static void GenerateGenericTestWithConcreteTypes( // Validate class type constraints var constraintsValid = ValidateClassTypeConstraints(testMethod.TypeSymbol, inferredTypes); - // TODO: Fix ValidateClassTypeConstraints method - temporarily skip validation - constraintsValid = true; - if (!constraintsValid) { continue; diff --git a/TUnit.Core.SourceGenerator/Helpers/GenericTypeInference.cs b/TUnit.Core.SourceGenerator/Helpers/GenericTypeInference.cs index 52cfbbd9ce..6acba46801 100644 --- a/TUnit.Core.SourceGenerator/Helpers/GenericTypeInference.cs +++ b/TUnit.Core.SourceGenerator/Helpers/GenericTypeInference.cs @@ -128,7 +128,6 @@ internal static class GenericTypeInference { var parameter = method.Parameters[i]; - // Check if this parameter uses a type parameter if (parameter.Type is ITypeParameterSymbol typeParam) { // Get the corresponding argument value from the attribute diff --git a/TUnit.Core.SourceGenerator/Helpers/InterfaceCache.cs b/TUnit.Core.SourceGenerator/Helpers/InterfaceCache.cs index 0064250efa..30f6f5cf71 100644 --- a/TUnit.Core.SourceGenerator/Helpers/InterfaceCache.cs +++ b/TUnit.Core.SourceGenerator/Helpers/InterfaceCache.cs @@ -47,14 +47,12 @@ public static bool IsAsyncEnumerable(ITypeSymbol type) { return _implementsCache.GetOrAdd((type, "System.Collections.Generic.IAsyncEnumerable"), key => { - // Check if the type itself is an IAsyncEnumerable if (key.Type is INamedTypeSymbol { IsGenericType: true } namedType && namedType.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IAsyncEnumerable") { return true; } - // Check if the type implements IAsyncEnumerable return key.Type.AllInterfaces.Any(i => i.IsGenericType && i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IAsyncEnumerable"); diff --git a/TUnit.Core.SourceGenerator/Models/TestDefinitionContext.cs b/TUnit.Core.SourceGenerator/Models/TestDefinitionContext.cs index e97be27235..61683d4d07 100644 --- a/TUnit.Core.SourceGenerator/Models/TestDefinitionContext.cs +++ b/TUnit.Core.SourceGenerator/Models/TestDefinitionContext.cs @@ -39,7 +39,6 @@ public static IEnumerable CreateContexts(TestMetadataGene var testIndex = 0; - // If no attributes, create tests based on repeat count if (!classDataAttrs.Any() && !methodDataAttrs.Any()) { for (var repeatIndex = 0; repeatIndex < repeatCount; repeatIndex++) @@ -56,7 +55,6 @@ public static IEnumerable CreateContexts(TestMetadataGene yield break; } - // If we have class data but no method data if (classDataAttrs.Any() && !methodDataAttrs.Any()) { foreach (var classAttr in classDataAttrs) @@ -74,7 +72,6 @@ public static IEnumerable CreateContexts(TestMetadataGene } } } - // If we have method data but no class data else if (!classDataAttrs.Any() && methodDataAttrs.Any()) { foreach (var methodAttr in methodDataAttrs) diff --git a/TUnit.Core.SourceGenerator/Models/TestMetadataGenerationContext.cs b/TUnit.Core.SourceGenerator/Models/TestMetadataGenerationContext.cs index 45041f01e5..8732001c98 100644 --- a/TUnit.Core.SourceGenerator/Models/TestMetadataGenerationContext.cs +++ b/TUnit.Core.SourceGenerator/Models/TestMetadataGenerationContext.cs @@ -121,7 +121,6 @@ private static bool DetermineIfStaticTestDefinition(TestMethodMetadata testInfo) { foreach (var attr in param.GetAttributes()) { - // Check if it's a data source attribute that requires runtime resolution if (IsRuntimeDataSourceAttribute(attr, testInfo.TypeSymbol)) { return false; diff --git a/TUnit.Core/Contexts/DiscoveredTestContext.cs b/TUnit.Core/Contexts/DiscoveredTestContext.cs index 1013e6f928..4326d1a165 100644 --- a/TUnit.Core/Contexts/DiscoveredTestContext.cs +++ b/TUnit.Core/Contexts/DiscoveredTestContext.cs @@ -68,16 +68,6 @@ public void AddParallelConstraint(IParallelConstraint constraint) { TestContext.AddParallelConstraint(constraint); } - - /// - /// Sets the parallel constraint, replacing any existing constraints. - /// Maintained for backward compatibility. - /// - [Obsolete("Use AddParallelConstraint to support multiple constraints. This method replaces all existing constraints.")] - public void SetParallelConstraint(IParallelConstraint constraint) - { - TestContext.ParallelConstraint = constraint; - } public void AddArgumentDisplayFormatter(ArgumentDisplayFormatter formatter) { diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs index 4f3dabc706..2ecf39146a 100644 --- a/TUnit.Core/TestContext.cs +++ b/TUnit.Core/TestContext.cs @@ -120,25 +120,6 @@ public static string WorkingDirectory /// public IReadOnlyList ParallelConstraints => _parallelConstraints; - /// - /// Gets or sets the primary parallel constraint for backward compatibility. - /// When setting, this replaces all existing constraints. - /// When getting, returns the first constraint or null if none exist. - /// - [Obsolete("Use ParallelConstraints collection instead. This property is maintained for backward compatibility.")] - public IParallelConstraint? ParallelConstraint - { - get => _parallelConstraints.FirstOrDefault(); - set - { - _parallelConstraints.Clear(); - if (value != null) - { - _parallelConstraints.Add(value); - } - } - } - /// /// Adds a parallel constraint to this test context. /// Multiple constraints can be combined to create complex parallelization rules. @@ -320,33 +301,6 @@ public void OverrideResult(TestState state, string reason) InternalExecutableTest.State = state; } - /// - /// Reregisters a test with new arguments. This method is currently non-functional as the underlying - /// ITestFinder interface has been removed. This functionality may be reimplemented in a future version. - /// - /// - /// Previously used for dynamically modifying test arguments at runtime. Consider using data source - /// attributes for parameterized tests instead. - /// - [Obsolete("This method is non-functional after the removal of ITestFinder. It will be removed in a future version.")] - public async Task ReregisterTestWithArguments(object?[]? methodArguments = null, Dictionary? objectBag = null) - { - if (methodArguments != null) - { - TestDetails.TestMethodArguments = methodArguments; - } - - if (objectBag != null) - { - foreach (var kvp in objectBag) - { - ObjectBag[kvp.Key] = kvp.Value; - } - } - - // This method is currently non-functional - see Obsolete attribute above - await Task.CompletedTask; - } public List Dependencies { get; } = [ @@ -383,7 +337,6 @@ public List GetTests(string testName) // Use the current test's class type by default var classType = TestDetails.ClassType; - // Call GetTestsByNameAndParameters with empty parameter lists to get all tests with this name var tests = testFinder.GetTestsByNameAndParameters( testName, [ @@ -409,7 +362,6 @@ public List GetTests(string testName, Type classType) { var testFinder = ServiceProvider.GetService()!; - // Call GetTestsByNameAndParameters with empty parameter lists to get all tests with this name return testFinder.GetTestsByNameAndParameters( testName, [ diff --git a/TUnit.Engine/Services/PropertyInitializationPipeline.cs b/TUnit.Engine/Services/PropertyInitializationPipeline.cs index f819acfdb9..f301f3ed05 100644 --- a/TUnit.Engine/Services/PropertyInitializationPipeline.cs +++ b/TUnit.Engine/Services/PropertyInitializationPipeline.cs @@ -62,7 +62,6 @@ public async Task ExecuteAsync(PropertyInitializationContext context) { try { - // Execute before steps foreach (var step in _beforeSteps) { await step(context); @@ -88,7 +87,6 @@ public async Task ExecuteAsync(PropertyInitializationContext context) $"of type '{context.PropertyType.Name}' on '{context.Instance.GetType().Name}'"); } - // Execute after steps foreach (var step in _afterSteps) { await step(context); diff --git a/TUnit.Engine/Services/ReflectionStaticPropertyInitializer.cs b/TUnit.Engine/Services/ReflectionStaticPropertyInitializer.cs index fa1bab8b5b..204fca6798 100644 --- a/TUnit.Engine/Services/ReflectionStaticPropertyInitializer.cs +++ b/TUnit.Engine/Services/ReflectionStaticPropertyInitializer.cs @@ -25,14 +25,12 @@ public async Task InitializeAsync(CancellationToken cancellationToken) { try { - // Execute all registered global initializers from source generation while (Sources.GlobalInitializers.TryDequeue(out var initializer)) { cancellationToken.ThrowIfCancellationRequested(); await initializer(); } - // Additionally, initialize static properties discovered via reflection await StaticPropertyReflectionInitializer.InitializeAllStaticPropertiesAsync(); } catch (Exception ex) diff --git a/TUnit.Engine/Services/SourceGenStaticPropertyInitializer.cs b/TUnit.Engine/Services/SourceGenStaticPropertyInitializer.cs index eb24ffb7fd..542fdaf046 100644 --- a/TUnit.Engine/Services/SourceGenStaticPropertyInitializer.cs +++ b/TUnit.Engine/Services/SourceGenStaticPropertyInitializer.cs @@ -20,7 +20,6 @@ public async Task InitializeAsync(CancellationToken cancellationToken) { try { - // Execute all registered global initializers from source generation while (Sources.GlobalInitializers.TryDequeue(out var initializer)) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/TUnit.Engine/Services/StaticPropertyHandler.cs b/TUnit.Engine/Services/StaticPropertyHandler.cs index e46a44eda7..c860b0524a 100644 --- a/TUnit.Engine/Services/StaticPropertyHandler.cs +++ b/TUnit.Engine/Services/StaticPropertyHandler.cs @@ -46,7 +46,6 @@ public async Task InitializeStaticPropertiesAsync(CancellationToken cancellation { try { - // Call the generated initializer var value = await property.InitializerAsync(); if (value != null) diff --git a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs index 46344b1faf..308c91be75 100644 --- a/TUnit.Engine/Services/TestExecution/TestCoordinator.cs +++ b/TUnit.Engine/Services/TestExecution/TestCoordinator.cs @@ -90,10 +90,8 @@ await RetryHelper.ExecuteWithRetry(test.Context, async () => { await _stateManager.MarkSkippedAsync(test, test.Context.SkipReason ?? "Test was skipped"); - // Invoke skipped event receivers await _eventReceiverOrchestrator.InvokeTestSkippedEventReceiversAsync(test.Context, cancellationToken); - // Invoke test end event receivers for skipped tests await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(test.Context, cancellationToken); return; diff --git a/TUnit.Engine/Services/TestGroupingService.cs b/TUnit.Engine/Services/TestGroupingService.cs index bd1473130d..a72db5bfe9 100644 --- a/TUnit.Engine/Services/TestGroupingService.cs +++ b/TUnit.Engine/Services/TestGroupingService.cs @@ -227,7 +227,6 @@ private static void ProcessNotInParallelConstraint( } else { - // Add test only once with all its constraint keys keyedNotInParallelList.Add((test, className, constraint.NotInParallelConstraintKeys, testPriority)); } } diff --git a/TUnit.Engine/TestDiscoveryService.cs b/TUnit.Engine/TestDiscoveryService.cs index 9d40643ac2..41682fdcd8 100644 --- a/TUnit.Engine/TestDiscoveryService.cs +++ b/TUnit.Engine/TestDiscoveryService.cs @@ -67,7 +67,6 @@ public async Task DiscoverTests(string testSessionId, ITest { allTests.Add(test); - // Check if this test has dependencies based on metadata if (test.Metadata.Dependencies.Length > 0) { // Buffer tests with dependencies for later resolution @@ -219,7 +218,6 @@ public async IAsyncEnumerable DiscoverTestsFullyStreamin foreach (var test in remainingTests) { - // Check if all dependencies have been yielded var allDependenciesYielded = test.Dependencies.All(dep => yieldedTests.Contains(dep.Test.TestId)); if (allDependenciesYielded) diff --git a/TUnit.Engine/TestExecutor.cs b/TUnit.Engine/TestExecutor.cs index cba434941f..19d60c4c55 100644 --- a/TUnit.Engine/TestExecutor.cs +++ b/TUnit.Engine/TestExecutor.cs @@ -59,7 +59,6 @@ public async Task ExecuteAsync(AbstractExecutableTest executableTest, Cancellati try { - // Ensure TestSession hooks have been executed await EnsureTestSessionHooksExecutedAsync().ConfigureAwait(false); // Event receivers have their own internal coordination to run once @@ -94,7 +93,6 @@ await _eventReceiverOrchestrator.InvokeFirstTestInClassEventReceiversAsync( await _hookExecutor.ExecuteBeforeTestHooksAsync(executableTest, cancellationToken).ConfigureAwait(false); - // Invoke test start event receivers await _eventReceiverOrchestrator.InvokeTestStartEventReceiversAsync(executableTest.Context, cancellationToken).ConfigureAwait(false); executableTest.Context.RestoreExecutionContext(); @@ -128,7 +126,6 @@ await TimeoutHelper.ExecuteWithTimeoutAsync( // Run After(Test) hooks await _hookExecutor.ExecuteAfterTestHooksAsync(executableTest, cancellationToken).ConfigureAwait(false); - // Invoke test end event receivers await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, cancellationToken).ConfigureAwait(false); } catch @@ -155,7 +152,6 @@ await TimeoutHelper.ExecuteWithTimeoutAsync( // Run After(Test) hooks await _hookExecutor.ExecuteAfterTestHooksAsync(executableTest, cancellationToken).ConfigureAwait(false); - // Invoke test end event receivers await _eventReceiverOrchestrator.InvokeTestEndEventReceiversAsync(executableTest.Context, cancellationToken).ConfigureAwait(false); } // Note: Test instance disposal and After(Class/Assembly/Session) hooks diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 32d6a99f6e..736677af9a 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -208,36 +208,6 @@ namespace .Attributes public TargetType { get; } public bool TreatAsInstance { get; set; } } - [(.Class, AllowMultiple=true)] - [("Use AssertionFromAttribute instead. This attribute will be removed in a future ve" + - "rsion.")] - public class CreateAssertionAttribute : - { - public CreateAssertionAttribute( targetType, string methodName) { } - public CreateAssertionAttribute( targetType, containingType, string methodName) { } - public ? ContainingType { get; } - public string? CustomName { get; set; } - public string MethodName { get; } - public bool NegateLogic { get; set; } - public bool RequiresGenericTypeParameter { get; set; } - public TargetType { get; } - public bool TreatAsInstance { get; set; } - } - [(.Class, AllowMultiple=true)] - [("Use AssertionFromAttribute instead. This attribute will be removed in a future" + - " version.")] - public class CreateAssertionAttribute : - { - public CreateAssertionAttribute(string methodName) { } - public CreateAssertionAttribute( containingType, string methodName) { } - public ? ContainingType { get; } - public string? CustomName { get; set; } - public string MethodName { get; } - public bool NegateLogic { get; set; } - public bool RequiresGenericTypeParameter { get; set; } - public TargetType { get; } - public bool TreatAsInstance { get; set; } - } [(.Method, AllowMultiple=false, Inherited=false)] public sealed class GenerateAssertionAttribute : { diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt index d0e272c035..e63d34e85f 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -205,36 +205,6 @@ namespace .Attributes public TargetType { get; } public bool TreatAsInstance { get; set; } } - [(.Class, AllowMultiple=true)] - [("Use AssertionFromAttribute instead. This attribute will be removed in a future ve" + - "rsion.")] - public class CreateAssertionAttribute : - { - public CreateAssertionAttribute( targetType, string methodName) { } - public CreateAssertionAttribute( targetType, containingType, string methodName) { } - public ? ContainingType { get; } - public string? CustomName { get; set; } - public string MethodName { get; } - public bool NegateLogic { get; set; } - public bool RequiresGenericTypeParameter { get; set; } - public TargetType { get; } - public bool TreatAsInstance { get; set; } - } - [(.Class, AllowMultiple=true)] - [("Use AssertionFromAttribute instead. This attribute will be removed in a future" + - " version.")] - public class CreateAssertionAttribute : - { - public CreateAssertionAttribute(string methodName) { } - public CreateAssertionAttribute( containingType, string methodName) { } - public ? ContainingType { get; } - public string? CustomName { get; set; } - public string MethodName { get; } - public bool NegateLogic { get; set; } - public bool RequiresGenericTypeParameter { get; set; } - public TargetType { get; } - public bool TreatAsInstance { get; set; } - } [(.Method, AllowMultiple=false, Inherited=false)] public sealed class GenerateAssertionAttribute : { diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt index dc4a6d144e..fdc5a12ab2 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -208,36 +208,6 @@ namespace .Attributes public TargetType { get; } public bool TreatAsInstance { get; set; } } - [(.Class, AllowMultiple=true)] - [("Use AssertionFromAttribute instead. This attribute will be removed in a future ve" + - "rsion.")] - public class CreateAssertionAttribute : - { - public CreateAssertionAttribute( targetType, string methodName) { } - public CreateAssertionAttribute( targetType, containingType, string methodName) { } - public ? ContainingType { get; } - public string? CustomName { get; set; } - public string MethodName { get; } - public bool NegateLogic { get; set; } - public bool RequiresGenericTypeParameter { get; set; } - public TargetType { get; } - public bool TreatAsInstance { get; set; } - } - [(.Class, AllowMultiple=true)] - [("Use AssertionFromAttribute instead. This attribute will be removed in a future" + - " version.")] - public class CreateAssertionAttribute : - { - public CreateAssertionAttribute(string methodName) { } - public CreateAssertionAttribute( containingType, string methodName) { } - public ? ContainingType { get; } - public string? CustomName { get; set; } - public string MethodName { get; } - public bool NegateLogic { get; set; } - public bool RequiresGenericTypeParameter { get; set; } - public TargetType { get; } - public bool TreatAsInstance { get; set; } - } [(.Method, AllowMultiple=false, Inherited=false)] public sealed class GenerateAssertionAttribute : { diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt index d875beeb7d..d60eb1cca4 100644 --- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -205,36 +205,6 @@ namespace .Attributes public TargetType { get; } public bool TreatAsInstance { get; set; } } - [(.Class, AllowMultiple=true)] - [("Use AssertionFromAttribute instead. This attribute will be removed in a future ve" + - "rsion.")] - public class CreateAssertionAttribute : - { - public CreateAssertionAttribute( targetType, string methodName) { } - public CreateAssertionAttribute( targetType, containingType, string methodName) { } - public ? ContainingType { get; } - public string? CustomName { get; set; } - public string MethodName { get; } - public bool NegateLogic { get; set; } - public bool RequiresGenericTypeParameter { get; set; } - public TargetType { get; } - public bool TreatAsInstance { get; set; } - } - [(.Class, AllowMultiple=true)] - [("Use AssertionFromAttribute instead. This attribute will be removed in a future" + - " version.")] - public class CreateAssertionAttribute : - { - public CreateAssertionAttribute(string methodName) { } - public CreateAssertionAttribute( containingType, string methodName) { } - public ? ContainingType { get; } - public string? CustomName { get; set; } - public string MethodName { get; } - public bool NegateLogic { get; set; } - public bool RequiresGenericTypeParameter { get; set; } - public TargetType { get; } - public bool TreatAsInstance { get; set; } - } [(.Method, AllowMultiple=false, Inherited=false)] public sealed class GenerateAssertionAttribute : { diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt index 4c6d041a8d..a215fd5bee 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet10_0.verified.txt @@ -563,9 +563,6 @@ namespace public string GetDisplayName() { } public void SetDisplayName(string displayName) { } public void SetDisplayNameFormatter( formatterType) { } - [("Use AddParallelConstraint to support multiple constraints. This method replaces a" + - "ll existing constraints.")] - public void SetParallelConstraint(. constraint) { } public void SetPriority(. priority) { } public void SetRetryLimit(int retryLimit) { } public void SetRetryLimit(int retryCount, <.TestContext, , int, .> shouldRetry) { } @@ -1283,9 +1280,6 @@ namespace public .CancellationTokenSource? LinkedCancellationTokens { get; set; } public object Lock { get; } public . ObjectBag { get; } - [("Use ParallelConstraints collection instead. This property is maintained for backw" + - "ard compatibility.")] - public .? ParallelConstraint { get; set; } public .<.> ParallelConstraints { get; } public .? ParallelLimiter { get; } public string? ParentTestId { get; set; } @@ -1318,9 +1312,6 @@ namespace public .<.TestContext> GetTests(string testName, classType) { } public void OverrideResult(string reason) { } public void OverrideResult(.TestState state, string reason) { } - [("This method is non-functional after the removal of ITestFinder. It will be remove" + - "d in a future version.")] - public . ReregisterTestWithArguments(object?[]? methodArguments = null, .? objectBag = null) { } public void SetParallelLimiter(. parallelLimit) { } public void WriteError(string message) { } public void WriteLine(string message) { } diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt index 1ccc7971a8..d6a47aa0a7 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet8_0.verified.txt @@ -563,9 +563,6 @@ namespace public string GetDisplayName() { } public void SetDisplayName(string displayName) { } public void SetDisplayNameFormatter( formatterType) { } - [("Use AddParallelConstraint to support multiple constraints. This method replaces a" + - "ll existing constraints.")] - public void SetParallelConstraint(. constraint) { } public void SetPriority(. priority) { } public void SetRetryLimit(int retryLimit) { } public void SetRetryLimit(int retryCount, <.TestContext, , int, .> shouldRetry) { } @@ -1283,9 +1280,6 @@ namespace public .CancellationTokenSource? LinkedCancellationTokens { get; set; } public object Lock { get; } public . ObjectBag { get; } - [("Use ParallelConstraints collection instead. This property is maintained for backw" + - "ard compatibility.")] - public .? ParallelConstraint { get; set; } public .<.> ParallelConstraints { get; } public .? ParallelLimiter { get; } public string? ParentTestId { get; set; } @@ -1318,9 +1312,6 @@ namespace public .<.TestContext> GetTests(string testName, classType) { } public void OverrideResult(string reason) { } public void OverrideResult(.TestState state, string reason) { } - [("This method is non-functional after the removal of ITestFinder. It will be remove" + - "d in a future version.")] - public . ReregisterTestWithArguments(object?[]? methodArguments = null, .? objectBag = null) { } public void SetParallelLimiter(. parallelLimit) { } public void WriteError(string message) { } public void WriteLine(string message) { } diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt index d1c3f9b3e9..b73fbaf1bb 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.DotNet9_0.verified.txt @@ -563,9 +563,6 @@ namespace public string GetDisplayName() { } public void SetDisplayName(string displayName) { } public void SetDisplayNameFormatter( formatterType) { } - [("Use AddParallelConstraint to support multiple constraints. This method replaces a" + - "ll existing constraints.")] - public void SetParallelConstraint(. constraint) { } public void SetPriority(. priority) { } public void SetRetryLimit(int retryLimit) { } public void SetRetryLimit(int retryCount, <.TestContext, , int, .> shouldRetry) { } @@ -1283,9 +1280,6 @@ namespace public .CancellationTokenSource? LinkedCancellationTokens { get; set; } public object Lock { get; } public . ObjectBag { get; } - [("Use ParallelConstraints collection instead. This property is maintained for backw" + - "ard compatibility.")] - public .? ParallelConstraint { get; set; } public .<.> ParallelConstraints { get; } public .? ParallelLimiter { get; } public string? ParentTestId { get; set; } @@ -1318,9 +1312,6 @@ namespace public .<.TestContext> GetTests(string testName, classType) { } public void OverrideResult(string reason) { } public void OverrideResult(.TestState state, string reason) { } - [("This method is non-functional after the removal of ITestFinder. It will be remove" + - "d in a future version.")] - public . ReregisterTestWithArguments(object?[]? methodArguments = null, .? objectBag = null) { } public void SetParallelLimiter(. parallelLimit) { } public void WriteError(string message) { } public void WriteLine(string message) { } diff --git a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt index b6a0ad1953..d96a686458 100644 --- a/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt +++ b/TUnit.PublicAPI/Tests.Core_Library_Has_No_API_Changes.Net4_7.verified.txt @@ -543,9 +543,6 @@ namespace public string GetDisplayName() { } public void SetDisplayName(string displayName) { } public void SetDisplayNameFormatter( formatterType) { } - [("Use AddParallelConstraint to support multiple constraints. This method replaces a" + - "ll existing constraints.")] - public void SetParallelConstraint(. constraint) { } public void SetPriority(. priority) { } public void SetRetryLimit(int retryLimit) { } public void SetRetryLimit(int retryCount, <.TestContext, , int, .> shouldRetry) { } @@ -1237,9 +1234,6 @@ namespace public .CancellationTokenSource? LinkedCancellationTokens { get; set; } public object Lock { get; } public . ObjectBag { get; } - [("Use ParallelConstraints collection instead. This property is maintained for backw" + - "ard compatibility.")] - public .? ParallelConstraint { get; set; } public .<.> ParallelConstraints { get; } public .? ParallelLimiter { get; } public string? ParentTestId { get; set; } @@ -1272,9 +1266,6 @@ namespace public .<.TestContext> GetTests(string testName, classType) { } public void OverrideResult(string reason) { } public void OverrideResult(.TestState state, string reason) { } - [("This method is non-functional after the removal of ITestFinder. It will be remove" + - "d in a future version.")] - public . ReregisterTestWithArguments(object?[]? methodArguments = null, .? objectBag = null) { } public void SetParallelLimiter(. parallelLimit) { } public void WriteError(string message) { } public void WriteLine(string message) { } diff --git a/TUnit.TestProject/Attributes/SkipMacOSAttribute.cs b/TUnit.TestProject/Attributes/SkipMacOSAttribute.cs deleted file mode 100644 index 9d245e083a..0000000000 --- a/TUnit.TestProject/Attributes/SkipMacOSAttribute.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Runtime.InteropServices; - -namespace TUnit.TestProject.Attributes; - -[Obsolete("Use `[ExcludeOnAttribute(OS.MacOS)]` instead.")] -public class SkipMacOSAttribute(string reason) : SkipAttribute(reason) -{ - public override Task ShouldSkip(TestRegisteredContext context) - { - return Task.FromResult(RuntimeInformation.IsOSPlatform(OSPlatform.OSX)); - } -} diff --git a/TUnit.TestProject/DynamicallyAddedTestsAtRuntimeTests.cs b/TUnit.TestProject/DynamicallyAddedTestsAtRuntimeTests.cs deleted file mode 100644 index ade4c9df14..0000000000 --- a/TUnit.TestProject/DynamicallyAddedTestsAtRuntimeTests.cs +++ /dev/null @@ -1,29 +0,0 @@ -#pragma warning disable -namespace TUnit.TestProject; - -public class DynamicallyAddedTestsAtRuntimeTests -{ - private static int _testRepeatLimit = 0; - - [Test] - [Arguments(1)] - public void Failure(int i) - { - throw new Exception($"Random reason: {i}"); - } - - [After(Test)] - public void CreateRepeatTestIfFailure(TestContext context) - { - // Implementation pending - intended to demonstrate dynamic test repetition on failure - // See DynamicallyRegisteredTests.cs for a working example using ReregisterTestWithArguments - // Note: ReregisterTestWithArguments is currently marked as Obsolete and non-functional - - // Example implementation (currently non-functional): - // if (context.Result?.State == TestState.Failed && _testRepeatLimit < 3) - // { - // _testRepeatLimit++; - // await context.ReregisterTestWithArguments(methodArguments: [_testRepeatLimit]); - // } - } -} diff --git a/TUnit.TestProject/DynamicallyRegisteredTests.cs b/TUnit.TestProject/DynamicallyRegisteredTests.cs deleted file mode 100644 index 78dca77ec8..0000000000 --- a/TUnit.TestProject/DynamicallyRegisteredTests.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using TUnit.Core.Interfaces; - -namespace TUnit.TestProject; - -[DynamicCodeOnly] -public class DynamicallyRegisteredTests -{ - [Test] - [DynamicDataGenerator] - public void MyTest(int value) - { - throw new Exception($@"Value {value} !"); - } -} - -public class DynamicDataGenerator : DataSourceGeneratorAttribute, ITestStartEventReceiver, ITestEndEventReceiver -{ - private static int _count; - - private readonly CancellationTokenSource _cancellationTokenSource = new(); - - protected override IEnumerable> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) - { - yield return () => new Random().Next(); - } - - public ValueTask OnTestStart(TestContext testContext) - { - if (!IsReregisteredTest(testContext)) - { - testContext.AddLinkedCancellationToken(_cancellationTokenSource.Token); - } - - return default(ValueTask); - } - - [Experimental("WIP")] - [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Dynamic Code Only attribute on test")] - public async ValueTask OnTestEnd(TestContext testContext) - { - if (testContext.Result?.State == TestState.Failed) - { - await _cancellationTokenSource.CancelAsync(); - - // We need a condition to end execution at some point otherwise we could go forever recursively - if (Interlocked.Increment(ref _count) > 5) - { - throw new Exception("DynamicDataGenerator reached maximum retry count."); - } - - if (IsReregisteredTest(testContext)) - { - // Optional to reduce noise - // testContext.SuppressReportingResult(); - } - - await testContext.ReregisterTestWithArguments(methodArguments: [new Random().Next()], - objectBag: new() - { - ["DynamicDataGeneratorRetry"] = true - }); - } - } - - private static bool IsReregisteredTest(TestContext testContext) - { - return testContext.ObjectBag.ContainsKey("DynamicDataGeneratorRetry"); - } - - public int Order => 0; -} diff --git a/docs/docs/assertions/extensibility/source-generator-assertions.md b/docs/docs/assertions/extensibility/source-generator-assertions.md index b9c32853a4..dd907f9c21 100644 --- a/docs/docs/assertions/extensibility/source-generator-assertions.md +++ b/docs/docs/assertions/extensibility/source-generator-assertions.md @@ -382,24 +382,6 @@ await Assert.That(number) --- -## Migration from CreateAssertion - -If you're using the old `CreateAssertionAttribute`: - -```csharp -// Old (still works, but deprecated): -[CreateAssertion("StartsWith")] -public static partial class StringAssertionExtensions { } - -// New: -[AssertionFrom(nameof(string.StartsWith), ExpectationMessage = "to start with {value}")] -public static partial class StringAssertionExtensions { } -``` - -The old attribute shows an obsolete warning but continues to work for backward compatibility. - ---- - ## Complete Example Here's a comprehensive example showing all features: