Skip to content

Add the Dummies.Xunit companion package for reproducible xUnit v3 tests - #300

Merged
Reefact merged 4 commits into
mainfrom
claude/generatemany-terminal-method-oxor0f
Jul 26, 2026
Merged

Add the Dummies.Xunit companion package for reproducible xUnit v3 tests#300
Reefact merged 4 commits into
mainfrom
claude/generatemany-terminal-method-oxor0f

Conversation

@Reefact

@Reefact Reefact commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

Answers the open question the 2026-07-20 Dummies audit raised: the test-framework adapter that ADR-0006's follow-ups anticipated and the ADR-0026 rebase dropped. A new Dummies.Xunit companion package supplies [Reproducible], which pins the ambient seed for a test and reports it only when the test fails — replacing the per-test Any.Reproducibly(() => { ... }) lambda on xUnit v3. Getting there needed one addition to Dummies itself: a public handle for pinning the ambient seed, since the delegate-scoped runner cannot serve a caller that observes a test from before/after hooks.

Type of change

  • New feature
  • Tests
  • Documentation
  • Build / CI / tooling

Changes

  • ADR-0038 — open the ambient seed scope to test-framework adapters (EN + FR).
  • ADR-0039 — adapt Dummies to xUnit v3 through a companion package (EN + FR).
  • Any.UseSeed(int) / Any.UseSeed(int, string) — the ambient seed scope, previously internal, exposed as a public disposable handle. The second overload carries the replay instruction a generation-failure diagnostic names: the default points at Any.Reproducibly(seed, ...), which is the wrong instruction for a run pinned from outside the test body. The scope now carries that instruction rather than the source fixing it. Public rather than an InternalsVisibleTo grant, so any adapter — including one for another framework — needs no further change to Dummies.
  • Dummies.Xunit — new netstandard2.0 package exposing ReproducibleAttribute, built on BeforeAfterTestAttribute rather than a Fact/Theory subclass. The hook is collected from the method, the class and the assembly and runs once per theory case, so one attribute covers a test, a class, a whole suite and every case of a theory — without touching test discovery.
  • Dummies.Xunit.UnitTests — 14 tests, including the architecture guard that the package references no FirstClassErrors assembly.
  • PackagingDummies.Xunit rides the existing dum train in tools/packaging/pack.sh; both projects are wired into the solution.
  • CIDummies.Xunit.UnitTests added to the framework-floor job.
  • DocsArbitraryTestValues (EN + FR), the Dummies readme, and a new Dummies.Xunit readme.

Why xUnit v3 only

The capability is "report the seed only when the test fails", which requires observing a finished test's outcome. Verified empirically against both majors:

  • v3 (3.2.2)BeforeAfterTestAttribute.After(MethodInfo, IXunitTest), with TestContext.Current.TestState carrying Result, FailureCause and the exception detail, plus TestOutputHelper.
  • v2 (2.9.3) — reflection over xunit.core shows only Before(MethodInfo) / After(MethodInfo), and no TestContext type in the assembly. The outcome is unreachable from the equivalent hook; expressing the rule there means owning test-case discovery and execution against semi-internal surface.

Users of v2, NUnit and MSTest lose nothing: Any.Reproducibly(...) is unaffected and remains the portable form.

Testing

  • dotnet build FirstClassErrors.sln — succeeds, no warnings
  • dotnet test FirstClassErrors.sln — 11 suites, 1559 tests, 0 failures
  • Analyzer tests pass (FirstClassErrors.Analyzers.UnitTests) — 132 passed, as part of the solution run

Design assumptions were verified by execution in throwaway spikes before implementation, not assumed from documentation: that an AsyncLocal written in Before reaches sync bodies, async bodies after Task.Yield(), and each theory case; that it is still readable in After; that TestState.Result reports Failed with FailureCause = Assertion on a failing test; that hooks are collected at method, class and assembly level; and that After runs in exact reverse order of Before.

Not run here: the net472 leg of Dummies.Xunit.UnitTests compiles (dotnet build -p:EnableNet472Floor=true -f net472) but could not be executed — there is no Mono on the Linux legs, which is the documented reason that job is Windows-only. Execution is the framework-floor job's to prove.

One behaviour is covered by a seam rather than end to end: a test that genuinely fails would fail the suite, so the "report only on failure" rule is decided by an internal ReportFor(bool, int) proved directly, paired with a contract test whose assertion lives in an after-hook and fails if xUnit stops exposing a finished test's outcome.

Documentation

  • Public API / error documentation updated
  • README / doc/ updated
  • French translation updated if user-facing behavior changed

