diff --git a/Dummies.UnitTests/CompositionTests.cs b/Dummies.UnitTests/CompositionTests.cs index 2f6c0025..8b408005 100644 --- a/Dummies.UnitTests/CompositionTests.cs +++ b/Dummies.UnitTests/CompositionTests.cs @@ -104,6 +104,38 @@ public void CombineWrapsComposerFailures() { Check.That(caught.InnerException).IsInstanceOf(); } + [Fact(DisplayName = "A composer failure over ambient generators reports the Any.Reproducibly replay hint.")] + public void CombineOverAmbientGeneratorsReportsReproduciblyHint() { + IAny generator = Any.Combine( + Any.Int32().Between(1, 3), + Any.Int32().Between(4, 6), + (first, second) => throw new InvalidOperationException($"rejected {first}/{second}")); + + AnyGenerationException caught = Assert.Throws( + () => Any.Reproducibly(31415, () => generator.Generate(), _ => { })); + + Check.That(caught.Seed).IsEqualTo(31415); + Check.That(caught.Message).Contains("Any.Reproducibly(31415"); + Check.That(caught.Message).Not.Contains("Any.WithSeed("); + } + + [Fact(DisplayName = "A composer failure over an Any.WithSeed(...) context reports the WithSeed replay hint, not the inapplicable Any.Reproducibly instruction.")] + public void CombineOverFixedContextReportsWithSeedHint() { + AnyContext seeded = Any.WithSeed(4242); + + IAny generator = Any.Combine( + seeded.Int32().Between(1, 3), + seeded.Int32().Between(4, 6), + (first, second) => throw new InvalidOperationException($"rejected {first}/{second}")); + + AnyGenerationException caught = Assert.Throws(() => generator.Generate()); + + Check.That(caught.Seed).IsEqualTo(4242); + Check.That(caught.Message).Contains("Combine(...)"); + Check.That(caught.Message).Contains("Any.WithSeed(4242)"); + Check.That(caught.Message).Not.Contains("Any.Reproducibly("); + } + [Fact(DisplayName = "Combine composes four through eight parts, passing every constrained part to the lambda.")] public void CombineSupportsHigherArities() { IAny part = Any.Int32().Between(1, 9); diff --git a/Dummies.UnitTests/ContinuousExclusionNudgeTests.cs b/Dummies.UnitTests/ContinuousExclusionNudgeTests.cs index b6451167..bab7ba54 100644 --- a/Dummies.UnitTests/ContinuousExclusionNudgeTests.cs +++ b/Dummies.UnitTests/ContinuousExclusionNudgeTests.cs @@ -71,7 +71,7 @@ public void DoubleExclusionInsideNarrowRange() { } } - [Fact(DisplayName = "A range whose every representable value is excluded fails with a seeded AnyGenerationException, not by budget exhaustion.")] + [Fact(DisplayName = "A range whose every representable value is excluded fails with a seeded AnyGenerationException whose replay hint points at Any.WithSeed, not the inapplicable Any.Reproducibly.")] public void ExhaustedRangeThrowsSeededGenerationException() { Half min = (Half)1f; Half max = (Half)1.001f; // exactly two representable Half values in [min, max] @@ -81,7 +81,10 @@ public void ExhaustedRangeThrowsSeededGenerationException() { Check.That(thrown.Seed).IsEqualTo(207); Check.That(thrown.Message).Contains("207"); - Check.That(thrown.Message).Contains("Any.Reproducibly("); + // The draw came from Any.WithSeed(207) — a fixed context that replays by itself — so the hint must name it, + // not the ambient Any.Reproducibly(...) instruction, which would not reproduce this run. + Check.That(thrown.Message).Contains("Any.WithSeed(207)"); + Check.That(thrown.Message).Not.Contains("Any.Reproducibly("); } [Fact(DisplayName = "The nudge stays reproducible: the same seed yields the same value across runs.")] diff --git a/Dummies/AnyDecimal.cs b/Dummies/AnyDecimal.cs index f90696fc..a40fb7c5 100644 --- a/Dummies/AnyDecimal.cs +++ b/Dummies/AnyDecimal.cs @@ -162,9 +162,7 @@ public AnyDecimal DifferentFrom(decimal value) { /// public decimal Generate() { - SeededRandom current = _source.Current; - - return _spec.Generate(current.Random, current.Seed); + return _spec.Generate(_source); } } diff --git a/Dummies/AnyDerivation.cs b/Dummies/AnyDerivation.cs index a67d9d45..9d1af729 100644 --- a/Dummies/AnyDerivation.cs +++ b/Dummies/AnyDerivation.cs @@ -64,8 +64,8 @@ internal static T Invoke(Func invoke, RandomSource? source, string failure } catch (Exception exception) { int? seed = source?.Current.Seed; string message = $"Generation failed: {failure} ({exception.GetType().Name}: {exception.Message})."; - if (seed is not null) { - message += $" The arbitrary values were seeded with {seed}; reproduce this run with Any.Reproducibly({seed}, ...)."; + if (source is not null) { + message += $" {source.ReplayHint(seed!.Value)}"; } throw new AnyGenerationException(message, seed, exception); diff --git a/Dummies/AnyDouble.cs b/Dummies/AnyDouble.cs index 2dadb182..34f3ec0b 100644 --- a/Dummies/AnyDouble.cs +++ b/Dummies/AnyDouble.cs @@ -175,9 +175,7 @@ public AnyDouble DifferentFrom(double value) { /// public double Generate() { - SeededRandom current = _source.Current; - - return _spec.Generate(current.Random, current.Seed); + return _spec.Generate(_source); } } diff --git a/Dummies/AnyGenerationException.cs b/Dummies/AnyGenerationException.cs index fa90de27..48923955 100644 --- a/Dummies/AnyGenerationException.cs +++ b/Dummies/AnyGenerationException.cs @@ -37,9 +37,11 @@ internal AnyGenerationException(string message, int? seed) : base(message) { } /// - /// The seed of the random context the failing generation drew from, when it is known — pass it to - /// Any.Reproducibly(seed, ...) to replay the run. null when the failing generator does not draw - /// from one of the library's random contexts. + /// The seed of the random context the failing generation drew from, when it is known. Under the ambient context + /// (Any.Reproducibly(...)) pass it to Any.Reproducibly(seed, ...) to replay the run; a value drawn + /// from an Any.WithSeed(seed) context already replays deterministically on its own. The failure message + /// states which of the two applies. null when the failing generator does not draw from one of the + /// library's random contexts. /// public int? Seed { get; } diff --git a/Dummies/AnyHalf.cs b/Dummies/AnyHalf.cs index e7efc5bc..4c706359 100644 --- a/Dummies/AnyHalf.cs +++ b/Dummies/AnyHalf.cs @@ -193,9 +193,7 @@ public AnyHalf DifferentFrom(Half value) { /// public Half Generate() { - SeededRandom current = _source.Current; - - return (Half)_spec.Generate(current.Random, current.Seed); + return (Half)_spec.Generate(_source); } } diff --git a/Dummies/AnySingle.cs b/Dummies/AnySingle.cs index bede8742..2a54b5fe 100644 --- a/Dummies/AnySingle.cs +++ b/Dummies/AnySingle.cs @@ -186,9 +186,7 @@ public AnySingle DifferentFrom(float value) { /// public float Generate() { - SeededRandom current = _source.Current; - - return (float)_spec.Generate(current.Random, current.Seed); + return (float)_spec.Generate(_source); } } diff --git a/Dummies/CollectionState.cs b/Dummies/CollectionState.cs index ddba71b9..0be5c910 100644 --- a/Dummies/CollectionState.cs +++ b/Dummies/CollectionState.cs @@ -244,11 +244,8 @@ private int ExhaustionBudget(int target) { } private static AnyGenerationException Exhausted(RandomSource source, int reached, int target, string what) { - int? seed = source.Current.Seed; - string message = $"Could not generate a distinct collection of {Elements(target)}: {what} produced only {reached} distinct value(s) before the draw budget was exhausted. Loosen the count or widen the element generator's domain."; - if (seed is not null) { - message += $" The arbitrary values were seeded with {seed}; reproduce this run with Any.Reproducibly({seed}, ...)."; - } + int seed = source.Current.Seed; + string message = $"Could not generate a distinct collection of {Elements(target)}: {what} produced only {reached} distinct value(s) before the draw budget was exhausted. Loosen the count or widen the element generator's domain. {source.ReplayHint(seed)}"; return new AnyGenerationException(message, seed); } diff --git a/Dummies/ContinuousIntervalSpec.cs b/Dummies/ContinuousIntervalSpec.cs index f8ceea8f..ec869793 100644 --- a/Dummies/ContinuousIntervalSpec.cs +++ b/Dummies/ContinuousIntervalSpec.cs @@ -171,7 +171,10 @@ internal bool Contains(double value) { } /// Draws one value satisfying the whole specification. - internal double Generate(Random random, int seed) { + internal double Generate(RandomSource source) { + SeededRandom current = source.Current; + Random random = current.Random; + if (_effectiveAllowed is not null) { return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; } @@ -191,8 +194,8 @@ internal double Generate(Random random, int seed) { double? free = NudgeToFree(candidate, ascending: true) ?? NudgeToFree(candidate, ascending: false); if (free is null) { throw new AnyGenerationException( - $"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. The arbitrary values were seeded with {seed}; reproduce this run with Any.Reproducibly({seed}, ...).", - seed, + $"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. {source.ReplayHint(current.Seed)}", + current.Seed, new InvalidOperationException("No representable value in range remains after applying the exclusions.")); } diff --git a/Dummies/DecimalIntervalSpec.cs b/Dummies/DecimalIntervalSpec.cs index 894973ae..9d0c941f 100644 --- a/Dummies/DecimalIntervalSpec.cs +++ b/Dummies/DecimalIntervalSpec.cs @@ -134,7 +134,10 @@ internal bool Contains(decimal value) { } /// Draws one value satisfying the whole specification. - internal decimal Generate(Random random, int seed) { + internal decimal Generate(RandomSource source) { + SeededRandom current = source.Current; + Random random = current.Random; + if (_effectiveAllowed is not null) { return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; } @@ -165,8 +168,8 @@ internal decimal Generate(Random random, int seed) { decimal next = Clamped(candidate + SmallestStep); if (next == candidate || budget-- == 0) { throw new AnyGenerationException( - $"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. The arbitrary values were seeded with {seed}; reproduce this run with Any.Reproducibly({seed}, ...).", - seed, + $"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. {source.ReplayHint(current.Seed)}", + current.Seed, new InvalidOperationException("The exclusion nudge could not leave the excluded point within the allowed range.")); } diff --git a/Dummies/RandomSource.cs b/Dummies/RandomSource.cs index c6dac8d8..932eee32 100644 --- a/Dummies/RandomSource.cs +++ b/Dummies/RandomSource.cs @@ -12,6 +12,14 @@ internal abstract class RandomSource { /// The seeded pseudo-random generator to draw from right now. internal abstract SeededRandom Current { get; } + /// + /// The reproduction guidance to append to a generation-failure message, phrased for this kind of source. The + /// ambient source points at Any.Reproducibly(seed, ...); a fixed Any.WithSeed(...) context replays + /// deterministically on its own, so pinning the ambient source would not apply — naming the wrong instruction is + /// exactly the misleading diagnostic this method exists to avoid. + /// + internal abstract string ReplayHint(int seed); + } /// A pseudo-random generator that remembers the seed it was created from. @@ -70,6 +78,10 @@ internal override SeededRandom Current { } } + internal override string ReplayHint(int seed) { + return $"The arbitrary values were seeded with {seed}; reproduce this run with Any.Reproducibly({seed}, ...)."; + } + #region Nested types private sealed class SeedScope : IDisposable { @@ -109,6 +121,10 @@ internal FixedRandomSource(int seed) { internal override SeededRandom Current => _random; + internal override string ReplayHint(int seed) { + return $"The arbitrary values were drawn from Any.WithSeed({seed}), which already replays deterministically."; + } + } ///