Skip to content

feat: throw ObjectDisposedException on use-after-dispose; test the guard (#205) - #318

Merged
Chris-Wolfgang merged 2 commits into
vNextfrom
test/205-raise-mutation-score
Jul 23, 2026
Merged

feat: throw ObjectDisposedException on use-after-dispose; test the guard (#205)#318
Chris-Wolfgang merged 2 commits into
vNextfrom
test/205-raise-mutation-score

Conversation

@Chris-Wolfgang

Copy link
Copy Markdown
Owner

Summary

Follow-up to #317 (merged). Adds a real use-after-dispose contract and converts the last testable _disposed-guard mutants from annotated-equivalents into ones killed by real tests.

Refs #205.

What changed

  • New private ThrowIfDisposed() on LoaderBase / ExtractorBase / TransformerBase, called at the top of every public entry point (ExtractAsync / LoadAsync / TransformAsync, all 4 overloads each). Once disposed, they throw ObjectDisposedException instead of silently running.
  • DisposedGuardTests covers every overload after both Dispose() and DisposeAsync(), plus idempotency and the live path.

Why (mutation testing)

Previously the Dispose(bool) idempotency guard was annotated equivalent because _disposed had no reader. Now it has one, so the guard's negation, the _disposed = true assignment, and the whole-method-body removal are all killed by real tests. Only the guard's { return; } block/return stays annotated — dropping it merely re-runs the idempotent _disposed = true, which is genuinely unobservable.

State

  • Score ~97% (97.06%), 370 tests passing.
  • Remaining survivors: SystemProgressTimer start/stop/dispose + base timer.Start — timing-only, deliberately unmasked.
  • Remaining // Stryker disable: the EnsureStarted early-return, this guard's redundant block/return, the sink's redundant cancellation check, and Report's unreachable overflow boundary — all provable equivalents.

Note

ObjectDisposedException on use-after-dispose is a behavior addition (previously such calls ran). No public signature changes.

Chris-Wolfgang and others added 2 commits July 23, 2026 15:12
…ard (#205)

Adds a real dispose contract to the three base classes: once disposed, every
public entry point (ExtractAsync / LoadAsync / TransformAsync, all overloads)
throws ObjectDisposedException via a new private ThrowIfDisposed().

This gives the Dispose(bool) idempotency flag a real reader, so its guard is no
longer "equivalent because _disposed is never read": DisposedGuardTests now kills
the guard's negation, the _disposed assignment, and the whole-method-body removal
by asserting use-after-dispose throws (every overload, after both Dispose and
DisposeAsync) and that Dispose stays idempotent. The 12 ThrowIfDisposed call sites
are each covered.

Only the guard's `{ return; }` block/return removal stays annotated — dropping it
just re-runs the idempotent `_disposed = true`, which is genuinely unobservable.

Score ~97% (97.06%), 370 tests. Remaining survivors: SystemProgressTimer/timer.Start
timing only. Remaining // Stryker disable: the EnsureStarted early-return, this
guard's redundant block/return, the sink's redundant cancellation check, and
Report's unreachable overflow boundary — all provable equivalents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 23, 2026 19:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BenchmarkDotNet

Details
Benchmark suite Current: 993cf8c Previous: 732efdd Ratio
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_NoProgress(RecordCount: 1000) 32732.0576171875 ns (± 80.74249824063652) 32688.512502034504 ns (± 213.01288290577438) 1.00
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_WithProgress(RecordCount: 1000) 34869.42077636719 ns (± 107.25465551142001) 34913.1728108724 ns (± 92.90601829761779) 1.00
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_NoProgress(RecordCount: 100000) 3428437.5520833335 ns (± 46239.06222200528) 3227835.359375 ns (± 5331.793855491133) 1.06
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_WithProgress(RecordCount: 100000) 3446404.3619791665 ns (± 18173.18798666761) 3403880.4401041665 ns (± 29793.796946240178) 1.01
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.FluentPipeline(RecordCount: 1000) 32691.82314046224 ns (± 165.5095721903417) 31655.328531901043 ns (± 117.35331683022335) 1.03
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.ManualComposition(RecordCount: 1000) 32335.262837727863 ns (± 149.18971403849528) 32588.058044433594 ns (± 149.83187729728172) 0.99
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.BaseClassComposition(RecordCount: 1000) 71301.486328125 ns (± 334.2925739232508) 70625.31201171875 ns (± 1269.4931495812575) 1.01
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.FluentPipeline(RecordCount: 100000) 3201795.609375 ns (± 1710.5236012409246) 3210122.2981770835 ns (± 297.86708111041185) 1.00
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.ManualComposition(RecordCount: 100000) 3209299.3411458335 ns (± 8392.07769805364) 3240046.2122395835 ns (± 4670.818271378057) 0.99
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.BaseClassComposition(RecordCount: 100000) 6853578.205729167 ns (± 790.9315334288442) 6896450.057291667 ns (± 10313.052399783468) 0.99

This comment was automatically generated by workflow using github-action-benchmark.

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.

2 participants