diff --git a/docs/adr/0001-record-architecture-decisions.md b/docs/adr/0001-record-architecture-decisions.md new file mode 100644 index 0000000..a32da3a --- /dev/null +++ b/docs/adr/0001-record-architecture-decisions.md @@ -0,0 +1,27 @@ +# 1. Record architecture decisions + +## Status + +Accepted + +## Context + +Wolfgang.Etl.TestKit is a small but long-lived pair of NuGet packages (the test +doubles and the xUnit contract-test base classes) maintained across many release +cycles, often by different contributors and automated agents. Several non-obvious +design choices — the pinned `AssemblyVersion`, the split into two packages, the +injectable progress timer — are easy to accidentally undo in a later change +because the *reasoning* lives only in commit messages or a reviewer's memory. + +## Decision + +We will keep Architecture Decision Records in `docs/adr/`, one Markdown file per +decision, in the Nygard format (Context / Decision / Consequences). Records are +immutable once accepted; a changed decision is captured as a new, superseding ADR. + +## Consequences + +- The rationale behind load-bearing choices is discoverable next to the code. +- Reviewers can point at an ADR instead of re-litigating a settled decision. +- There is a small ongoing cost: a genuinely architectural change should come + with an ADR, not just code. diff --git a/docs/adr/0002-pin-assemblyversion-for-binding-stability.md b/docs/adr/0002-pin-assemblyversion-for-binding-stability.md new file mode 100644 index 0000000..24783c2 --- /dev/null +++ b/docs/adr/0002-pin-assemblyversion-for-binding-stability.md @@ -0,0 +1,33 @@ +# 2. Pin AssemblyVersion at 1.0.0.0 for binding stability + +## Status + +Accepted + +## Context + +Both packages ship to NuGet and target .NET Framework TFMs (net462, net481) in +addition to modern .NET. On .NET Framework, the CLR binds by the assembly's +**strong `AssemblyVersion`**: if `AssemblyVersion` tracks the package version, +every minor/patch bump changes the bind identity, so a consumer that references +`1.2.0` but resolves `1.3.0` at runtime needs an assembly binding redirect or +fails to load. For a *test-support* library pulled transitively into many test +projects, that friction is disproportionate to the value. + +## Decision + +We will pin `` at `1.0.0.0` and let `` and +`` (derived from ``) carry the real release +version. `AssemblyVersion` is bumped **only** on a deliberate breaking API change +(a new major), never on a minor/patch release. + +## Consequences + +- Consumers do not need binding redirects when a minor/patch bump flows in + transitively — the bind identity is stable across the whole `1.x` line. +- The actual shipped version is still discoverable via file/informational + version and the NuGet package version. +- The pin is load-bearing: a reviewer must not "fix" `AssemblyVersion` to match + the package version. A major bump is the only time it moves. +- Binary compatibility within a bind identity is additionally guarded by + PackageValidation (see the ABI-gate decision and `EnablePackageValidation`). diff --git a/docs/adr/0003-split-doubles-and-contract-tests-into-two-packages.md b/docs/adr/0003-split-doubles-and-contract-tests-into-two-packages.md new file mode 100644 index 0000000..54edb35 --- /dev/null +++ b/docs/adr/0003-split-doubles-and-contract-tests-into-two-packages.md @@ -0,0 +1,39 @@ +# 3. Ship the test doubles and the contract-test base classes as two packages + +## Status + +Accepted + +## Context + +The kit provides two distinct kinds of test support: + +1. **Test doubles** (`TestExtractor`, `TestLoader`, `TestTransformer`, + and their `Faulty*` variants) — concrete, framework-agnostic implementations + of the Abstractions base classes, usable from any test runner (or none). +2. **Contract-test base classes** (`ExtractorBaseContractTests<…>`, etc.) — + abstract xUnit `[Fact]`/`[Theory]` suites a downstream library subclasses to + verify its own extractor/loader/transformer honours the Abstractions contract. + +The second kind hard-depends on xUnit; the first does not. Bundling them would +force every consumer of the doubles to take an xUnit dependency, including +consumers on MSTest/NUnit or using the doubles outside a test project (e.g. in +benchmarks or samples). + +## Decision + +We will ship two packages: **`Wolfgang.Etl.TestKit`** (the doubles, no test- +framework dependency) and **`Wolfgang.Etl.TestKit.Xunit`** (the contract-test +base classes, depends on `Wolfgang.Etl.TestKit` + xUnit). The Xunit package +references the core package by ProjectReference in-repo and by NuGet version +downstream. + +## Consequences + +- Consumers who only need the doubles do not pay for xUnit. +- The xUnit-specific contract suites live behind an explicit, separately-versioned + package boundary; a future MSTest/NUnit contract package can be added without + touching the core. +- Both packages must be versioned and released together in lock-step for the + Xunit package's dependency on the core to resolve; the release pipeline packs + both. diff --git a/docs/adr/0004-deterministic-progress-timer-via-injection.md b/docs/adr/0004-deterministic-progress-timer-via-injection.md new file mode 100644 index 0000000..af3bb9b --- /dev/null +++ b/docs/adr/0004-deterministic-progress-timer-via-injection.md @@ -0,0 +1,39 @@ +# 4. Make progress reporting deterministic via an injectable timer + +## Status + +Accepted + +## Context + +The Abstractions base classes report progress on a wall-clock interval +(`ReportingInterval`) driven by an internal timer. A test that wants to assert +"a progress report was raised" cannot depend on a real timer firing: the test +would either sleep (slow, flaky) or race the timer against a fast synchronous +source that completes — and unsubscribes the timer's `Elapsed` handler in the +worker's `finally` — before the timer ever fires. + +## Decision + +We will expose progress-timer injection as a first-class part of the doubles and +the contract-test base classes: + +- Every double offers a constructor overload taking an `IProgressTimer`, and the + kit ships **`ManualProgressTimer`** whose `Start`/`Stop` are no-ops and which + only raises `Elapsed` when `Fire()` is called explicitly. +- The contract-test base classes drive progress assertions by pulling the first + item (so the pipeline is mid-flight), calling `Fire()` deterministically, then + draining the rest — never by waiting on wall-clock time. +- Implementations wiring an injected timer guard against duplicate `Elapsed` + subscriptions when `CreateProgressTimer` is overridden. + +## Consequences + +- Progress-callback tests are fast and deterministic — no sleeps, no timing races. +- The injection seam is public API surface (the `IProgressTimer` constructor + overloads and `ManualProgressTimer`) and is therefore guarded by the + PublicAPI baseline and PackageValidation; it cannot be removed without a + breaking-change bump. +- Test authors must fire the timer while the pipeline is mid-flight; firing it + after a synchronous source has drained observes no callback (documented in the + `ManualProgressTimer` example). diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..bbbab89 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,24 @@ +# Architecture Decision Records + +This directory records the significant architecture / design decisions for +**Wolfgang.Etl.TestKit** and **Wolfgang.Etl.TestKit.Xunit**, using lightweight +[Architecture Decision Records (ADRs)](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions). + +An ADR captures a single decision: the context that forced it, the decision +itself, and the consequences (good and bad). ADRs are immutable once accepted — +when a decision changes, add a **new** ADR that supersedes the old one rather +than editing history. + +- **[index.md](index.md)** — the list of all ADRs and their status. +- **[TEMPLATE.md](TEMPLATE.md)** — the skeleton to copy when adding one. + +## Adding an ADR + +1. Copy [`TEMPLATE.md`](TEMPLATE.md) to `NNNN-short-title.md`, numbering it with + the next free 4-digit sequence. +2. Fill in Context / Decision / Consequences (Nygard style). +3. Set the status to `Proposed`, then `Accepted` once agreed (or + `Superseded by ADR-NNNN`). +4. Add a row to [`index.md`](index.md). +5. Land the ADR alongside the PR that introduces the decision, so it is part of + the review. diff --git a/docs/adr/TEMPLATE.md b/docs/adr/TEMPLATE.md new file mode 100644 index 0000000..b2adfc4 --- /dev/null +++ b/docs/adr/TEMPLATE.md @@ -0,0 +1,22 @@ +# N. Short title of the decision + +## Status + +Proposed | Accepted | Superseded by [ADR-NNNN](NNNN-....md) + +## Context + +What is the issue or force that motivates this decision? Describe the facts and +constraints — technical, product, or process — that are driving the choice. +State the problem, not the solution. + +## Decision + +The change we are making, in active voice: "We will ...". Be specific enough +that a future reader can tell whether later code still honours it. + +## Consequences + +What becomes easier or harder as a result — the good, the bad, and the neutral. +Include any follow-up work, risks, or constraints the decision imposes (e.g. a +test that guards it, or a rule new code must follow). diff --git a/docs/adr/index.md b/docs/adr/index.md new file mode 100644 index 0000000..20966da --- /dev/null +++ b/docs/adr/index.md @@ -0,0 +1,8 @@ +# ADR Index + +| ADR | Title | Status | +| --- | --- | --- | +| [0001](0001-record-architecture-decisions.md) | Record architecture decisions | Accepted | +| [0002](0002-pin-assemblyversion-for-binding-stability.md) | Pin AssemblyVersion at 1.0.0.0 for binding stability | Accepted | +| [0003](0003-split-doubles-and-contract-tests-into-two-packages.md) | Ship the test doubles and the contract-test base classes as two packages | Accepted | +| [0004](0004-deterministic-progress-timer-via-injection.md) | Make progress reporting deterministic via an injectable timer | Accepted |