Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: thomhurst/TUnit
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.32.0
Choose a base ref
...
head repository: thomhurst/TUnit
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v1.33.0
Choose a head ref
  • 7 commits
  • 92 files changed
  • 2 contributors

Commits on Apr 11, 2026

  1. chore(deps): update tunit to 1.32.0 (#5513)

    Co-authored-by: Renovate Bot <renovate@whitesourcesoftware.com>
    thomhurst and renovate-bot authored Apr 11, 2026
    Configuration menu
    Copy the full SHA
    a2d9a86 View commit details
    Browse the repository at this point in the history

Commits on Apr 12, 2026

  1. Configuration menu
    Copy the full SHA
    336ac83 View commit details
    Browse the repository at this point in the history
  2. Configuration menu
    Copy the full SHA
    c6915ca View commit details
    Browse the repository at this point in the history
  3. perf: engine-wide performance optimizations (#5520)

    * perf: engine-wide performance optimizations across discovery, scheduling, and reporting
    
    Fix critical hash degradation, eliminate O(n²) algorithms, reduce allocations,
    and improve thread safety across 14 files in TUnit.Core and TUnit.Engine.
    
    Key changes:
    - Fix ConstraintKeysCollection.GetHashCode() returning constant 1, restoring
      O(1) dictionary lookups (was O(n) due to hash collisions)
    - Replace O(n²) topological sort in test discovery with Kahn's algorithm O(V+E)
    - Replace ConcurrentBag with ConcurrentQueue for better enumeration performance
    - Replace 6-pass LINQ categorization in GitHubReporter with single-pass loop
    - Fix thread safety in GitHubReporter/JUnitReporter (List → ConcurrentQueue)
    - Replace GroupBy + Count() with single-pass counter loops in orchestrators
    - Cache reflection results (ExplicitAttribute, ConstructorInfo, PropertyBag)
    - Guard trace log string interpolation behind IsTraceEnabled checks
    - Add TestContext.RemoveById() to prevent memory leak in long-running hosts
    - Remove redundant EnsureTestSessionHooksExecutedAsync call
    - Add fast-path in XML sanitization to skip StringBuilder when not needed
    - Make factory lambdas static to avoid delegate allocations
    
    * fix: address code review findings
    
    - Revert ConstraintKeysCollection.GetHashCode() to constant (0) — intersection-based
      Equals means two "equal" collections can have different key sets, making a content-based
      hash violate the hash/equals contract
    - Replace SingleOrDefault<TestNodeStateProperty>() with OfType<>().FirstOrDefault()
      in GitHubReporter to avoid throwing if multiple state properties exist
    - Add comments on _latestUpdates explaining the intentional non-atomic ordering trade-off
    - Improve cycle-break comment in Kahn's algorithm noting CircularDependencyDetector
      handles the error reporting
    
    * fix: address follow-up review findings
    
    - Remove dead _updates field from JUnitReporter (only _latestUpdates is needed)
    - Fix second SingleOrDefault in GitHubReporter flaky detection loop
    - Use _latestUpdates.IsEmpty for empty check in both reporters
    - Use typed constructor lookup in MetadataFilterMatcher instead of fragile [0] indexer
    thomhurst authored Apr 12, 2026
    Configuration menu
    Copy the full SHA
    bfd4a20 View commit details
    Browse the repository at this point in the history
  4. feat: Add TUnitSettings static API for programmatic configuration (#5522

    )
    
    * feat: add TUnitSettings static API classes (#5521)
    
    * refactor: deprecate Defaults class and switch internal reads to TUnitSettings (#5521)
    
    Mark the Defaults class and all its fields as [Obsolete], pointing users
    to TUnitSettings.Timeouts.* equivalents. Migrate all internal references
    from Defaults.* to Settings.TUnitSettings.Timeouts.* to eliminate CS0618
    warnings under TreatWarningsAsErrors. Also fix a namespace collision in
    NuGetDownloader.cs caused by the new TUnit.Core.Settings namespace.
    
    * feat: wire TUnitSettings into engine for parallelism, fail-fast, and display (#5521)
    
    * test: update public API snapshots for TUnitSettings (#5521)
    
    * test: add TUnitSettings unit tests (#5521)
    
    * docs: add programmatic configuration documentation (#5521)
    
    * refactor: eliminate redundant defaults and simplify Settings access paths (#5521)
    
    - Defaults.cs fields now delegate to TUnitSettings.Timeouts (single source of truth)
    - Add `using TUnit.Core.Settings` to TUnit.Core files for consistent shorter access
    
    * fix: address review feedback — remove DisableLogo, lazy FailFast, validate parallelism (#5521)
    
    - Remove DisplaySettings.DisableLogo (banner fires before discovery hooks, making it useless as a code setting)
    - Make FailFast check lazy in TestRunner (reads TUnitSettings.Execution.FailFast at failure time, not at construction)
    - Add setter validation on MaximumParallelTests (reject negative values)
    - Document that MaximumParallelTests is read before discovery hooks
    - Update public API snapshots and docs
    
    * fix: address second review — clean up FailFast redundancy, revert Defaults alias, fix docs (#5521)
    
    - Remove TUnitSettings.Execution.FailFast from TUnitServiceProvider (eager capture).
      TestRunner already checks it lazily at failure time — no double-check needed.
    - Revert Defaults.cs to hardcoded values (static readonly fields can't track
      mutable TUnitSettings; stale alias is worse than honest deprecation).
    - Remove MaximumParallelTests from discovery hook example in docs (silently
      ignored due to scheduler timing) and add a note about the limitation.
    
    * docs: fix misleading parallelism example and document hook timeout ordering (#5521)
    
    - Replace discovery-hook example in parallelism.md with CLI/env-var approach
      (MaximumParallelTests is read before discovery hooks run)
    - Qualify "When to Set" section to note MaximumParallelTests exception
    - Document DefaultHookTimeout ordering constraint in XML doc
    
    * fix: address review round 5 — doc gap, test guard, dead code
    
    - Document DefaultHookTimeout timing exception in "When to Set" section
    - Add Before/After hooks to TUnitSettingsTests to snapshot and restore
      static state, making Defaults_Are_Correct resilient to prior mutations
    - Remove dead negative-value branch in GetMaxParallelism (setter already
      validates)
    
    * fix: add thread-safe volatile reads/writes to boolean settings
    
    Use Volatile.Read/Write for FailFast and DetailedStackTrace backing
    fields so cross-thread visibility is guaranteed when settings are
    configured in a discovery hook and read during parallel test execution.
    
    * fix: remove Volatile from boolean settings for consistency
    
    Revert to plain auto-properties on all settings classes. The framework's
    lifecycle guarantees that discovery hooks complete before test threads
    start, so no per-field synchronization is needed. Volatile cannot be
    applied uniformly to TimeSpan/int? types, so mixing patterns was worse
    than relying on the existing happens-before guarantee.
    
    Added threading contract doc comment on TUnitSettings.
    
    * fix: Defaults delegates to TUnitSettings, add TimeoutSettings validation
    
    - Change Defaults fields from static readonly to computed properties that
      delegate to TUnitSettings, so deprecated consumers see the correct
      value even after programmatic configuration.
    - Add setter validation to all TimeoutSettings properties — reject
      TimeSpan.Zero and negative values with ArgumentOutOfRangeException,
      consistent with ParallelismSettings.
    - Update public API snapshots for Defaults field→property change.
    
    * feat: make TUnitSettings non-static, expose via BeforeTestDiscoveryContext
    
    TUnitSettings is now a sealed class (not static) with an internal-only
    Default singleton. Users access settings exclusively through
    context.Settings in a [Before(HookType.TestDiscovery)] hook, which
    naturally enforces the correct lifecycle timing.
    
    Engine code uses TUnitSettings.Default.* via InternalsVisibleTo.
    Removed TUnitSettingsAccessor (no longer needed). Updated all docs
    to reference context.Settings instead of the static API.
    
    * fix: allow TimeSpan.Zero for ProcessExitHookDelay
    
    ProcessExitHookDelay is a delay, not a timeout — zero is a valid value
    meaning "no delay". Only reject negative values, unlike the three actual
    timeout properties which reject zero (a zero-duration timeout would
    cause immediate cancellation).
    
    * feat: defer MaximumParallelTests read with Lazy<T>
    
    Use Lazy<int> for _maxParallelism and Lazy<SemaphoreSlim?> for the
    semaphore so the value is computed on first access (during test
    execution) rather than at TestScheduler construction (before discovery
    hooks). Users can now set MaximumParallelTests in a
    [Before(HookType.TestDiscovery)] hook and have it take effect.
    
    Closes #5523
    
    Removed timing caveats from ParallelismSettings XML doc and
    programmatic-configuration.md since the limitation no longer exists.
    
    * refactor: remove redundant comments and resolve Lazy<T> values once in hot paths
    
    * docs: remove DefaultHookTimeout from usage example
    
    It is captured at hook registration time before discovery hooks run,
    so setting it in a [Before(HookType.TestDiscovery)] hook has no effect.
    The "When to Set" section already documents this exception.
    
    * fix: defer DefaultHookTimeout resolution to execution time
    
    HookMethod.Timeout now defaults to null instead of eagerly capturing
    TUnitSettings.Default.Timeouts.DefaultHookTimeout at registration time.
    HookTimeoutHelper resolves the fallback lazily at execution time, so
    discovery-hook configuration is respected — matching the pattern already
    used for FailFast and MaximumParallelTests.
    
    * refactor: make sub-settings constructors internal
    
    Users should access settings via context.Settings, not by constructing
    new instances directly. Internal constructors prevent creating orphaned
    instances that have no connection to the engine.
    thomhurst authored Apr 12, 2026
    Configuration menu
    Copy the full SHA
    5c72b1b View commit details
    Browse the repository at this point in the history
  5. perf: reduce allocations and improve hot-path performance (#5524)

    * perf: reduce allocations and improve hot-path performance across engine
    
    Key optimizations:
    - Lazy-init Context output state (StringBuilder, RWLS, ConsoleLineBuffer) with thread-safe LazyInitializer — most test contexts never capture output
    - Volatile cached array for log sinks (eliminates lock + ToArray on every Console.Write)
    - Replace ConcurrentBag with List for sequential-write collections (Timings, Artifacts)
    - O(1) duplicate detection in ClassHookContext via generic ReferenceEqualityComparer<T>
    - Parallel test registration with Parallel.ForEachAsync for 8+ tests
    - HashSet-based UID filter lookup instead of O(N) list scan
    - CTS allocation fast-path in HookTimeoutHelper when no timeout configured
    - Targeted EventReceiverRegistry cache invalidation instead of blanket Clear()
    - Deferred StateBag/Events allocation in TestBuilderContext
    - Eliminate LINQ allocations in hot paths (TestBuilder, TestExtensions, TestFilterService)
    - Conditional List<Exception> allocation in test teardown
    
    * fix: address PR review feedback
    
    - Revert SingleOrDefault to OfType<T>().FirstOrDefault() in GitHubReporter
      (SingleOrDefault throws on >1 match, breaking defensive resilience)
    - Use LazyInitializer.EnsureInitialized for _uidFilterSet in TestFilterService
      (consistent thread-safe lazy init pattern across the PR)
    - Extract TimeSpan.FromDays(1) to static readonly field in HookTimeoutHelper
      (avoid recomputation on every call)
    
    * fix: address second review — thread-safe artifacts and documented parallelism
    
    - Revert _artifacts to thread-safe collection (Lock + List instead of
      ConcurrentBag) since AttachArtifact is user-facing and can be called
      from parallel Task.WhenAll branches within a single test
    - Document concurrency contract on RegisterTestsAsync: per-test state is
      isolated, shared services use ConcurrentDictionary, and
      ITestRegisteredEventReceiver implementations must be thread-safe
    
    * fix: simplification review — bug fix, efficiency, and cleanup
    
    - Fix ClassHookContext.RemoveTest not removing from _testSet (would
      silently drop re-added tests after removal)
    - Add null-check guard in GetOrCreateUidFilterSet to avoid closure
      allocation on every MatchesTest call after initialization
    - Capture testContext.Artifacts once in TestExtensions to eliminate
      double lock acquisition and double array copy
    - Trim narrating comment in Context.cs
    
    * docs: add thread-safety note to ITestRegisteredEventReceiver
    
    OnTestRegistered may be called concurrently when many tests are
    registered. Document this contract at the interface level so
    implementors are aware.
    
    * chore: accept public API snapshots for ReferenceEqualityComparer<T>
    
    New additive-only public API: ReferenceEqualityComparer<T> in
    TUnit.Core.Helpers. No breaking changes — existing APIs unchanged.
    
    * fix: make ReferenceEqualityComparer<T> internal
    
    Only used internally by ClassHookContext — no need to expose as
    public API. Reverts the snapshot changes since the type no longer
    appears in the public API surface.
    thomhurst authored Apr 12, 2026
    Configuration menu
    Copy the full SHA
    28a1d96 View commit details
    Browse the repository at this point in the history
  6. +semver:minor - fix: enforce ParallelLimiter semaphore in TestRunner …

    …to prevent DependsOn bypass (#5526)
    
    * fix: enforce ParallelLimiter semaphore in TestRunner to prevent DependsOn bypass (#5525)
    
    The parallel limiter semaphore was acquired in TestScheduler and
    ConstraintKeyScheduler, but TestRunner.ExecuteTestInternalAsync
    executes dependency tests via a direct recursive call that bypassed
    the semaphore entirely. When a test without [ParallelLimiter] but
    with [DependsOn] triggered its dependencies, those dependencies
    could run without holding a semaphore slot — exceeding the declared
    concurrency limit (e.g. peak=3 with Limit=2).
    
    Move semaphore acquisition into TestRunner.ExecuteTestInternalAsync,
    after dependency resolution but before the test body, so it applies
    regardless of entry point (scheduler or dependency recursion).
    
    * fix: reset static concurrency counters in Before(Class) hook
    
    Prevents stale s_peak from accumulating across re-runs in the same
    process (e.g. retry logic or IDE re-runs).
    
    * test: add check test with same limiter on dependent
    
    Covers the two-phase acquire path where the depending test shares
    the same ParallelLimiter as its dependencies.
    thomhurst authored Apr 12, 2026
    Configuration menu
    Copy the full SHA
    410fd39 View commit details
    Browse the repository at this point in the history
Loading