diff --git a/Dummies.UnitTests/AnyCollectionTests.cs b/Dummies.UnitTests/AnyCollectionTests.cs index 4c55db61..1899b3a0 100644 --- a/Dummies.UnitTests/AnyCollectionTests.cs +++ b/Dummies.UnitTests/AnyCollectionTests.cs @@ -354,6 +354,84 @@ 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"); + } + + [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 { @@ -374,6 +452,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.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 0be5c910..8db119dd 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,17 @@ 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'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 932eee32..8e84d3f6 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 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); + } /// 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 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 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 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."; + } + } /// @@ -139,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 {