This document is organized by topic to consolidate key learnings about the project's architecture, implementation, and testing.
- Namespaces: The main library code resides in the
SharpAssertnamespace, but the publicSharp.AssertAPI is exposed in the global scope for ease of use. The customSharpAssertionExceptionlives within theSharpAssertnamespace. - Test Project: The test project (
SharpAssert.Tests) should share the sameRootNamespaceas the main project (SharpAssert) but exist in its own separate physical namespace to mirror the structure of the code under test. - File Organization:
/SharpAssert/Sharp.cs: Public API entry point./SharpAssert/SharpAssertionException.cs: Custom exception./SharpAssert/SharpInternal.cs: Internal API for rewriter targets./SharpAssert/ExpressionAnalyzer.cs: Core expression tree visitor./SharpAssert.Tests/: Contains all test fixtures.
- Build Configuration:
- The project targets
net9.0to leverage modern C# language features. - Careful attention is needed for compatibility with features like file-scoped namespaces and implicit usings when working with the rewriter.
- The project targets
- Single Evaluation Principle: A core requirement is to evaluate each operand and sub-expression only once. This is achieved by using a visitor pattern (
ExpressionAnalyzer) combined with aConcurrentDictionaryto cache the results of evaluated sub-expressions. - Expression Tree Limitations: Expression trees cannot contain local functions. All logic must be implemented as instance or static methods.
- Binary Comparisons (
==,!=,>, etc.):- These are analyzed to extract the final values of the left and right operands for inclusion in the failure message.
- A
BinaryOpenum is used to represent all C# comparison operators.
- Logical Operators (
&&,||,!):- These require special handling separate from binary comparisons.
&&maps toExpressionType.AndAlso,||toExpressionType.OrElse, and!toExpressionType.Not.- Short-Circuiting: The natural short-circuiting behavior of
&&and||is preserved by evaluating the entire expression first and only analyzing the sub-expressions if the assertion fails. This avoids artificially enforcing evaluation rules. - The
!operator is aUnaryExpressionand requires its own handling path.
- Standard Failure Message: The exception format is inspired by pytest:
Assertion failed: {expr} at {file}:{line}. - Binary Comparison Message: For failed binary comparisons, the message is augmented with the evaluated operands: Left: {value} Right: {value}
- Logical Operator Message: For failed logical operations, the message shows the truthiness of the operands to provide context (e.g., "Left operand was false" for a short-circuited
&&). - Readability: Private helper methods are used extensively to keep the formatting logic clean and maintainable.
- Core Technology: The rewriter is implemented using Roslyn's
CSharpSyntaxRewriterto perform AST transformation. - Invocation Detection: Simple and fast identifier matching (
identifier.Identifier.ValueText == "Assert") is sufficient to findSharp.Assertcalls, avoiding the need for a full semantic model resolution in many cases. - Lambda Creation:
SyntaxFactory.ParenthesizedLambdaExpression()is used to wrap the user's expression into a lambda (() => ...). The factory produces compact but functionally correct code. - Async Prevention: The rewriter detects
awaitexpressions (.DescendantNodes().OfType<AwaitExpressionSyntax>().Any()) and skips rewriting them to avoid generating invalid expression trees. - MSBuild Integration:
- The rewrite task is injected
BeforeTargets="CoreCompile"to ensure it runs before the compiler. - The output is written to a standard pattern:
$(IntermediateOutputPath)SharpRewritten\**\*.sharp.g.cs.
- The rewrite task is injected
- Graceful Fallback: This is a critical principle. The MSBuild task is designed to fail gracefully. If the rewriter encounters an error, it copies the original source file, ensuring that a rewriter bug does not break the user's build.
- Overload Gotcha: Because the rewriter builds a semantic model per-file (single syntax tree), adding overloads to
Sharp.Assert(...)can make binding ambiguous for expressions that reference symbols from other files and cause those calls to stop rewriting; prefer a distinct entry point name (e.g.,AssertThat) for non-bool asserts. - Single Assert Alternative: A single
Assert(AssertValue ...)entry point avoids overload ambiguity; the rewriter can always rewrite toSharpInternal.AssertValue(Expression<Func<AssertValue>> ...)and route at runtime based on whichop_Implicitconversion the expression tree uses. - Conversion Gotchas: C# forbids user-defined conversions to/from interfaces (so
AssertValuecannot convert fromIExpectation), and only allows one user-defined conversion in a chain (so types withimplicit operator boolmay need a direct conversion toAssertValueunless they inheritExpectation). - Avoid bool Conversions on Expectations: Keeping
Throws<T>results purely asExpectation(noimplicit operator bool) keeps!Throws(...)and composition (.Not()/.And()/.Or()) on the expectation path without ExpressionAnalyzer special casing. - ExprNode for Method Calls: For invocation expressions, capturing the receiver (e.g., as
ExprNode.Left) in addition to arguments enables expectation composition to label operands (receiver vs argument) accurately. - Expectation Operators: Supporting
&/|forExpectationcomposition requires ExprNode generation for&/|and composition evaluators that understandExprNode.Left/Right. - Async Rewrite Discrimination:
Assert(await ...)must still rewrite toSharpInternal.AssertAsyncfor awaited booleans, butAssert(await ThrowsAsync(...))must NOT be rewritten (expression trees can't contain await andAssertAsyncexpectsTask<bool>); discriminating by awaitingTask<bool>/ValueTask<bool>keeps both working. - Expectation Ergonomics: Prefer extension methods that construct expectations (e.g.,
4.IsEven()) and suffix expectation types withExpectationfor clean call sites. - Two Construction Styles: Provide both
using staticfactories (great for unary:Assert(IsEven(4))) and extension methods (great for binary/parameterized:Assert(actual.IsEquivalentTo(expected))). - Record Inheritance Gotcha:
recordtypes can only inherit fromobjector anotherrecord, soExceptionResult<T>cannot be a record if it must inherit theExpectationbase class. - Line Directive Implementation:
- Use
SyntaxFactory.PreprocessingMessage()instead ofSyntaxFactory.LineDirectiveTrivia()for proper formatting of #line directives - Only add #line directives when actual rewrites occur to preserve unchanged files
- Track rewrite state with
HasRewritesproperty to conditionally add file-level #line directive - #line directives enable proper stack traces and debugging in original source files
- Use
- Line Number Tracking (Phase 4):
GetLineSpan(node.Span).StartLinePosition.Line + 1correctly extracts line numbers from syntax nodes- Multi-line expressions correctly use the start line of the Assert call for line mapping
- Line numbers flow through the entire rewriting pipeline via the 4th parameter to SharpInternal.Assert
- #line directives around rewritten Assert calls ensure accurate debugging and stack traces
- Public API: The user-facing API is minimal, relying on
[CallerArgumentExpression],[CallerFilePath], and[CallerLineNumber]to capture the assertion context automatically. These attributes work seamlessly in .NET 9.0. - Dependencies:
FluentAssertionsis used within the test suite (SharpAssert.Tests) for creating readable and maintainable test assertions.
- Unit Tests: The core rewriter functionality is tested using "golden file" tests, comparing the output of the rewriter against a known-good rewritten source file.
- Integration Tests: End-to-end tests are run on a sample project where
EnableSharpLambdaRewriteis set totrue, verifying that the entire process (rewrite -> compile -> run -> fail) works as expected. - MSBuild Task Testing: Direct unit testing of SharpLambdaRewriteTask provides fast, comprehensive coverage without MSBuild complexity. Key insights:
- MSBuild tasks require a
BuildEngineto log messages - use MockBuildEngine in tests - Test the task directly by setting properties and calling Execute()
- Verify file processing, error handling, and configuration properties
- Use temp directories for isolated file operations in tests
- MSBuild tasks require a
- MSBuild Task Enhancement (Phase 3): Key improvements for production-ready MSBuild tasks:
- Add [Output] properties for MSBuild to track generated files
- Implement comprehensive generated file detection (AssemblyInfo.cs, GlobalUsings.g.cs, .designer., etc.)
- Use MessageImportance.Low for diagnostic messages to respect MSBuild verbosity levels
- Maintain backward compatibility - existing tests expect files without Assert calls to be skipped entirely
- Provide detailed file mapping diagnostics for troubleshooting rewriter issues
- Fixtures: Test fixtures are organized by functionality (e.g.,
AssertionFixture,ExpressionAnalysisFixture,RewriterFixture,SharpLambdaRewriteTaskFixture).
ReferenceOutputAssembly="false"Pattern: Critical for MSBuild task testing - include rewriter project in build order without assembly reference pollution- Direct
.targetsImport Strategy: Import.targetsfile directly instead of via NuGet package for true integration testing without packaging overhead SharpAssertRewriterPathOverride: Custom property to point to local development build instead of packaged tools directory ($(MSBuildThisFileDirectory)..\SharpAssert.Rewriter\bin\$(Configuration)\net9.0\)- MSBuild Task Assembly Loading: Tasks loaded via
AssemblyFilein UsingTask are cached by MSBuild process - recompiling rewriter doesn't reload task without MSBuild restart - Generated File Directory Pattern:
$(IntermediateOutputPath)SharpRewritten\provides predictable output location separate from source files - Design-Time Build Exclusion: Critical to exclude rewriter during
$(DesignTimeBuild)and$(BuildingForLiveUnitTesting)to preserve IDE experience - File Pattern Exclusion Logic: Complex but necessary exclusion patterns for generated files:
Microsoft.NET.Test.Sdk.Program.cs,*.AssemblyInfo.cs,*.GlobalUsings.g.cs
- Separate Solution Files: Main solution (
SharpAssert.sln) vs package testing (SharpAssert.PackageTesting.sln) prevents dev workflow contamination - NuGet Source Mapping:
packageSourceMappinginnuget.package-tests.configensures SharpAssert packages only come from local feed, preventing version conflicts - Package Cache Isolation: Use
--packages ./test-packagesfor isolated cache that doesn't pollute global NuGet cache - File Linking with MSBuild:
<Compile Include="..\SharpAssert.IntegrationTests\**\*.cs" Link="..."/>shares test files without duplication - Wildcard Version References:
Version="1.0.0-dev*"allows testing against latest local builds without hardcoding timestamps - Config File Isolation: Separate
nuget.package-tests.configwith<clear/>prevents interference from user/machine level configs
- Conditional Project/Package References: Use
Condition="Exists('..\SharpAssert.Runtime\SharpAssert.csproj')"to enable local development with ProjectReference while maintaining PackageReference for NuGet packaging - Dependency Order in Packaging: When packages depend on each other, pack dependencies first then reference local feed:
--source local-feed --source https://api.nuget.org/v3/index.json - Single Package Strategy: Users prefer installing one main package (e.g., "SharpAssert") with transitive dependencies (e.g., "SharpAssert.Runtime") rather than multiple packages
- Wildcard Version Matching:
Version="1.0.0-dev*"enables flexible local development while maintaining precise version control in production
- Multi-Layer Testing Strategy Benefits: Unit (fast dev) → Integration (MSBuild behavior) → Package (real-world usage) → CI (clean environment)
- Timestamp-Based Dev Versions:
1.0.0-dev20250812155111pattern enables rapid iteration without version conflicts - Cache Management Critical: NuGet cache pollution is a major source of "works on my machine" issues - isolated caches prevent false positives
- MSBuild vs Package Testing Trade-offs: Integration tests faster but miss packaging issues; package tests slower but catch real deployment problems
- Script-Based Automation:
./dev-test.sh(fast dev cycle) vs./test-local.sh(full validation) provides appropriate tool for each workflow stage
- MSBuild Property Evaluation Order: Properties must be defined before UsingTask -
SharpAssertRewriterPathmust have default before being used in AssemblyFile - NuGet Package Source Precedence: Without source mapping, higher priority sources can override local packages even with version wildcards
- Project.Assets.Json Staleness: Incremental MSBuild restore doesn't always detect new packages with same version pattern - requires
--force-evaluate - Assembly Loading Context: MSBuild tasks run in separate AppDomain - assembly conflicts between task dependencies and target project dependencies
- Generated File Cleanup:
dotnet cleandoesn't automatically remove custom output directories - requires explicit<RemoveDir>target withBeforeTargets="Clean" - MSBuild Incremental Builds: Custom targets without
Inputs/Outputsattributes run on every build - proper tracking requires moving ItemGroups outside target scope for evaluation - Cross-Platform Path Handling: MSBuild path handling differences between Windows/Unix require careful attention to separators and absolute vs relative paths
- Package Source Discovery: NuGet source discovery during package testing can fail silently if local feed structure is incorrect - verify with
--verbosity detailed - DateTime Culture-Dependent Formatting: DateTime.ToString() produces different output across platforms (macOS: "1/1/2023", Linux: "01/01/2023 00:00:00") - use
dt.ToString("M/d/yyyy", CultureInfo.InvariantCulture)for consistent cross-platform formatting in error messages - DynamicInvoke Cross-Platform Issues: Using
Expression.Lambda(expression).Compile().DynamicInvoke()can fail on CI/certain runtimes withNotSupportedException: Specified method is not supported- use strongly-typedExpression.Lambda<Func<object>>(Expression.Convert(expression, typeof(object))).Compile()()for direct invocation without reflection - Expression.Compile InvalidProgramException: Expression compilation can emit invalid IL on some runtimes; prefer
Compile(preferInterpretation: true)or catchInvalidProgramExceptionand recompile in interpreted mode to ensure value extraction works on CI - ByRef-like expression evaluation: Ref structs (e.g.,
Span<T>,ReadOnlySpan<T>) cannot be boxed toobject; convert them to arrays (e.g.,MemoryExtensions.ToArray) before value extraction to avoid interpreterTypeLoadException/ArgumentException. - Reflection resilience: Do not assume specific overloads exist across runtimes; when reflecting for helpers like
MemoryExtensions.ToArray, select via tolerant predicate and allow null fallback to avoid type initializer failures. - Compilation fallback breadth: Expression compilation can also fail with
TypeLoadException/ArgumentExceptionon byref-like conversions; wrap compilation and fall back to interpreted mode or a null-returning delegate when both paths fail. - Interpretation-first evaluation: Prefer
Lambda.Compile(preferInterpretation: true)for expression value extraction to avoid invalid IL generation issues; only return null when both interpreted compilation and span-to-array normalization fail. - Evaluation sentinels: When evaluation fails, return a recognizable sentinel (e.g.,
EvaluationUnavailable) and have formatters render<unavailable: reason>instead of null/incorrect data to keep diagnostics honest. - NUnit Assert.ThrowsAsync return type:
Assert.ThrowsAsync<T>()returns the exception instance, not aTask, so async test methods need an explicit awaitable (e.g.,action.Should().ThrowAsync<T>()) to avoid CS1998 warnings.
- DiffPlex Character-Level Diffs: Use
Differ.CreateCharacterDiffs()for single-line strings to show precise character changes likeh[-e][+a]llo - DiffPlex Line-Level Diffs: Use
Differ.CreateLineDiffs()for multiline strings to show- line2and+ MODIFIEDformat - String vs Object Detection: Check
leftValue is string && rightValue is stringfor pure string-string comparisons, plus handle mixed null cases - Test-Driven Approach: Write failing tests first, verify they fail for right reasons (PowerAssert fallback), then implement feature and update test expectations
- FluentAssertions Wildcard Matching: Use
*pattern*in AssertExpressionThrows expectations to match partial message content flexibly
- IEnumerable Detection: Use
IsEnumerable(value) => value is IEnumerable && value is not stringto identify collections while excluding strings - Materialization Strategy: Convert IEnumerable to
List<object?>once to avoid re-enumeration issues when analyzing differences - First Difference Algorithm: Use linear scan with index tracking to find first non-equal elements for precise error location
- Missing/Extra Elements Detection: Compare collection lengths and use
Skip()+Take()for efficient subset extraction - Collection Preview Formatting: Limit preview to first N elements with "... (X items)" suffix for large collections
- List Reference Equality: List uses reference equality by default, making
list1 == list2perfect for testing collection formatter triggering - Expression Type Verification: Collections trigger BinaryExpression with NodeType.Equal, confirming proper expression tree analysis path
- Value Formatting Strategy: Use pattern matching (
null => "null",string s => $"\"{s}\"",_ => value.ToString()!) for consistent display
- TestBase Utility Methods: All test fixtures should inherit from TestBase and use the provided utility methods for consistency
AssertExpressionThrows<T>()- For testing expected exception scenarios with message pattern matchingAssertExpressionDoesNotThrow()- For testing successful assertion scenarios
- CollectionComparisonFixture Inconsistency: Originally called
SharpInternal.Assert()directly instead of using TestBase utilities, breaking the established pattern used by other fixtures like LogicalOperatorFixture, BinaryComparisonFixture, StringComparisonFixture - Expression Tree Pattern: All test fixtures should use
Expression<Func<bool>> expr = () => condition;pattern for proper expression tree creation in tests - Parameter Order Awareness: SharpInternal.Assert signature: (condition, expr, file, line, message=null, usePowerAssert=false) - must pass parameters in correct order
- CompareNETObjects Package: Use package name
CompareNETObjects(notKellermanSoftware.CompareNETObjects) and namespaceKellermanSoftware.CompareNetObjects - CompareLogic Configuration: Simple configuration approach
var logic = new CompareLogic(); logic.Config.MaxDifferences = MaxObjectDifferences;is more reliable than complex object initializers - Object Detection Strategy: Exclude strings (handled by StringComparisonFormatter), IEnumerable (handled by CollectionComparisonFormatter), primitives, and common value types from object comparison
- Property Path Formatting: CompareNETObjects provides property paths like "Address.City" automatically - no need to manually construct nested paths
- Test Pattern Consistency: All ObjectComparisonFixture tests should use the same
Expression<Func<bool>> expr = () => condition;pattern with AssertExpressionThrows/DoesNotThrow from TestBase - Configuration Constants: Use hardcoded constants like
MaxObjectDifferences = 20following same pattern as StringDiffer, rather than premature configuration system integration - Custom Equals Handling: C#
==operator naturally respects overridden Equals methods - no special handling needed in formatter, just let binary comparison work normally
- DiffPlex Import Requirements: Need both
using DiffPlex.Model;and the qualifiedDiffResulttype for proper compilation - SequenceEqual Method Detection: Added to ExpressionAnalyzer MethodCallExpression handling alongside Contains/Any/All for LINQ operations
- Unified Diff Strategy: Use DiffPlex
Differ.CreateLineDiffs()for sequence comparison, treating each element as a "line" - Materialization Pattern: Convert IEnumerable to List<object?> once to avoid multiple enumeration issues during analysis
- Length Mismatch Handling: Special case formatting when sequences have different lengths vs different content
- Custom Comparer Detection: Check
methodCall.Arguments.Countto detect when custom IEqualityComparer is provided - Truncation Logic: Apply MaxDiffLines limit to unified diff output with "truncated" message for large diffs
- Static Extension Support: Works with both instance method syntax (
seq1.SequenceEqual(seq2)) and static syntax (Enumerable.SequenceEqual(seq1, seq2)) - Test Structure Consistency: Used same pattern as LinqOperationsFixture with nested TestFixture classes (PositiveTestCases, FailureFormatting, StaticExtensionMethods)
- AssertAsync Method Design: Create async method with signature
AssertAsync(Func<Task<bool>>, string, string, int)for basic async support - Minimal Diagnostics Philosophy: For async cases, provide basic failure information (expression text and "Result: False") rather than complex expression tree analysis
- Exception Propagation Strategy: Let async exceptions bubble up naturally - don't catch and wrap them unless necessary
- BinaryOp Enum Addition: Required for future async binary comparison support (Increment 11), defines comparison operators: Eq, Ne, Lt, Le, Gt, Ge
- Test Migration Strategy: Convert ignored placeholder tests to real async tests using FluentAssertions async assertion patterns (
await action.Should().NotThrowAsync()) - Async Context Preservation: AssertAsync naturally preserves SynchronizationContext through proper async/await usage - no special handling needed
- Async Test Patterns: Use
async Tasktest methods withawaitfor assertions, test both success and failure paths with appropriate exception expectations
- Rewriter Architecture: Async binary comparisons are handled at the rewriter level, since they are rewritten before becoming expression trees
- Binary Operation Detection: Use
IsBinaryOperation()to detect comparison operators: ==, !=, <, <=, >, >= - requires proper parentheses grouping for operator precedence - SyntaxKind Token Names: Use correct Roslyn token names:
LessThanEqualsTokenandGreaterThanEqualsToken(not the incorrectLessThanOrEqualToken) - Async Thunk Generation: Generate thunks for both operands -
async () => operandfor await expressions,() => Task.FromResult<object?>(operand)for sync - Source Order Evaluation: AssertAsyncBinary evaluates left operand first, then right operand (
await leftAsync(); await rightAsync()) preserving source order as required - Formatter Reuse: Async binary formatting reuses existing IComparisonFormatter infrastructure (StringComparisonFormatter, CollectionComparisonFormatter, etc.)
- Type System Integration: Cast nullable types properly with
SingletonSeparatedList<TypeSyntax>(objectType)for SyntaxFactory TypeArgumentList - BinaryOp Enum Mapping: Map SyntaxKind tokens to BinaryOp enum values for runtime evaluation: EqualsEqualsToken => "Eq", etc.
- Test Strategy: Use FluentAssertions
.Where(ex => ex.Message.Contains(...))pattern for async exception testing with complex message validation
- Dynamic Language Runtime (DLR) Integration: Use
(dynamic?)leftValue == (dynamic?)rightValuepattern for dynamic operator semantics inEvaluateDynamicBinaryComparison - Exception Handling Strategy: Wrap dynamic operations in try-catch blocks and return false on failure - let comparison fail gracefully rather than propagate exceptions
- Formatter Reuse: Dynamic binary comparisons reuse existing
IComparisonFormatterinfrastructure (StringComparisonFormatter, CollectionComparisonFormatter, etc.) for consistent diff output - Minimal Diagnostics Philosophy: For general dynamic expressions, provide basic failure information (expression text + "Result: False") following same pattern as async support
- Method Signature Consistency: Both
AssertDynamicandAssertDynamicBinaryfollow same parameter pattern as async counterparts for consistency - Test Pattern Migration: Convert ignored placeholder tests to real implementation tests using FluentAssertions
.Should().Throw<T>()and.Should().NotThrow()patterns - Object Type Casting: Dynamic thunks cast to
object?for compatibility with comparison formatter system that expects object references
- Await Expression Construction: Must use explicit token with spacing to avoid concatenation -
SyntaxFactory.Token(TriviaList(), SyntaxKind.AwaitKeyword, TriviaList(Space))preventsawaitglobal::bug - Line Directive Placement: Preprocessor directives must appear as first non-whitespace on line - attach to outer expression (AwaitExpression) not inner (InvocationExpression)
- Trivia Attachment Strategy: Use
WithLeadingTrivia()andWithTrailingTrivia()on the outermost rewritten node to ensure directives wrap entire statement correctly - Spacing Between Tokens: Always use
SyntaxFactory.Spacein trailing trivia of keywords when followed by identifiers/expressions - Return Type Changes: Changing rewrite method return types (e.g., InvocationExpression to AwaitExpression) requires new trivia attachment methods to handle different node types
- Line Directive Format: Use
SyntaxFactory.PreprocessingMessage($"#line {lineNumber} \"{escapedPath}\"")for file/line mapping,PreprocessingMessage("#line default")to reset - Trivia Ordering: Leading trivia order: original trivia → line directive → newline; Trailing trivia order: newline → default directive → newline → original trivia
- Expression Wrapping: When wrapping expressions (e.g., adding await), construct inner expression first, then wrap with factory methods, then attach trivia to outer node
- Decoupled Testing: Separated logic verification (
LogicTests) from formatting verification (FormattingTests) to reduce brittleness and improve maintenance. - Structural Verification: Using
EvaluationResultrecord hierarchy to verify assertion logic structurally viaAssertFails(action, expectedResult)usingBeEquivalentTostructural equality. - Rendering Verification: Using
AssertRendersExactly(result, lines...)to verify string output independent of logic execution. - DSL Helpers:
TestBaseprovides composable helpers likeBinaryComparison,Value,Operandto construct expected results concisely. - Nested Fixtures: Grouping tests by concern (
LogicTestsvsFormattingTests) keeps fixture files organized. - Result Records: Rendering logic pushed down to data records (e.g.
InlineStringDiff.Render()), making them self-rendering and composable.
- Context Propagation: Child context gets
[index]appended to expression (e.g.,numbers.Each(...)[1]) for precise failure location. - No Short-Circuit: Evaluate ALL items to report ALL failures - complete diagnostics more useful than early exit.
- Vacuous Truth: Empty collections pass for Each/None (no items can fail/violate), but fail for Some/One (no items can satisfy).
- Two Overloads Pattern:
Func<T, Expectation>for composed expectations,Expression<Func<T, bool>>for simple predicates. - PredicateExpectation Wrapper: Internal class converts bool predicates to Expectation via
Expression.Body.ToString()for diagnostic text. - Rendering Pattern:
CollectionQuantifierResultshows summary ("3 of 5 failed") then iterates failures with[index]: BooleanValueformat. - AssertRendersExactly Gotcha: Test helper ignores
IndentLeveland just joinsText- test expectations shouldn't include leading spaces. - CollectionQuantifierResult Passed Parameter: Each quantifier computes its own pass/fail logic (e.g.,
passCount > 0for Some,passCount == expectedCountfor Exactly) and passes it to the result constructor;BooleanValuederives fromPassedrather than hardcoded logic. - Inverted Semantics (None): For None quantifier, items that PASS the inner expectation are VIOLATIONS - collect "violations" (passes) not "failures".
- Context-Dependent Failure (One/Exactly): Show failures when too few match, show extra passes when too many match - what's "relevant" depends on which direction we're off by.
- Quantifier Equivalences:
AtLeast(0)always passes,AtLeast(1)≈Some,AtMost(0)≈None,Exactly(0)≈None,Exactly(1)≈One.