diff --git a/CHANGELOG.md b/CHANGELOG.md index 6527e4c..4d27f86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/Wolfgang.Etl.TestKit.Xunit/ExtractorBaseContractTests.cs b/src/Wolfgang.Etl.TestKit.Xunit/ExtractorBaseContractTests.cs index 7874196..10446d4 100644 --- a/src/Wolfgang.Etl.TestKit.Xunit/ExtractorBaseContractTests.cs +++ b/src/Wolfgang.Etl.TestKit.Xunit/ExtractorBaseContractTests.cs @@ -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; @@ -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); /// } /// /// @@ -91,22 +89,23 @@ public abstract class ExtractorBaseContractTests protected abstract IReadOnlyList CreateExpectedItems(); /// - /// Creates the system under test with the supplied - /// injected via the derived class's protected constructor. + /// Deprecated. The contract now drives progress timing via + /// and WithManualProgressTimer, 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 + /// IProgressTimer-injection ctor) and it will be dropped in a future major version. /// - /// - /// The to inject. Typically a - /// so that progress callbacks can be fired - /// on demand during tests. - /// - /// A new, fully initialised instance of . - /// - /// - /// protected override MyExtractor CreateSutWithTimer(IProgressTimer timer) => - /// new MyExtractor(sourceData, timer); - /// - /// - protected abstract TSut CreateSutWithTimer(IProgressTimer timer); + /// Unused by the contract. + /// A new instance of (in existing overrides only). + /// + /// Always, if the base (non-overridden) implementation is invoked — the contract no longer calls it. + /// + 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." + ); @@ -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(r => captured = r); await using var enumerator = sut.ExtractAsync(progress).GetAsyncEnumerator(); await enumerator.MoveNextAsync().ConfigureAwait(false); - timer.Fire(); + timer.Tick(); Assert.NotNull(captured); } @@ -527,14 +527,15 @@ await Assert.ThrowsAnyAsync(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(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); } diff --git a/src/Wolfgang.Etl.TestKit.Xunit/LoaderBaseContractTests.cs b/src/Wolfgang.Etl.TestKit.Xunit/LoaderBaseContractTests.cs index 92811bb..43a32c1 100644 --- a/src/Wolfgang.Etl.TestKit.Xunit/LoaderBaseContractTests.cs +++ b/src/Wolfgang.Etl.TestKit.Xunit/LoaderBaseContractTests.cs @@ -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; @@ -50,9 +51,6 @@ namespace Wolfgang.Etl.TestKit.Xunit; /// /// protected override IReadOnlyList<MyRecord> CreateSourceItems() => /// new List<MyRecord> { new("a"), new("b"), new("c"), new("d"), new("e") }; -/// -/// protected override MyLoader CreateSutWithTimer(IProgressTimer timer) => -/// new MyLoader(connectionString, timer); /// } /// /// @@ -74,15 +72,23 @@ public abstract class LoaderBaseContractTests protected abstract TSut CreateSut(int itemCount); /// - /// Creates a with the supplied - /// injected via the derived class's protected constructor. + /// Deprecated. The contract now drives progress timing via + /// and WithManualProgressTimer, 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 + /// IProgressTimer-injection ctor) and it will be dropped in a future major version. /// - /// - /// The to inject. Typically a - /// so that progress callbacks can be fired - /// on demand during tests. - /// - protected abstract TSut CreateSutWithTimer(IProgressTimer timer); + /// Unused by the contract. + /// A new instance of (in existing overrides only). + /// + /// Always, if the base (non-overridden) implementation is invoked — the contract no longer calls it. + /// + 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." + ); /// /// Returns the source items used to feed the loader. Must return at least 5 items. @@ -107,7 +113,7 @@ private IAsyncEnumerable CreateInputItemsAsync() => /// /// Returns an input sequence that pauses after the first item until /// is released, keeping the load pipeline alive - /// so the test can call mid-flight. + /// so the test can call mid-flight. /// private async IAsyncEnumerable CreateGatedInputItemsAsync(TaskCompletionSource gate) { @@ -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(r => captured = r); var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var task = sut.LoadAsync(CreateGatedInputItemsAsync(gate), progress); - timer.Fire(); + timer.Tick(); gate.SetResult(true); await task.ConfigureAwait(false); @@ -554,14 +561,15 @@ await Assert.ThrowsAnyAsync(() => [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(r => captured = r); var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var task = sut.LoadAsync(CreateGatedInputItemsAsync(gate), progress, CancellationToken.None); - timer.Fire(); + timer.Tick(); gate.SetResult(true); await task.ConfigureAwait(false); diff --git a/src/Wolfgang.Etl.TestKit.Xunit/PublicAPI.Shipped.txt b/src/Wolfgang.Etl.TestKit.Xunit/PublicAPI.Shipped.txt index 3a7bd88..ab8b970 100644 --- a/src/Wolfgang.Etl.TestKit.Xunit/PublicAPI.Shipped.txt +++ b/src/Wolfgang.Etl.TestKit.Xunit/PublicAPI.Shipped.txt @@ -255,7 +255,7 @@ abstract Wolfgang.Etl.TestKit.Xunit.ExtractWithProgressAsyncContractTests.CreateSut(int itemCount) -> TSut abstract Wolfgang.Etl.TestKit.Xunit.ExtractorBaseContractTests.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList abstract Wolfgang.Etl.TestKit.Xunit.ExtractorBaseContractTests.CreateSut(int itemCount) -> TSut -abstract Wolfgang.Etl.TestKit.Xunit.ExtractorBaseContractTests.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut +virtual Wolfgang.Etl.TestKit.Xunit.ExtractorBaseContractTests.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut abstract Wolfgang.Etl.TestKit.Xunit.IdempotentExtractorContractTests.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList! abstract Wolfgang.Etl.TestKit.Xunit.IdempotentExtractorContractTests.CreateSut(int itemCount) -> TSut abstract Wolfgang.Etl.TestKit.Xunit.IdempotentLoaderContractTests.CreateSourceItems() -> System.Collections.Generic.IReadOnlyList! @@ -273,7 +273,7 @@ abstract Wolfgang.Etl.TestKit.Xunit.LoadWithProgressAsyncContractTests.CreateSut(int itemCount) -> TSut abstract Wolfgang.Etl.TestKit.Xunit.LoaderBaseContractTests.CreateSourceItems() -> System.Collections.Generic.IReadOnlyList abstract Wolfgang.Etl.TestKit.Xunit.LoaderBaseContractTests.CreateSut(int itemCount) -> TSut -abstract Wolfgang.Etl.TestKit.Xunit.LoaderBaseContractTests.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut +virtual Wolfgang.Etl.TestKit.Xunit.LoaderBaseContractTests.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut abstract Wolfgang.Etl.TestKit.Xunit.TransformAsyncContractTests.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList abstract Wolfgang.Etl.TestKit.Xunit.TransformAsyncContractTests.CreateSut(int itemCount) -> TSut abstract Wolfgang.Etl.TestKit.Xunit.TransformWithCancellationAsyncContractTests.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList @@ -284,7 +284,7 @@ abstract Wolfgang.Etl.TestKit.Xunit.TransformWithProgressAsyncContractTests.CreateSut(int itemCount) -> TSut abstract Wolfgang.Etl.TestKit.Xunit.TransformerBaseContractTests.CreateExpectedItems() -> System.Collections.Generic.IReadOnlyList abstract Wolfgang.Etl.TestKit.Xunit.TransformerBaseContractTests.CreateSut(int itemCount) -> TSut -abstract Wolfgang.Etl.TestKit.Xunit.TransformerBaseContractTests.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut +virtual Wolfgang.Etl.TestKit.Xunit.TransformerBaseContractTests.CreateSutWithTimer(Wolfgang.Etl.Abstractions.IProgressTimer timer) -> TSut static Wolfgang.Etl.TestKit.Xunit.ProgressAssert.AllReportsSatisfy(Wolfgang.Etl.TestKit.Xunit.ProgressCapture! capture, System.Func! predicate) -> void static Wolfgang.Etl.TestKit.Xunit.ProgressAssert.FinalReportSatisfies(Wolfgang.Etl.TestKit.Xunit.ProgressCapture! capture, System.Func! predicate) -> void static Wolfgang.Etl.TestKit.Xunit.ProgressAssert.HasExactly(Wolfgang.Etl.TestKit.Xunit.ProgressCapture! capture, int count) -> void diff --git a/src/Wolfgang.Etl.TestKit.Xunit/TransformerBaseContractTests.cs b/src/Wolfgang.Etl.TestKit.Xunit/TransformerBaseContractTests.cs index ef36884..deaf214 100644 --- a/src/Wolfgang.Etl.TestKit.Xunit/TransformerBaseContractTests.cs +++ b/src/Wolfgang.Etl.TestKit.Xunit/TransformerBaseContractTests.cs @@ -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; @@ -52,9 +53,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 MyTransformer CreateSutWithTimer(IProgressTimer timer) => -/// new MyTransformer(timer); /// } /// /// @@ -74,15 +72,23 @@ public abstract class TransformerBaseContractTests protected abstract TSut CreateSut(int itemCount); /// - /// Creates a with the supplied - /// injected via the derived class's protected constructor. + /// Deprecated. The contract now drives progress timing via + /// and WithManualProgressTimer, 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 + /// IProgressTimer-injection ctor) and it will be dropped in a future major version. /// - /// - /// The to inject. Typically a - /// so that progress callbacks can be fired - /// on demand during tests. - /// - protected abstract TSut CreateSutWithTimer(IProgressTimer timer); + /// Unused by the contract. + /// A new instance of (in existing overrides only). + /// + /// Always, if the base (non-overridden) implementation is invoked — the contract no longer calls it. + /// + 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; @@ -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(r => captured = r); await using var enumerator = sut.TransformAsync(CreateInputItemsAsync(), progress).GetAsyncEnumerator(); await enumerator.MoveNextAsync().ConfigureAwait(false); - timer.Fire(); + timer.Tick(); Assert.NotNull(captured); } @@ -589,14 +596,15 @@ await Assert.ThrowsAnyAsync(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(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); } diff --git a/src/Wolfgang.Etl.TestKit/ManualProgressTimerCore.cs b/src/Wolfgang.Etl.TestKit/ManualProgressTimerCore.cs new file mode 100644 index 0000000..ea2f6d5 --- /dev/null +++ b/src/Wolfgang.Etl.TestKit/ManualProgressTimerCore.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading; +using Wolfgang.Etl.Abstractions; + +namespace Wolfgang.Etl.TestKit; + +/// +/// A manually-driven progress timer for tests: instead of a real +/// firing on an interval, the stage's progress callback fires only when is called, +/// making progress-callback assertions deterministic. Attach it to a stage with +/// +/// (and the loader / transformer overloads). +/// +/// +/// This drives the internal timer-core seam on the base classes (the same seam the base's own +/// CreateProgressTimer uses), reachable here because Wolfgang.Etl.TestKit is an +/// internals-visible friend of Wolfgang.Etl.Abstractions — so a component needs no +/// per-type timer-injection plumbing to be timer-testable. Attach it before the run starts; the stage +/// builds its progress timer when the run begins. +/// +/// var timer = new ManualProgressTimerCore(); +/// var extractor = new TestExtractor<int>(new[] { 1, 2, 3 }).WithManualProgressTimer(timer); +/// +/// await using var e = extractor.ExtractAsync(progress).GetAsyncEnumerator(); +/// await e.MoveNextAsync(); +/// timer.Tick(); // fires the progress callback exactly once +/// +/// +public sealed class ManualProgressTimerCore +{ + private TimerCallback? _onTick; + + // Consumed by ProgressTimerExtensions to set the base's internal TimerCoreFactory. The base calls + // this with the per-tick callback when it builds its progress timer; we capture the callback so + // Tick() can invoke it on demand and return a no-op core (interval timing is irrelevant here). + internal Func CoreFactory => + onTick => + { + _onTick = onTick; + return new Core(); + }; + + + + /// + /// Fires a single timer tick, synchronously invoking the stage's progress callback exactly once. + /// + /// + /// The stage's progress timer has not been built yet — begin the run (start enumeration, or invoke + /// the loader) before calling . + /// + public void Tick() + { + var onTick = _onTick + ?? throw new InvalidOperationException + ( + "The progress timer has not started. Begin the run (start enumeration / invoke the " + + "loader) before calling Tick()." + ); + + onTick(state: null); + } + + + + // A do-nothing ITimerCore: a manual timer never schedules real callbacks, so Change is a no-op. + private sealed class Core : ITimerCore + { + public void Change(int dueTime, int period) + { + } + + + + public void Dispose() + { + } + } +} diff --git a/src/Wolfgang.Etl.TestKit/ProgressTimerExtensions.cs b/src/Wolfgang.Etl.TestKit/ProgressTimerExtensions.cs new file mode 100644 index 0000000..1f9cf1a --- /dev/null +++ b/src/Wolfgang.Etl.TestKit/ProgressTimerExtensions.cs @@ -0,0 +1,98 @@ +using System; +using Wolfgang.Etl.Abstractions; + +namespace Wolfgang.Etl.TestKit; + +/// +/// Extension methods that attach a to a stage so its progress +/// callback fires deterministically (on ) instead of on a +/// real timer interval. +/// +/// +/// These reach the internal timer-core seam on the base classes via the friend relationship between +/// Wolfgang.Etl.TestKit and Wolfgang.Etl.Abstractions, so a component needs no +/// per-type timer-injection plumbing. Attach the timer before the run starts; the stage builds its +/// progress timer when the run begins. +/// +public static class ProgressTimerExtensions +{ + /// Attaches to . + /// , for chaining. + /// Either argument is . + public static ExtractorBase WithManualProgressTimer + ( + this ExtractorBase extractor, + ManualProgressTimerCore timer + ) + where TSource : notnull + where TProgress : notnull + { + if (extractor is null) + { + throw new ArgumentNullException(nameof(extractor)); + } + + if (timer is null) + { + throw new ArgumentNullException(nameof(timer)); + } + + extractor.TimerCoreFactory = timer.CoreFactory; + return extractor; + } + + + + /// Attaches to . + /// , for chaining. + /// Either argument is . + public static LoaderBase WithManualProgressTimer + ( + this LoaderBase loader, + ManualProgressTimerCore timer + ) + where TDestination : notnull + where TProgress : notnull + { + if (loader is null) + { + throw new ArgumentNullException(nameof(loader)); + } + + if (timer is null) + { + throw new ArgumentNullException(nameof(timer)); + } + + loader.TimerCoreFactory = timer.CoreFactory; + return loader; + } + + + + /// Attaches to . + /// , for chaining. + /// Either argument is . + public static TransformerBase WithManualProgressTimer + ( + this TransformerBase transformer, + ManualProgressTimerCore timer + ) + where TSource : notnull + where TDestination : notnull + where TProgress : notnull + { + if (transformer is null) + { + throw new ArgumentNullException(nameof(transformer)); + } + + if (timer is null) + { + throw new ArgumentNullException(nameof(timer)); + } + + transformer.TimerCoreFactory = timer.CoreFactory; + return transformer; + } +} diff --git a/src/Wolfgang.Etl.TestKit/PublicAPI.Unshipped.txt b/src/Wolfgang.Etl.TestKit/PublicAPI.Unshipped.txt index 7dc5c58..459eb38 100644 --- a/src/Wolfgang.Etl.TestKit/PublicAPI.Unshipped.txt +++ b/src/Wolfgang.Etl.TestKit/PublicAPI.Unshipped.txt @@ -1 +1,8 @@ #nullable enable +Wolfgang.Etl.TestKit.ManualProgressTimerCore +Wolfgang.Etl.TestKit.ManualProgressTimerCore.ManualProgressTimerCore() -> void +Wolfgang.Etl.TestKit.ManualProgressTimerCore.Tick() -> void +Wolfgang.Etl.TestKit.ProgressTimerExtensions +static Wolfgang.Etl.TestKit.ProgressTimerExtensions.WithManualProgressTimer(this Wolfgang.Etl.Abstractions.LoaderBase! loader, Wolfgang.Etl.TestKit.ManualProgressTimerCore! timer) -> Wolfgang.Etl.Abstractions.LoaderBase! +static Wolfgang.Etl.TestKit.ProgressTimerExtensions.WithManualProgressTimer(this Wolfgang.Etl.Abstractions.TransformerBase! transformer, Wolfgang.Etl.TestKit.ManualProgressTimerCore! timer) -> Wolfgang.Etl.Abstractions.TransformerBase! +static Wolfgang.Etl.TestKit.ProgressTimerExtensions.WithManualProgressTimer(this Wolfgang.Etl.Abstractions.ExtractorBase! extractor, Wolfgang.Etl.TestKit.ManualProgressTimerCore! timer) -> Wolfgang.Etl.Abstractions.ExtractorBase! diff --git a/tests/Wolfgang.Etl.TestKit.Tests.Unit/ManualProgressTimerCoreTests.cs b/tests/Wolfgang.Etl.TestKit.Tests.Unit/ManualProgressTimerCoreTests.cs new file mode 100644 index 0000000..2ad2f34 --- /dev/null +++ b/tests/Wolfgang.Etl.TestKit.Tests.Unit/ManualProgressTimerCoreTests.cs @@ -0,0 +1,102 @@ +using System; +using System.Threading.Tasks; +using Wolfgang.Etl.Abstractions; +using Wolfgang.Etl.TestKit; +using Xunit; + +namespace Wolfgang.Etl.TestKit.Tests.Unit; + +public class ManualProgressTimerCoreTests +{ + [Fact] + public void Tick_before_the_run_starts_throws_InvalidOperationException() + { + var timer = new ManualProgressTimerCore(); + + Assert.Throws + ( + () => timer.Tick() + ); + } + + + + [Fact] + public async Task Tick_after_the_run_starts_invokes_the_progress_callback_Async() + { + var timer = new ManualProgressTimerCore(); + var sut = new TestExtractor(new[] { 1, 2, 3 }); + sut.WithManualProgressTimer(timer); + + Report? captured = null; + var progress = new SyncProgress(r => captured = r); + + await using var enumerator = sut.ExtractAsync(progress).GetAsyncEnumerator(); + await enumerator.MoveNextAsync().ConfigureAwait(false); // starts the run; builds the progress timer + timer.Tick(); // fires the callback exactly once + + Assert.NotNull(captured); + } + + + + [Fact] + public void WithManualProgressTimer_when_extractor_is_null_throws_ArgumentNullException() + { + Assert.Throws + ( + () => ((ExtractorBase)null!).WithManualProgressTimer(new ManualProgressTimerCore()) + ); + } + + + + [Fact] + public void WithManualProgressTimer_when_extractor_timer_is_null_throws_ArgumentNullException() + { + var sut = new TestExtractor(new[] { 1 }); + + Assert.Throws + ( + () => sut.WithManualProgressTimer(null!) + ); + } + + + + [Fact] + public void WithManualProgressTimer_when_loader_timer_is_null_throws_ArgumentNullException() + { + var sut = new TestLoader(collectItems: false); + + Assert.Throws + ( + () => sut.WithManualProgressTimer(null!) + ); + } + + + + [Fact] + public void WithManualProgressTimer_when_transformer_timer_is_null_throws_ArgumentNullException() + { + var sut = new TestTransformer(); + + Assert.Throws + ( + () => sut.WithManualProgressTimer(null!) + ); + } + + + + // A synchronous IProgress so a Tick's callback is observed inline (System.Progress posts async). + private sealed class SyncProgress : IProgress + { + private readonly Action _callback; + + public SyncProgress(Action callback) => _callback = callback; + + public void Report(T value) => _callback(value); + } +}