Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/adr/0001-record-architecture-decisions.md
Original file line number Diff line number Diff line change
@@ -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.
33 changes: 33 additions & 0 deletions docs/adr/0002-pin-assemblyversion-for-binding-stability.md
Original file line number Diff line number Diff line change
@@ -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 `<AssemblyVersion>` at `1.0.0.0` and let `<FileVersion>` and
`<InformationalVersion>` (derived from `<Version>`) 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`).
Original file line number Diff line number Diff line change
@@ -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<T>`, `TestLoader<T>`, `TestTransformer<T>`,
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.
39 changes: 39 additions & 0 deletions docs/adr/0004-deterministic-progress-timer-via-injection.md
Original file line number Diff line number Diff line change
@@ -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).
24 changes: 24 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 22 additions & 0 deletions docs/adr/TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -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).
8 changes: 8 additions & 0 deletions docs/adr/index.md
Original file line number Diff line number Diff line change
@@ -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 |
Loading