diff --git a/JustDummies.UnitTests/CompositionTests.cs b/JustDummies.UnitTests/CompositionTests.cs index b6e0010..e7b54c4 100644 --- a/JustDummies.UnitTests/CompositionTests.cs +++ b/JustDummies.UnitTests/CompositionTests.cs @@ -155,6 +155,25 @@ public void CombineOverMixedForeignAndLibraryQualifiesTheHint() { Check.That(caught.Message).Not.Contains("The arbitrary values were seeded with"); } + [Fact(DisplayName = "A composer failure over a Combine mixing two different seeded sources does not promise a full replay from one seed.")] + public void CombineOverMixedSeededSourcesQualifiesTheHint() { + // The first operand draws from Any.WithSeed(4242); the second from the ambient source. The composed value + // depends on BOTH, so replaying WithSeed(4242) alone reproduces only the first — the hint must not promise a + // deterministic full replay from that one seed (issue #319). + IAny generator = Any.Combine( + Any.WithSeed(4242).Int32().Between(1, 3), + Any.Int32().Between(4, 6), + (first, second) => throw new InvalidOperationException($"rejected {first}/{second}")); + + AnyGenerationException caught = Assert.Throws(() => generator.Generate()); + + Check.That(caught.Message).Contains("Combine(...)"); + Check.WithCustomMessage($"The hint over-promised a full replay. Message: {caught.Message}") + .That(caught.Message).Not.Contains("already replays deterministically"); + Check.WithCustomMessage($"The hint did not qualify the replay promise. Message: {caught.Message}") + .That(caught.Message).Contains("not reproducible from this seed alone"); + } + [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/JustDummies/Any.cs b/JustDummies/Any.cs index 5fed7a6..a64dbdb 100644 --- a/JustDummies/Any.cs +++ b/JustDummies/Any.cs @@ -613,7 +613,7 @@ public static IAny Combine(IAny first, IAny se if (compose is null) { throw new ArgumentNullException(nameof(compose)); } RandomSource? source = AnyDerivation.SourceOf(first) ?? AnyDerivation.SourceOf(second); - bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -644,7 +644,7 @@ public static IAny Combine(IAny first, IAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -678,7 +678,7 @@ public static IAny Combine(IAny first, IAn if (compose is null) { throw new ArgumentNullException(nameof(compose)); } 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); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -716,7 +716,7 @@ public static IAny Combine(IAny first, 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); - bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth) && AnyDerivation.IsReproducible(fifth); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -758,7 +758,7 @@ public static IAny Combine(IAny fi 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); - bool reproducible = AnyDerivation.IsReproducible(first) && AnyDerivation.IsReproducible(second) && AnyDerivation.IsReproducible(third) && AnyDerivation.IsReproducible(fourth) && AnyDerivation.IsReproducible(fifth) && AnyDerivation.IsReproducible(sixth); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source) && AnyDerivation.DrawsOnlyFrom(sixth, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -806,7 +806,7 @@ public static IAny Combine(IAny(source, reproducible, () => { T1 firstValue = first.Generate(); @@ -859,7 +859,7 @@ public static IAny Combine(IAn 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); - 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); + bool reproducible = AnyDerivation.DrawsOnlyFrom(first, source) && AnyDerivation.DrawsOnlyFrom(second, source) && AnyDerivation.DrawsOnlyFrom(third, source) && AnyDerivation.DrawsOnlyFrom(fourth, source) && AnyDerivation.DrawsOnlyFrom(fifth, source) && AnyDerivation.DrawsOnlyFrom(sixth, source) && AnyDerivation.DrawsOnlyFrom(seventh, source) && AnyDerivation.DrawsOnlyFrom(eighth, source); return new DerivedAny(source, reproducible, () => { T1 firstValue = first.Generate(); diff --git a/JustDummies/AnyDerivation.cs b/JustDummies/AnyDerivation.cs index 27ace87..f7466ca 100644 --- a/JustDummies/AnyDerivation.cs +++ b/JustDummies/AnyDerivation.cs @@ -69,6 +69,18 @@ internal static bool IsReproducible(IAny generator) { return SourceOf(generator) is not null; } + /// + /// Whether is reproducible and draws from + /// specifically — the per-operand condition for a Combine's full-replay promise. An operand that is + /// individually reproducible but draws from a different seeded source (a second + /// context, or the ambient source alongside a fixed one) leaves the reported seed covering only part of the run, + /// so naming it as a deterministic full replay would over-promise. When the operands do not all draw from the one + /// reported source, the hint is qualified instead — exactly as it is for a foreign operand. + /// + internal static bool DrawsOnlyFrom(IAny generator, RandomSource? source) { + return IsReproducible(generator) && ReferenceEquals(SourceOf(generator), source); + } + /// /// A conservative upper bound on the number of distinct values yields, when it /// advertises one through ; null when the domain is unbounded or unknown.