Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions Dummies.UnitTests/AnyCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnyGenerationException>(
() => 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<int> derivedOverForeign = new ForeignPair().As(value => value);

AnyGenerationException caught = Assert.Throws<AnyGenerationException>(
() => 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<AnyGenerationException>(
() => 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<int> modTen = new ModuloComparer(10);

AnyGenerationException caught = Assert.Throws<AnyGenerationException>(
() => 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<int> mixed = Any.Combine(new ForeignPair(), Any.Int32(), (foreign, _) => foreign);

AnyGenerationException caught = Assert.Throws<AnyGenerationException>(
() => 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<int> {
Expand All @@ -374,6 +452,18 @@ public int GetHashCode(int obj) {

}

private sealed class ForeignPair : IAny<int> {

private int _n;

// Foreign on purpose: implements IAny<int> 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

}
33 changes: 33 additions & 0 deletions Dummies.UnitTests/CompositionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> generator = Any.Combine<int, int, string>(
new ForeignInt(),
Any.Int32().Between(1, 3),
(first, second) => throw new InvalidOperationException($"rejected {first}/{second}"));

AnyGenerationException caught = Assert.Throws<AnyGenerationException>(
() => 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<int> part = Any.Int32().Between(1, 9);
Expand Down Expand Up @@ -202,4 +221,18 @@ public void DerivedGeneratorsDrawFreshValues() {
Check.That(seen.Count).IsStrictlyGreaterThan(1);
}

#region Nested types

private sealed class ForeignInt : IAny<int> {

// Foreign on purpose: implements IAny<int> 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

}
Loading