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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **`ManualProgressTimerCore` + `WithManualProgressTimer` extensions (#352):** a manually-driven progress
timer for tests. Attach it to any extractor / loader / transformer with `.WithManualProgressTimer(timer)`
and fire the stage's progress callback deterministically with `timer.Tick()` — no per-component
`IProgressTimer`-injection plumbing required. Drives the base's internal timer-core seam via the
`Wolfgang.Etl.TestKit` ⇆ `Wolfgang.Etl.Abstractions` friend relationship.

### Changed

- **Contract-test bases now drive progress timing via `ManualProgressTimerCore` (#352).** The
`ExtractorBaseContractTests` / `LoaderBaseContractTests` / `TransformerBaseContractTests` timer tests
build the SUT with the standard `CreateSut(...)` factory and attach a `ManualProgressTimerCore` — they
no longer require `CreateSutWithTimer`. That member is now **`virtual`** (was `abstract`) and throws if
the base implementation is invoked; existing overrides still compile. Additive — no downstream change
required to adopt the new TestKit.

### Deprecated

- **`*BaseContractTests.CreateSutWithTimer(IProgressTimer)` (#352):** no longer called by the contract.
Remove your override (and the component's `IProgressTimer`-injection constructor); it will be dropped in
a future major version.

### Removed

### Fixed
Expand Down
49 changes: 25 additions & 24 deletions src/Wolfgang.Etl.TestKit.Xunit/ExtractorBaseContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using Wolfgang.Etl.Abstractions;
using Wolfgang.Etl.TestKit;
using Xunit;

namespace Wolfgang.Etl.TestKit.Xunit;
Expand Down Expand Up @@ -51,9 +52,6 @@ namespace Wolfgang.Etl.TestKit.Xunit;
///
/// protected override IReadOnlyList<MyRecord> CreateExpectedItems() =>
/// new List<MyRecord> { new("a"), new("b"), new("c"), new("d"), new("e") };
///
/// protected override MyExtractor CreateSutWithTimer(IProgressTimer timer) =>
/// new MyExtractor("path/to/test-data.csv", timer);
/// }
/// </code>
/// </example>
Expand Down Expand Up @@ -91,22 +89,23 @@ public abstract class ExtractorBaseContractTests<TSut, TItem, TProgress>
protected abstract IReadOnlyList<TItem> CreateExpectedItems();

/// <summary>
/// Creates the system under test with the supplied <see cref="IProgressTimer"/>
/// injected via the derived class's protected constructor.
/// <b>Deprecated.</b> The contract now drives progress timing via
/// <see cref="ManualProgressTimerCore"/> and <c>WithManualProgressTimer</c>, which need no
/// per-component timer plumbing — so overriding this is no longer required. Retained for source
/// compatibility with existing overrides; remove your override (and the component's
/// <c>IProgressTimer</c>-injection ctor) and it will be dropped in a future major version.
/// </summary>
/// <param name="timer">
/// The <see cref="IProgressTimer"/> to inject. Typically a
/// <see cref="ManualProgressTimer"/> so that progress callbacks can be fired
/// on demand during tests.
/// </param>
/// <returns>A new, fully initialised instance of <typeparamref name="TSut"/>.</returns>
/// <example>
/// <code>
/// protected override MyExtractor CreateSutWithTimer(IProgressTimer timer) =>
/// new MyExtractor(sourceData, timer);
/// </code>
/// </example>
protected abstract TSut CreateSutWithTimer(IProgressTimer timer);
/// <param name="timer">Unused by the contract.</param>
/// <returns>A new instance of <typeparamref name="TSut"/> (in existing overrides only).</returns>
/// <exception cref="NotSupportedException">
/// Always, if the base (non-overridden) implementation is invoked — the contract no longer calls it.
/// </exception>
protected virtual TSut CreateSutWithTimer(IProgressTimer timer) =>
throw new NotSupportedException
(
"CreateSutWithTimer is no longer used by the contract; progress timing is driven via " +
"ManualProgressTimerCore + WithManualProgressTimer. Remove this override."
);



Expand Down Expand Up @@ -378,14 +377,15 @@ public async Task ExtractAsync_with_progress_and_empty_source_yields_no_items_As
[Fact]
public async Task ExtractAsync_with_progress_invokes_callback_when_timer_fires_Async()
{
using var timer = new ManualProgressTimer();
var sut = CreateSutWithTimer(timer);
var timer = new ManualProgressTimerCore();
var sut = CreateSut();
sut.WithManualProgressTimer(timer);
TProgress? captured = default;
var progress = new SynchronousProgress<TProgress>(r => captured = r);

await using var enumerator = sut.ExtractAsync(progress).GetAsyncEnumerator();
await enumerator.MoveNextAsync().ConfigureAwait(false);
timer.Fire();
timer.Tick();

Assert.NotNull(captured);
}
Expand Down Expand Up @@ -527,14 +527,15 @@ await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
[Fact]
public async Task ExtractAsync_with_progress_and_token_invokes_callback_when_timer_fires_Async()
{
using var timer = new ManualProgressTimer();
var sut = CreateSutWithTimer(timer);
var timer = new ManualProgressTimerCore();
var sut = CreateSut();
sut.WithManualProgressTimer(timer);
TProgress? captured = default;
var progress = new SynchronousProgress<TProgress>(r => captured = r);

await using var enumerator = sut.ExtractAsync(progress, CancellationToken.None).GetAsyncEnumerator();
await enumerator.MoveNextAsync().ConfigureAwait(false);
timer.Fire();
timer.Tick();

Assert.NotNull(captured);
}
Expand Down
44 changes: 26 additions & 18 deletions src/Wolfgang.Etl.TestKit.Xunit/LoaderBaseContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using Wolfgang.Etl.Abstractions;
using Wolfgang.Etl.TestKit;
using Xunit;

namespace Wolfgang.Etl.TestKit.Xunit;
Expand Down Expand Up @@ -50,9 +51,6 @@ namespace Wolfgang.Etl.TestKit.Xunit;
///
/// protected override IReadOnlyList&lt;MyRecord&gt; CreateSourceItems() =>
/// new List&lt;MyRecord&gt; { new("a"), new("b"), new("c"), new("d"), new("e") };
///
/// protected override MyLoader CreateSutWithTimer(IProgressTimer timer) =>
/// new MyLoader(connectionString, timer);
/// }
/// </code>
/// </example>
Expand All @@ -74,15 +72,23 @@ public abstract class LoaderBaseContractTests<TSut, TItem, TProgress>
protected abstract TSut CreateSut(int itemCount);

/// <summary>
/// Creates a <typeparamref name="TSut"/> with the supplied <see cref="IProgressTimer"/>
/// injected via the derived class's protected constructor.
/// <b>Deprecated.</b> The contract now drives progress timing via
/// <see cref="ManualProgressTimerCore"/> and <c>WithManualProgressTimer</c>, which need no
/// per-component timer plumbing — so overriding this is no longer required. Retained for source
/// compatibility with existing overrides; remove your override (and the component's
/// <c>IProgressTimer</c>-injection ctor) and it will be dropped in a future major version.
/// </summary>
/// <param name="timer">
/// The <see cref="IProgressTimer"/> to inject. Typically a
/// <see cref="ManualProgressTimer"/> so that progress callbacks can be fired
/// on demand during tests.
/// </param>
protected abstract TSut CreateSutWithTimer(IProgressTimer timer);
/// <param name="timer">Unused by the contract.</param>
/// <returns>A new instance of <typeparamref name="TSut"/> (in existing overrides only).</returns>
/// <exception cref="NotSupportedException">
/// Always, if the base (non-overridden) implementation is invoked — the contract no longer calls it.
/// </exception>
protected virtual TSut CreateSutWithTimer(IProgressTimer timer) =>
throw new NotSupportedException
(
"CreateSutWithTimer is no longer used by the contract; progress timing is driven via " +
"ManualProgressTimerCore + WithManualProgressTimer. Remove this override."
);

/// <summary>
/// Returns the source items used to feed the loader. Must return at least 5 items.
Expand All @@ -107,7 +113,7 @@ private IAsyncEnumerable<TItem> CreateInputItemsAsync() =>
/// <summary>
/// Returns an input sequence that pauses after the first item until
/// <paramref name="gate"/> is released, keeping the load pipeline alive
/// so the test can call <see cref="ManualProgressTimer.Fire"/> mid-flight.
/// so the test can call <see cref="ManualProgressTimerCore.Tick"/> mid-flight.
/// </summary>
private async IAsyncEnumerable<TItem> CreateGatedInputItemsAsync(TaskCompletionSource<bool> gate)
{
Expand Down Expand Up @@ -400,14 +406,15 @@ public async Task LoadAsync_with_progress_and_empty_source_completes_without_err
[Fact]
public async Task LoadAsync_with_progress_invokes_callback_when_timer_fires_Async()
{
using var timer = new ManualProgressTimer();
var sut = CreateSutWithTimer(timer);
var timer = new ManualProgressTimerCore();
var sut = CreateSut();
sut.WithManualProgressTimer(timer);
TProgress? captured = default;
var progress = new SynchronousProgress<TProgress>(r => captured = r);
var gate = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

var task = sut.LoadAsync(CreateGatedInputItemsAsync(gate), progress);
timer.Fire();
timer.Tick();
gate.SetResult(true);
await task.ConfigureAwait(false);

Expand Down Expand Up @@ -554,14 +561,15 @@ await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
[Fact]
public async Task LoadAsync_with_progress_and_token_invokes_callback_when_timer_fires_Async()
{
using var timer = new ManualProgressTimer();
var sut = CreateSutWithTimer(timer);
var timer = new ManualProgressTimerCore();
var sut = CreateSut();
sut.WithManualProgressTimer(timer);
TProgress? captured = default;
var progress = new SynchronousProgress<TProgress>(r => captured = r);
var gate = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);

var task = sut.LoadAsync(CreateGatedInputItemsAsync(gate), progress, CancellationToken.None);
timer.Fire();
timer.Tick();
gate.SetResult(true);
await task.ConfigureAwait(false);

Expand Down
6 changes: 3 additions & 3 deletions src/Wolfgang.Etl.TestKit.Xunit/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ abstract Wolfgang.Etl.TestKit.Xunit.ExtractWithProgressAsyncContractTests<TSut,
abstract Wolfgang.Etl.TestKit.Xunit.ExtractWithProgressAsyncContractTests<TSut, TItem, TProgress>.CreateSut(int itemCount) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.ExtractorBaseContractTests<TSut, TItem, TProgress>.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList<TItem>
abstract Wolfgang.Etl.TestKit.Xunit.ExtractorBaseContractTests<TSut, TItem, TProgress>.CreateSut(int itemCount) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.ExtractorBaseContractTests<TSut, TItem, TProgress>.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut
virtual Wolfgang.Etl.TestKit.Xunit.ExtractorBaseContractTests<TSut, TItem, TProgress>.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.IdempotentExtractorContractTests<TSut, TItem, TProgress>.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList<TItem>!
abstract Wolfgang.Etl.TestKit.Xunit.IdempotentExtractorContractTests<TSut, TItem, TProgress>.CreateSut(int itemCount) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.IdempotentLoaderContractTests<TSut, TItem>.CreateSourceItems() -> System.Collections.Generic.IReadOnlyList<TItem>!
Expand All @@ -273,7 +273,7 @@ abstract Wolfgang.Etl.TestKit.Xunit.LoadWithProgressAsyncContractTests<TSut, TIt
abstract Wolfgang.Etl.TestKit.Xunit.LoadWithProgressAsyncContractTests<TSut, TItem, TProgress>.CreateSut(int itemCount) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.LoaderBaseContractTests<TSut, TItem, TProgress>.CreateSourceItems() -> System.Collections.Generic.IReadOnlyList<TItem>
abstract Wolfgang.Etl.TestKit.Xunit.LoaderBaseContractTests<TSut, TItem, TProgress>.CreateSut(int itemCount) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.LoaderBaseContractTests<TSut, TItem, TProgress>.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut
virtual Wolfgang.Etl.TestKit.Xunit.LoaderBaseContractTests<TSut, TItem, TProgress>.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.TransformAsyncContractTests<TSut, TItem>.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList<TItem>
abstract Wolfgang.Etl.TestKit.Xunit.TransformAsyncContractTests<TSut, TItem>.CreateSut(int itemCount) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.TransformWithCancellationAsyncContractTests<TSut, TItem>.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList<TItem>
Expand All @@ -284,7 +284,7 @@ abstract Wolfgang.Etl.TestKit.Xunit.TransformWithProgressAsyncContractTests<TSut
abstract Wolfgang.Etl.TestKit.Xunit.TransformWithProgressAsyncContractTests<TSut, TItem, TProgress>.CreateSut(int itemCount) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.TransformerBaseContractTests<TSut, TItem, TProgress>.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList<TItem>
abstract Wolfgang.Etl.TestKit.Xunit.TransformerBaseContractTests<TSut, TItem, TProgress>.CreateSut(int itemCount) -> TSut
abstract Wolfgang.Etl.TestKit.Xunit.TransformerBaseContractTests<TSut, TItem, TProgress>.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut
virtual Wolfgang.Etl.TestKit.Xunit.TransformerBaseContractTests<TSut, TItem, TProgress>.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut
static Wolfgang.Etl.TestKit.Xunit.ProgressAssert.AllReportsSatisfy<T>(Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>! capture, System.Func<T, bool>! predicate) -> 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.HasExactly<T>(Wolfgang.Etl.TestKit.Xunit.ProgressCapture<T>! capture, int count) -> void
Expand Down
42 changes: 25 additions & 17 deletions src/Wolfgang.Etl.TestKit.Xunit/TransformerBaseContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading;
using System.Threading.Tasks;
using Wolfgang.Etl.Abstractions;
using Wolfgang.Etl.TestKit;
using Xunit;

namespace Wolfgang.Etl.TestKit.Xunit;
Expand Down Expand Up @@ -52,9 +53,6 @@ namespace Wolfgang.Etl.TestKit.Xunit;
///
/// protected override IReadOnlyList&lt;MyRecord&gt; CreateExpectedItems() =>
/// new List&lt;MyRecord&gt; { new("a"), new("b"), new("c"), new("d"), new("e") };
///
/// protected override MyTransformer CreateSutWithTimer(IProgressTimer timer) =>
/// new MyTransformer(timer);
/// }
/// </code>
/// </example>
Expand All @@ -74,15 +72,23 @@ public abstract class TransformerBaseContractTests<TSut, TItem, TProgress>
protected abstract TSut CreateSut(int itemCount);

/// <summary>
/// Creates a <typeparamref name="TSut"/> with the supplied <see cref="IProgressTimer"/>
/// injected via the derived class's protected constructor.
/// <b>Deprecated.</b> The contract now drives progress timing via
/// <see cref="ManualProgressTimerCore"/> and <c>WithManualProgressTimer</c>, which need no
/// per-component timer plumbing — so overriding this is no longer required. Retained for source
/// compatibility with existing overrides; remove your override (and the component's
/// <c>IProgressTimer</c>-injection ctor) and it will be dropped in a future major version.
/// </summary>
/// <param name="timer">
/// The <see cref="IProgressTimer"/> to inject. Typically a
/// <see cref="ManualProgressTimer"/> so that progress callbacks can be fired
/// on demand during tests.
/// </param>
protected abstract TSut CreateSutWithTimer(IProgressTimer timer);
/// <param name="timer">Unused by the contract.</param>
/// <returns>A new instance of <typeparamref name="TSut"/> (in existing overrides only).</returns>
/// <exception cref="NotSupportedException">
/// Always, if the base (non-overridden) implementation is invoked — the contract no longer calls it.
/// </exception>
protected virtual TSut CreateSutWithTimer(IProgressTimer timer) =>
throw new NotSupportedException
(
"CreateSutWithTimer is no longer used by the contract; progress timing is driven via " +
"ManualProgressTimerCore + WithManualProgressTimer. Remove this override."
);


private const int DefaultItemCount = 5;
Expand Down Expand Up @@ -423,14 +429,15 @@ public async Task TransformAsync_with_progress_and_empty_source_yields_no_items_
[Fact]
public async Task TransformAsync_with_progress_invokes_callback_when_timer_fires_Async()
{
using var timer = new ManualProgressTimer();
var sut = CreateSutWithTimer(timer);
var timer = new ManualProgressTimerCore();
var sut = CreateSut();
sut.WithManualProgressTimer(timer);
TProgress? captured = default;
var progress = new SynchronousProgress<TProgress>(r => captured = r);

await using var enumerator = sut.TransformAsync(CreateInputItemsAsync(), progress).GetAsyncEnumerator();
await enumerator.MoveNextAsync().ConfigureAwait(false);
timer.Fire();
timer.Tick();

Assert.NotNull(captured);
}
Expand Down Expand Up @@ -589,14 +596,15 @@ await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
[Fact]
public async Task TransformAsync_with_progress_and_token_invokes_callback_when_timer_fires_Async()
{
using var timer = new ManualProgressTimer();
var sut = CreateSutWithTimer(timer);
var timer = new ManualProgressTimerCore();
var sut = CreateSut();
sut.WithManualProgressTimer(timer);
TProgress? captured = default;
var progress = new SynchronousProgress<TProgress>(r => captured = r);

await using var enumerator = sut.TransformAsync(CreateInputItemsAsync(), progress, CancellationToken.None).GetAsyncEnumerator();
await enumerator.MoveNextAsync().ConfigureAwait(false);
timer.Fire();
timer.Tick();

Assert.NotNull(captured);
}
Expand Down
Loading
Loading