Skip to content

feat: add ProgressCapture<T> and ProgressAssert helpers (#14)#172

Merged
Chris-Wolfgang merged 3 commits into
vNextfrom
feature/progress-assert-14
Jun 24, 2026
Merged

feat: add ProgressCapture<T> and ProgressAssert helpers (#14)#172
Chris-Wolfgang merged 3 commits into
vNextfrom
feature/progress-assert-14

Conversation

@Chris-Wolfgang

Copy link
Copy Markdown
Owner

Implements #14 (Progress Assertion Helpers) in Wolfgang.Etl.TestKit.Xunit.

What's new

ProgressCapture<T> : IProgress<T> (ProgressCapture.cs)

A capturing, synchronous IProgress<T> (reports on the calling thread, like SynchronousProgress<T>) that records every reported value:

  • IReadOnlyList<T> Reports — all captured reports, in order (returns a snapshot)
  • T? FinalReport — last report, or default if none
  • int Count — number of captured reports
  • explicit void IProgress<T>.Report(T value) — appends under a lock

Replaces the manual new List<T>() + new SynchronousProgress<T>(r => list.Add(r)) wiring downstream suites repeat. SynchronousProgress<T> is left in place (still used by the contract bases); ProgressCapture<T> is a separate opt-in utility.

ProgressAssert (static) (ProgressAssert.cs)

The five v1 assertions from the issue, each null-guarding its args (ArgumentNullException) and throwing xUnit assertion exceptions via Assert.* (no custom exception type):

  • HasReports<T>(ProgressCapture<T>)
  • HasExactly<T>(ProgressCapture<T>, int count)
  • IsMonotonicallyIncreasing<T, TKey>(ProgressCapture<T>, Func<T, TKey>) where TKey : IComparable<TKey> — reports the breaking index in the failure message
  • FinalReportSatisfies<T>(ProgressCapture<T>, Func<T, bool>) — fails clearly when there are zero reports
  • AllReportsSatisfy<T>(ProgressCapture<T>, Func<T, bool>) — reports the breaking index

Design decision — Option B (static asserts)

Implemented the static ProgressAssert class (Option B), not the fluent Option A: simpler, lower surface area, and consistent with xUnit's own Assert.* style, per the issue's own assessment.

Nullable handling across TFMs

FinalReport is typed T? on an unconstrained T. With <Nullable>enable</Nullable> (set in Directory.Build.props), T? on an unconstrained type parameter is the correct "maybe-null" annotation and needs no [MaybeNull] attribute — it compiles cleanly with 0 warnings across all five source TFMs (net462, net481, netstandard2.0, net8.0, net10.0) under treat-warnings-as-errors.

Build & test

  • dotnet build src/Wolfgang.Etl.TestKit.Xunit/...csproj -c Release0 warnings, 0 errors (all 5 TFMs)
  • dotnet test tests/...Xunit.Tests.Unit -c Release -f net8.0 (new tests) → 28 passed, 0 failed
  • Coverage includes a realistic end-to-end test driving a real TestExtractor<int> through ExtractAsync(capture) and asserting monotonic CurrentItemCount + final count, plus passing/failing cases and null-arg guards for every assertion.

PublicAPI follow-up (PR #166 not yet merged)

No PublicAPI.*.txt files were created (baseline lands with #166). After #166 merges, add these to src/Wolfgang.Etl.TestKit.Xunit/PublicAPI.Unshipped.txt:

Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>
Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>.ProgressCapture() -> void
Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>.Reports.get -> System.Collections.Generic.IReadOnlyList<T>!
Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>.FinalReport.get -> T?
Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>.Count.get -> int
Wolfgang.Etl.TestKit.Xunit.ProgressAssert
static Wolfgang.Etl.TestKit.Xunit.ProgressAssert.HasReports<T>(Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>! capture) -> void
static Wolfgang.Etl.TestKit.Xunit.ProgressAssert.HasExactly<T>(Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>! capture, int count) -> void
static Wolfgang.Etl.TestKit.Xunit.ProgressAssert.IsMonotonicallyIncreasing<T, TKey>(Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>! capture, System.Func<T, TKey>! selector) -> void
static Wolfgang.Etl.TestKit.Xunit.ProgressAssert.FinalReportSatisfies<T>(Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>! capture, System.Func<T, bool>! predicate) -> void
static Wolfgang.Etl.TestKit.Xunit.ProgressAssert.AllReportsSatisfy<T>(Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>! capture, System.Func<T, bool>! predicate) -> void

Note: the explicit interface implementation IProgress<T>.Report is not a public API entry.

Closes #14

🤖 Generated with Claude Code

Adds a capturing IProgress<T> implementation plus xUnit-style static
assertion helpers for validating progress report sequences, replacing
the manual List<T> + SynchronousProgress<T> boilerplate every downstream
ETL test suite repeats today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Chris-Wolfgang Chris-Wolfgang changed the base branch from main to vNext June 23, 2026 23:32
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.

Enhancement: Progress Assertion Helpers

1 participant