From aea665b3751bf3412338ba196529c0ad48e6c7be Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 19:52:34 +0000 Subject: [PATCH 1/2] fix(dummies): qualify exhaustion replay hint for foreign generators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A distinct collection whose element generator is foreign (carries no IHasRandomSource) falls back to the ambient source for its count and layout, but the foreign generator's own draws ignore that seed. The exhaustion message nonetheless appended the faithful "reproduce this run with Any.Reproducibly(seed, ...)" hint, so a user who replayed the seed got different elements — and not even the same shortfall — the exact trust failure the seed reporting exists to prevent. Thread the failing generator into CollectionState.Exhausted and key the hint on AnyDerivation.SourceOf(culprit): when it is null — a foreign IAny, or an As/Combine derivation built over one — emit a qualified hint that scopes the promise to the count and layout. The discriminator is the null source, not an "is IHasRandomSource" test: DerivedAny carries a null source over a foreign operand and must be qualified too. Both exhaustion sites are covered — the element generator (FillDistinct) and a ContainingAny(...) generator (DrawFresh) — keyed on that specific generator's source. The qualified phrasing lives on RandomSource (PartialReplayHint, a twin of ReplayHint) so it names Any.WithSeed(seed) when the collection's own source is fixed, never the inapplicable Any.Reproducibly, staying consistent with #211. Library-sourced and As-over-library generators are unchanged: they stay faithful and keep the full-replay hint. The dead null-guard the issue also flagged was already removed by #211. Add four tests covering the foreign, As-over-foreign, foreign-ContainingAny-over-fixed-source, and unchanged-library cases. Refs: #212 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018tME7DBWLTD7rSa5TWtodi --- Dummies.UnitTests/AnyCollectionTests.cs | 75 +++++++++++++++++++++++++ Dummies/CollectionState.cs | 17 ++++-- Dummies/RandomSource.cs | 18 ++++++ 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index 10d24726..ce10d802 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -354,6 +354,69 @@ public void CollectionsComposeThroughAsAndCombine() { Check.That(list).ContainsOnlyElementsThatMatch(reference => reference.Value.StartsWith("ORD-")); } + [Fact(DisplayName = "Exhaustion over a foreign element generator qualifies the replay hint instead of promising a full replay of the elements.")] + public void ExhaustionOverAForeignElementGeneratorQualifiesTheHint() { + // A foreign IAny carries no IHasRandomSource, so the collection falls back to the ambient source for its count + // and layout while the foreign generator's own draws ignore that seed. The reported seed therefore cannot + // replay the elements, and the message must not claim it can. + AnyGenerationException caught = Assert.Throws( + () => Any.Reproducibly(2026, () => Any.SetOf(new ForeignPair()).WithCount(5).Generate(), _ => { })); + + Check.That(caught.Seed).IsEqualTo(2026); + Check.That(caught.Message).Contains("the element generator"); + Check.That(caught.Message).Contains("not reproducible from this seed alone"); + Check.That(caught.Message).Contains("Any.Reproducibly(2026"); + // The faithful full-replay sentence must be gone — it is the false promise this fix removes. + Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); + } + + [Fact(DisplayName = "A derivation built over a foreign generator is qualified too: the discriminator is a null source, not the IHasRandomSource type.")] + public void ExhaustionOverAnAsDerivedForeignGeneratorQualifiesTheHint() { + // DerivedAny (from As) implements IHasRandomSource but propagates a null source when its operand is foreign, + // so its elements are as unreproducible as the foreign generator's. Keying on the type rather than the null + // source would misclassify this as faithful and keep over-promising. + IAny derivedOverForeign = new ForeignPair().As(value => value); + + AnyGenerationException caught = Assert.Throws( + () => Any.SetOf(derivedOverForeign).WithCount(5).Generate()); + + Check.That(caught.Message).Contains("not reproducible from this seed alone"); + Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); + } + + [Fact(DisplayName = "A foreign ContainingAny generator is qualified at its own site, and a fixed source is named as Any.WithSeed rather than Any.Reproducibly.")] + public void ExhaustionOverAForeignContainingAnyQualifiesAndNamesTheFixedSource() { + // The collection's own elements come from a fixed Any.WithSeed(...) context (faithful), but the ContainingAny + // draw is foreign. The twin exhaustion site must qualify the hint for that specific generator — and, because + // the collection's source is fixed, name Any.WithSeed, never the inapplicable Any.Reproducibly. + AnyContext seeded = Any.WithSeed(4242); + + AnyGenerationException caught = Assert.Throws( + () => Any.SetOf(seeded.Int32()).Containing(0).Containing(1).ContainingAny(new ForeignPair()).Generate()); + + Check.That(caught.Seed).IsEqualTo(4242); + Check.That(caught.Message).Contains("a ContainingAny(...) generator"); + Check.That(caught.Message).Contains("Any.WithSeed(4242)"); + Check.That(caught.Message).Contains("not reproducible from this seed alone"); + Check.That(caught.Message).Not.Contains("Any.Reproducibly("); + } + + [Fact(DisplayName = "Exhaustion over a library element generator keeps the faithful full-replay hint unchanged.")] + public void ExhaustionOverALibraryElementGeneratorKeepsTheFaithfulHint() { + // A comparer collapses the effective domain below the requested count, so a library generator — whose draws do + // follow the reported seed — exhausts the bounded draw. Its message must stay the faithful one: the fix only + // touches the genuinely-foreign case. + IEqualityComparer modTen = new ModuloComparer(10); + + AnyGenerationException caught = Assert.Throws( + () => Any.Reproducibly(1234, () => Any.SetOf(Any.Int32().Between(0, 999), modTen).WithCount(20).Generate(), _ => { })); + + Check.That(caught.Seed).IsEqualTo(1234); + Check.That(caught.Message).Contains("The arbitrary values were seeded with 1234"); + Check.That(caught.Message).Contains("Any.Reproducibly(1234"); + Check.That(caught.Message).Not.Contains("not reproducible from this seed alone"); + } + #region Nested types private sealed class ModuloComparer : IEqualityComparer { @@ -374,6 +437,18 @@ public int GetHashCode(int obj) { } + private sealed class ForeignPair : IAny { + + private int _n; + + // Foreign on purpose: implements IAny but NOT IHasRandomSource, so it does not draw from the collection's + // reported source. It yields only two distinct values (0 and 1), driving a distinct collection past its budget. + public int Generate() { + return _n++ % 2; + } + + } + #endregion } diff --git a/Dummies/CollectionState.cs b/Dummies/CollectionState.cs index 0be5c910..41c04cb9 100644 --- a/Dummies/CollectionState.cs +++ b/Dummies/CollectionState.cs @@ -215,7 +215,7 @@ private T DrawFresh(IAny generator, HashSet seen, RandomSource source, int for (int collisions = 0;;) { T value = generator.Generate(); if (!seen.Contains(value)) { return value; } - if (++collisions > budget) { throw Exhausted(source, seen.Count, target, "a ContainingAny(...) generator"); } + if (++collisions > budget) { throw Exhausted(source, seen.Count, target, "a ContainingAny(...) generator", generator); } } } @@ -228,7 +228,7 @@ private void FillDistinct(List ordered, HashSet seen, RandomSource source, ordered.Add(value); collisions = 0; } else if (++collisions > budget) { - throw Exhausted(source, ordered.Count, target, "the element generator"); + throw Exhausted(source, ordered.Count, target, "the element generator", _item); } } } @@ -243,9 +243,16 @@ private int ExhaustionBudget(int target) { return (int)Math.Min(Math.Max(bounded, 10_000L), int.MaxValue); } - 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. {source.ReplayHint(seed)}"; + private static AnyGenerationException Exhausted(RandomSource source, int reached, int target, string what, IAny culprit) { + int seed = source.Current.Seed; + // The reported seed reproduces the count and layout, but it reproduces the elements only when the failing + // generator draws from that same source. A generator carrying no source of its own — a foreign IAny, or a + // derivation built over one (AnyDerivation.SourceOf is null in both cases) — ignores the seed, so promising a + // full replay of its elements would be false: qualify the hint to the count and layout instead. + string replay = AnyDerivation.SourceOf(culprit) is null + ? source.PartialReplayHint(seed) + : source.ReplayHint(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. {replay}"; return new AnyGenerationException(message, seed); } diff --git a/Dummies/RandomSource.cs b/Dummies/RandomSource.cs index 932eee32..d7a768cc 100644 --- a/Dummies/RandomSource.cs +++ b/Dummies/RandomSource.cs @@ -20,6 +20,16 @@ internal abstract class RandomSource { /// internal abstract string ReplayHint(int seed); + /// + /// The reproduction guidance for a distinct-collection failure whose count and layout this source seeded but + /// whose elements are drawn from a generator that does not draw from this source — a foreign + /// , or a derivation built over one. It names the same replay mechanism as + /// for the count and layout, but scopes the promise to them: the element sequence is + /// not reproducible from this seed alone, so claiming a full replay would be the misleading diagnostic the seed + /// reporting exists to avoid. + /// + internal abstract string PartialReplayHint(int seed); + } /// A pseudo-random generator that remembers the seed it was created from. @@ -82,6 +92,10 @@ internal override string ReplayHint(int seed) { return $"The arbitrary values were seeded with {seed}; reproduce this run with Any.Reproducibly({seed}, ...)."; } + internal override string PartialReplayHint(int seed) { + return $"The collection's count and layout were seeded with {seed} (Any.Reproducibly({seed}, ...)), but its elements come from a generator that does not draw from this source, so the element sequence is not reproducible from this seed alone."; + } + #region Nested types private sealed class SeedScope : IDisposable { @@ -125,6 +139,10 @@ internal override string ReplayHint(int seed) { return $"The arbitrary values were drawn from Any.WithSeed({seed}), which already replays deterministically."; } + internal override string PartialReplayHint(int seed) { + return $"The collection's count and layout were drawn from Any.WithSeed({seed}), but its elements come from a generator that does not draw from it, so the element sequence is not reproducible from this seed alone."; + } + } /// From 9213baf9b43d1045cd8fe398cbabfae00d03c8fc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 21 Jul 2026 20:23:59 +0000 Subject: [PATCH 2/2] fix(dummies): qualify replay hint for mixed-source derivations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Combine derivation with at least one foreign operand keeps a sourced operand's non-null source through the SourceOf(first) ?? ... collapse, so the previous discriminator — a null source — classified it as faithful and still promised a full Any.Reproducibly replay, even though the composed value follows the foreign draw. This is the same over-promise one level up, surfaced in review of #212. Add AnyDerivation.IsReproducible: a generator is reproducible when it draws only from its reported source — a library generator with a source, or a derivation whose operands are all themselves reproducible. Record it on DerivedAny (IReproducibilityHint) at construction from the operands, computed in As, Combine (arities 2-8) and OrNull. A non-null source to name and full reproducibility are now distinct signals. Route both replay sites through the predicate: distinct-collection exhaustion (CollectionState.Exhausted) and composer/factory failures (AnyDerivation.Invoke). Generalize the qualified hint on RandomSource (PartialReplayHint) so it phrases both the collection-element and the composed-value case, per source (Any.Reproducibly or Any.WithSeed). Add tests for the exhaustion and composer-failure paths over a Combine that mixes a foreign operand with a library one. Refs: #212 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_018tME7DBWLTD7rSa5TWtodi --- Dummies.UnitTests/AnyCollectionTests.cs | 15 ++++++++ Dummies.UnitTests/CompositionTests.cs | 33 +++++++++++++++++ Dummies/Any.cs | 49 ++++++++++++++----------- Dummies/AnyDerivation.cs | 38 +++++++++++++++---- Dummies/AnyExtensions.cs | 7 ++-- Dummies/CollectionState.cs | 13 ++++--- Dummies/NullableExtensions.cs | 10 +++-- Dummies/RandomSource.cs | 29 +++++++++++---- 8 files changed, 144 insertions(+), 50 deletions(-) diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index 99433e64..1899b3a0 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -417,6 +417,21 @@ public void ExhaustionOverALibraryElementGeneratorKeepsTheFaithfulHint() { Check.That(caught.Message).Not.Contains("not reproducible from this seed alone"); } + [Fact(DisplayName = "Exhaustion over a Combine that mixes a foreign operand is qualified, even though a library operand supplies a non-null source.")] + public void ExhaustionOverACombineMixingAForeignOperandQualifiesTheHint() { + // Any.Combine keeps the library operand's non-null source (SourceOf(first) ?? SourceOf(second)), but the + // composed value follows the foreign draw, so the elements are not reproducible from the reported seed. The + // discriminator is full reproducibility, not merely a non-null source. + IAny mixed = Any.Combine(new ForeignPair(), Any.Int32(), (foreign, _) => foreign); + + AnyGenerationException caught = Assert.Throws( + () => Any.Reproducibly(777, () => Any.SetOf(mixed).WithCount(5).Generate(), _ => { })); + + Check.That(caught.Seed).IsEqualTo(777); + Check.That(caught.Message).Contains("not reproducible from this seed alone"); + Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); + } + #region Nested types private sealed class ModuloComparer : IEqualityComparer { diff --git a/Dummies.UnitTests/CompositionTests.cs b/Dummies.UnitTests/CompositionTests.cs index 8b408005..c14c3bfc 100644 --- a/Dummies.UnitTests/CompositionTests.cs +++ b/Dummies.UnitTests/CompositionTests.cs @@ -136,6 +136,25 @@ public void CombineOverFixedContextReportsWithSeedHint() { Check.That(caught.Message).Not.Contains("Any.Reproducibly("); } + [Fact(DisplayName = "A composer failure over a Combine mixing a foreign operand qualifies the replay hint, though a library operand supplies a nameable source.")] + public void CombineOverMixedForeignAndLibraryQualifiesTheHint() { + // The foreign operand has no source, but Any.Int32()'s ambient source survives the ?? collapse, so a naive + // "non-null source means faithful" rule would over-promise. The composed value depends on the foreign draw, so + // the hint must be qualified even though a seed can still be named. + IAny generator = Any.Combine( + new ForeignInt(), + Any.Int32().Between(1, 3), + (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("Combine(...)"); + Check.That(caught.Message).Contains("not reproducible from this seed alone"); + Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); + } + [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); @@ -202,4 +221,18 @@ public void DerivedGeneratorsDrawFreshValues() { Check.That(seen.Count).IsStrictlyGreaterThan(1); } + #region Nested types + + private sealed class ForeignInt : IAny { + + // Foreign on purpose: implements IAny but NOT IHasRandomSource, so it draws from no reported source and a + // Combine that includes it is not fully reproducible even when another operand carries one. + public int Generate() { + return 0; + } + + } + + #endregion + } diff --git a/Dummies/Any.cs b/Dummies/Any.cs index d46b876e..bcd758ed 100644 --- a/Dummies/Any.cs +++ b/Dummies/Any.cs @@ -443,13 +443,14 @@ public static IAny Combine(IAny first, IAny se if (second is null) { throw new ArgumentNullException(nameof(second)); } if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second); + RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second); + bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second); - return new DerivedAny(source, () => { + return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); T2 secondValue = second.Generate(); - return AnyDerivation.Invoke(() => compose(firstValue, secondValue), source, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)})"); + return AnyDerivation.Invoke(() => compose(firstValue, secondValue), source, reproducible, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)})"); }); } @@ -473,14 +474,15 @@ public static IAny Combine(IAny first, IAny(source, () => { + return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); T2 secondValue = second.Generate(); T3 thirdValue = third.Generate(); - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue), source, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)})"); + return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue), source, reproducible, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)})"); }); } @@ -506,15 +508,16 @@ public static IAny Combine(IAny first, IAn if (fourth is null) { throw new ArgumentNullException(nameof(fourth)); } if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth); + RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth); + bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth); - return new DerivedAny(source, () => { + return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); T2 secondValue = second.Generate(); T3 thirdValue = third.Generate(); T4 fourthValue = fourth.Generate(); - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue), source, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)})"); + return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue), source, reproducible, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)})"); }); } @@ -543,16 +546,17 @@ public static IAny Combine(IAny first, if (fifth is null) { throw new ArgumentNullException(nameof(fifth)); } if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth); + RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth); + bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth) && AnyDerivation.IsReproducible(fifth); - return new DerivedAny(source, () => { + return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); T2 secondValue = second.Generate(); T3 thirdValue = third.Generate(); T4 fourthValue = fourth.Generate(); T5 fifthValue = fifth.Generate(); - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue), source, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)})"); + return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue), source, reproducible, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)})"); }); } @@ -584,9 +588,10 @@ public static IAny Combine(IAny fi if (sixth is null) { throw new ArgumentNullException(nameof(sixth)); } if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth) ?? AnyDerivation.SourceOf(sixth); + RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth) ?? AnyDerivation.SourceOf(sixth); + bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth) && AnyDerivation.IsReproducible(fifth) && AnyDerivation.IsReproducible(sixth); - return new DerivedAny(source, () => { + return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); T2 secondValue = second.Generate(); T3 thirdValue = third.Generate(); @@ -594,7 +599,7 @@ public static IAny Combine(IAny fi T5 fifthValue = fifth.Generate(); T6 sixthValue = sixth.Generate(); - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue, sixthValue), source, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)}, {AnyDerivation.Display(sixthValue)})"); + return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue, sixthValue), source, reproducible, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)}, {AnyDerivation.Display(sixthValue)})"); }); } @@ -631,9 +636,10 @@ public static IAny Combine(IAny(source, () => { + return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); T2 secondValue = second.Generate(); T3 thirdValue = third.Generate(); @@ -642,7 +648,7 @@ public static IAny Combine(IAny compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue, sixthValue, seventhValue), source, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)}, {AnyDerivation.Display(sixthValue)}, {AnyDerivation.Display(seventhValue)})"); + return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue, sixthValue, seventhValue), source, reproducible, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)}, {AnyDerivation.Display(sixthValue)}, {AnyDerivation.Display(seventhValue)})"); }); } @@ -683,9 +689,10 @@ public static IAny Combine(IAn if (eighth is null) { throw new ArgumentNullException(nameof(eighth)); } if (compose is null) { throw new ArgumentNullException(nameof(compose)); } - RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth) ?? AnyDerivation.SourceOf(sixth) ?? AnyDerivation.SourceOf(seventh) ?? AnyDerivation.SourceOf(eighth); + RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second) ?? AnyDerivation.SourceOf(third) ?? AnyDerivation.SourceOf(fourth) ?? AnyDerivation.SourceOf(fifth) ?? AnyDerivation.SourceOf(sixth) ?? AnyDerivation.SourceOf(seventh) ?? AnyDerivation.SourceOf(eighth); + bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth) && AnyDerivation.IsReproducible(fifth) && AnyDerivation.IsReproducible(sixth) && AnyDerivation.IsReproducible(seventh) && AnyDerivation.IsReproducible(eighth); - return new DerivedAny(source, () => { + return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); T2 secondValue = second.Generate(); T3 thirdValue = third.Generate(); @@ -695,7 +702,7 @@ public static IAny Combine(IAn T7 seventhValue = seventh.Generate(); T8 eighthValue = eighth.Generate(); - return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue, sixthValue, seventhValue, eighthValue), source, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)}, {AnyDerivation.Display(sixthValue)}, {AnyDerivation.Display(seventhValue)}, {AnyDerivation.Display(eighthValue)})"); + return AnyDerivation.Invoke(() => compose(firstValue, secondValue, thirdValue, fourthValue, fifthValue, sixthValue, seventhValue, eighthValue), source, reproducible, $"the composer passed to Combine(...) threw for the generated values ({AnyDerivation.Display(firstValue)}, {AnyDerivation.Display(secondValue)}, {AnyDerivation.Display(thirdValue)}, {AnyDerivation.Display(fourthValue)}, {AnyDerivation.Display(fifthValue)}, {AnyDerivation.Display(sixthValue)}, {AnyDerivation.Display(seventhValue)}, {AnyDerivation.Display(eighthValue)})"); }); } diff --git a/Dummies/AnyDerivation.cs b/Dummies/AnyDerivation.cs index 9d1af729..1c3978e8 100644 --- a/Dummies/AnyDerivation.cs +++ b/Dummies/AnyDerivation.cs @@ -9,25 +9,31 @@ namespace Dummies; /// /// A generator derived from other generators (As, Combine): it delegates generation to a closure /// and carries, when known, the random context of the generators it derives from — so a failure inside the -/// derivation can still name the seed that replays the run. +/// derivation can still name the seed that replays the run. It also remembers whether every operand it draws from +/// is reproducible (): a single foreign operand leaves a non-null source to name +/// but makes the derived value unreproducible, which the seed reporting must not over-promise. /// /// The type of the generated values. -internal sealed class DerivedAny : IAny, IHasRandomSource { +internal sealed class DerivedAny : IAny, IHasRandomSource, IReproducibilityHint { #region Fields declarations + private readonly bool _drawsOnlyFromSource; private readonly Func _generate; private readonly RandomSource? _source; #endregion - internal DerivedAny(RandomSource? source, Func generate) { - _source = source; - _generate = generate; + internal DerivedAny(RandomSource? source, bool drawsOnlyFromSource, Func generate) { + _source = source; + _drawsOnlyFromSource = drawsOnlyFromSource; + _generate = generate; } RandomSource? IHasRandomSource.Source => _source; + bool IReproducibilityHint.DrawsOnlyFromSource => _drawsOnlyFromSource; + /// public T Generate() { return _generate(); @@ -43,6 +49,20 @@ internal static class AnyDerivation { return (generator as IHasRandomSource)?.Source; } + /// + /// Whether every value yields is replayable from the source it reports: true + /// for a library generator carrying a source, and for a derivation whose operands are all themselves + /// reproducible; false for a foreign generator (no source) or a derivation built over one. This is + /// stronger than being non-null — a Combine that mixes a foreign operand with a + /// library one keeps a non-null source to name, yet its composed value follows the foreign draw and cannot be + /// replayed from that seed. + /// + internal static bool IsReproducible(IAny generator) { + if (generator is IReproducibilityHint hint) { return hint.DrawsOnlyFromSource; } + + return SourceOf(generator) is not null; + } + /// /// A conservative upper bound on the number of distinct values yields, when it /// advertises one through ; null when the domain is unbounded or unknown. @@ -54,9 +74,11 @@ internal static class AnyDerivation { /// /// Runs a user-supplied factory or composer and converts its failure into an /// that names the generated value(s) and, when the random context is - /// known, the seed that replays the run. The library's own exceptions pass through untouched. + /// known, the seed that replays the run. tells whether the derived value draws + /// only from that source: when it does not — a foreign operand contributes — the hint is qualified rather than + /// promising a full replay the seed cannot deliver. The library's own exceptions pass through untouched. /// - internal static T Invoke(Func invoke, RandomSource? source, string failure) { + internal static T Invoke(Func invoke, RandomSource? source, bool reproducible, string failure) { try { return invoke(); } catch (AnyException) { @@ -65,7 +87,7 @@ internal static T Invoke(Func invoke, RandomSource? source, string failure int? seed = source?.Current.Seed; string message = $"Generation failed: {failure} ({exception.GetType().Name}: {exception.Message})."; if (source is not null) { - message += $" {source.ReplayHint(seed!.Value)}"; + message += $" {(reproducible ? source.ReplayHint(seed!.Value) : source.PartialReplayHint(seed!.Value))}"; } throw new AnyGenerationException(message, seed, exception); diff --git a/Dummies/AnyExtensions.cs b/Dummies/AnyExtensions.cs index e656e543..54a7e522 100644 --- a/Dummies/AnyExtensions.cs +++ b/Dummies/AnyExtensions.cs @@ -37,12 +37,13 @@ public static IAny As(this IAny generator, F if (generator is null) { throw new ArgumentNullException(nameof(generator)); } if (factory is null) { throw new ArgumentNullException(nameof(factory)); } - RandomSource? source = AnyDerivation.SourceOf(generator); + RandomSource? source = AnyDerivation.SourceOf(generator); + bool reproducible = AnyDerivation.IsReproducible(generator); - return new DerivedAny(source, () => { + return new DerivedAny(source, reproducible, () => { TSource value = generator.Generate(); - return AnyDerivation.Invoke(() => factory(value), source, $"the factory passed to As(...) threw for the generated value {AnyDerivation.Display(value)}"); + return AnyDerivation.Invoke(() => factory(value), source, reproducible, $"the factory passed to As(...) threw for the generated value {AnyDerivation.Display(value)}"); }); } diff --git a/Dummies/CollectionState.cs b/Dummies/CollectionState.cs index 41c04cb9..8db119dd 100644 --- a/Dummies/CollectionState.cs +++ b/Dummies/CollectionState.cs @@ -246,12 +246,13 @@ private int ExhaustionBudget(int target) { private static AnyGenerationException Exhausted(RandomSource source, int reached, int target, string what, IAny culprit) { int seed = source.Current.Seed; // The reported seed reproduces the count and layout, but it reproduces the elements only when the failing - // generator draws from that same source. A generator carrying no source of its own — a foreign IAny, or a - // derivation built over one (AnyDerivation.SourceOf is null in both cases) — ignores the seed, so promising a - // full replay of its elements would be false: qualify the hint to the count and layout instead. - string replay = AnyDerivation.SourceOf(culprit) is null - ? source.PartialReplayHint(seed) - : source.ReplayHint(seed); + // generator's every draw follows that same source. A foreign IAny, a derivation built over one, or a Combine + // that mixes a foreign operand with a sourced one is not fully reproducible (AnyDerivation.IsReproducible is + // false) — even where it still carries a non-null source to name — so promising a full replay of its elements + // would be false: qualify the hint instead. + string replay = AnyDerivation.IsReproducible(culprit) + ? source.ReplayHint(seed) + : source.PartialReplayHint(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. {replay}"; return new AnyGenerationException(message, seed); diff --git a/Dummies/NullableExtensions.cs b/Dummies/NullableExtensions.cs index 04587418..27073361 100644 --- a/Dummies/NullableExtensions.cs +++ b/Dummies/NullableExtensions.cs @@ -33,9 +33,10 @@ public static class NullableExtensions { where T : struct { if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - RandomSource? source = AnyDerivation.SourceOf(generator); + RandomSource? source = AnyDerivation.SourceOf(generator); + bool reproducible = AnyDerivation.IsReproducible(generator); - return new DerivedAny(source, () => { + return new DerivedAny(source, reproducible, () => { RandomSource working = source ?? AmbientRandomSource.Instance; return working.Current.Random.Next(2) == 0 ? (T?)null : generator.Generate(); @@ -68,9 +69,10 @@ public static class NullableReferenceExtensions { where T : class { if (generator is null) { throw new ArgumentNullException(nameof(generator)); } - RandomSource? source = AnyDerivation.SourceOf(generator); + RandomSource? source = AnyDerivation.SourceOf(generator); + bool reproducible = AnyDerivation.IsReproducible(generator); - return new DerivedAny(source, () => { + return new DerivedAny(source, reproducible, () => { RandomSource working = source ?? AmbientRandomSource.Instance; return working.Current.Random.Next(2) == 0 ? (T?)null : generator.Generate(); diff --git a/Dummies/RandomSource.cs b/Dummies/RandomSource.cs index d7a768cc..8e84d3f6 100644 --- a/Dummies/RandomSource.cs +++ b/Dummies/RandomSource.cs @@ -21,12 +21,12 @@ internal abstract class RandomSource { internal abstract string ReplayHint(int seed); /// - /// The reproduction guidance for a distinct-collection failure whose count and layout this source seeded but - /// whose elements are drawn from a generator that does not draw from this source — a foreign - /// , or a derivation built over one. It names the same replay mechanism as - /// for the count and layout, but scopes the promise to them: the element sequence is - /// not reproducible from this seed alone, so claiming a full replay would be the misleading diagnostic the seed - /// reporting exists to avoid. + /// The reproduction guidance for a failure whose seeded draws this source drove but whose result also depends on + /// a generator that does not draw from this source — a foreign , or a derivation built over + /// one (including a Combine that mixes a foreign operand with a sourced one). It names the same replay + /// mechanism as for the seeded part, but scopes the promise to it: the foreign values + /// are not reproducible from this seed alone, so claiming a full replay would be the misleading diagnostic the + /// seed reporting exists to avoid. /// internal abstract string PartialReplayHint(int seed); @@ -93,7 +93,7 @@ internal override string ReplayHint(int seed) { } internal override string PartialReplayHint(int seed) { - return $"The collection's count and layout were seeded with {seed} (Any.Reproducibly({seed}, ...)), but its elements come from a generator that does not draw from this source, so the element sequence is not reproducible from this seed alone."; + return $"The seeded draws were made with {seed} (Any.Reproducibly({seed}, ...)), but some values come from a generator that does not draw from this source, so they are not reproducible from this seed alone."; } #region Nested types @@ -140,7 +140,7 @@ internal override string ReplayHint(int seed) { } internal override string PartialReplayHint(int seed) { - return $"The collection's count and layout were drawn from Any.WithSeed({seed}), but its elements come from a generator that does not draw from it, so the element sequence is not reproducible from this seed alone."; + return $"The seeded draws were made from Any.WithSeed({seed}), but some values come from a generator that does not draw from it, so they are not reproducible from this seed alone."; } } @@ -157,6 +157,19 @@ internal interface IHasRandomSource { } +/// +/// Implemented by derived generators (As, Combine) to report whether every operand they draw from is +/// itself reproducible. A single source-less (foreign) operand makes the derived value unreproducible even when +/// another operand supplies a non-null for the replay hint to name — so a +/// full-replay promise must be withheld. Generators that draw only from their own source do not implement this and +/// are treated as reproducible whenever they carry a source. +/// +internal interface IReproducibilityHint { + + bool DrawsOnlyFromSource { get; } + +} + /// Uniform sampling helpers shared by the generators. internal static class RandomSampling {