doc/handwritten/for-users/ArbitraryTestValues.{en,fr}.md gained the scope form and the [Reproducible] section in lockstep; Dummies/README.nuget.md mentions UseSeed; Dummies.Xunit/README.nuget.md is new. Both ADRs ship EN + FR.

Architecture decisions

  • New decision recorded — ADR drafted as Proposed: ADR-0038 and ADR-0039

Two ADRs rather than one, because the decisions have independent lifetimes: the public seed scope stands even if the xUnit adapter were dropped, and it serves any framework.

Note on status. Both were drafted as Proposed, then flipped to Accepted on @Reefact's explicit instruction in the session — not on the agent's own initiative. They were also renumbered from 0035/0036 to 0038/0039 after main moved and claimed those numbers.

Related issues

Refs #226 — this completes the Dummies.Xunit companion-package item; the tracking issue stays open for its other candidates.


Generated by Claude Code

claude added 4 commits July 26, 2026 13:33
The audit of 2026-07-20 asks for an explicit yes/no on the test-framework
adapter that ADR-0006's follow-ups anticipated and the ADR-0026 rebase
dropped. Two decisions, one ADR each:

* ADR-0038 opens the ambient seed scope as a public, disposable handle
  carrying the replay instruction diagnostics will name. An adapter runs
  before and after a test, so it cannot use the delegate-scoped runner;
  a public handle keeps every adapter possible without privileged access.
* ADR-0039 adapts Dummies to xUnit v3 only. The capability is "report the
  seed only when the test fails": v3 exposes a finished test's outcome in
  its documented extensibility, v2 exposes none from the equivalent hook
  and would require owning test discovery and execution.

Refs: #226
Any.Reproducibly needs a delegate to wrap. A test-framework adapter
observes a test through before/after hooks and has none, so it could not
pin the ambient source at all: the disposable handle existed but was
internal.

UseSeed(int) exposes it. The second overload carries the instruction a
generation-failure diagnostic must name: the default points at
Any.Reproducibly(seed, ...), which is the wrong instruction for a run
pinned from outside the test body, where replaying means changing what
the adapter reads. The scope now carries that instruction rather than the
source fixing it, so the guarantee the hint mechanism exists for -- never
name a mechanism the reader does not use -- survives the third way of
pinning the ambient source.

Public rather than an internals grant to Dummies.Xunit, so any adapter is
possible without a further change here (ADR-0038).

The surface-parity guard excludes UseSeed the way it already excludes
WithSeed and Reproducibly: none is a generator factory, and an AnyContext
already is an explicit deterministic context.

Refs: #226
[Reproducible] pins the ambient seed for a test and reports it only when
the test fails, replacing the per-test Any.Reproducibly lambda on xUnit v3.

Built on BeforeAfterTestAttribute rather than a Fact/Theory subclass: the
hook is collected from the method, the class and the assembly and runs once
per theory case, so one attribute covers a test, a class, a whole suite and
every case of a theory -- without touching test discovery. Levels nest, and
xUnit closes them in reverse, so the most specific declaration wins for the
duration of the test and the outer ones are restored after it.

The failure-only rule reads the finished test's outcome from the ambient
test context, which xUnit v3 exposes and v2 does not; that asymmetry is why
the package targets v3 only (ADR-0039). The seed is pinned through the
public Any.UseSeed handle, so the package holds no privileged access.

Targets netstandard2.0: xunit.v3.extensibility.core is itself a
netstandard2.0 library, so nothing forces the adapter above the floor
Dummies already keeps.

Rides the dum train with Dummies. Tests cover pinning, a zero seed, the
per-case seed, scope closing, the class-level and method-level precedence,
the replay instruction reaching generation failures, and the reporting rule;
a contract test fails if xUnit stops exposing a finished test's outcome.

Refs: #226
Dummies.Xunit ships netstandard2.0, so a .NET Framework consumer loads
that asset -- but the adapter suite only ever ran on net10, leaving the
floor it claims to support unexercised. Import the shared Net472TestFloor
plumbing and add the project to the framework-floor job, the same way
Dummies.UnitTests already carries its own contract suite there (ADR-0022).

Verified: the suite compiles on net472. Running it needs Windows -- there
is no Mono on the Linux legs -- so execution is the CI job's to prove.

Refs: #226
@Reefact
Reefact merged commit 41401ec into main Jul 26, 2026
16 checks passed
@Reefact
Reefact deleted the claude/generatemany-terminal-method-oxor0f branch July 26, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants