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
32 changes: 32 additions & 0 deletions Dummies.UnitTests/CompositionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,38 @@ public void CombineWrapsComposerFailures() {
Check.That(caught.InnerException).IsInstanceOf<InvalidOperationException>();
}

[Fact(DisplayName = "A composer failure over ambient generators reports the Any.Reproducibly replay hint.")]
public void CombineOverAmbientGeneratorsReportsReproduciblyHint() {
IAny<string> generator = Any.Combine<int, int, string>(
Any.Int32().Between(1, 3),
Any.Int32().Between(4, 6),
(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("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<string> generator = Any.Combine<int, int, string>(
seeded.Int32().Between(1, 3),
seeded.Int32().Between(4, 6),
(first, second) => throw new InvalidOperationException($"rejected {first}/{second}"));

AnyGenerationException caught = Assert.Throws<AnyGenerationException>(() => 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<int> part = Any.Int32().Between(1, 9);
Expand Down
7 changes: 5 additions & 2 deletions Dummies.UnitTests/ContinuousExclusionNudgeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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.")]
Expand Down
4 changes: 1 addition & 3 deletions Dummies/AnyDecimal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,7 @@ public AnyDecimal DifferentFrom(decimal value) {

/// <inheritdoc />
public decimal Generate() {
SeededRandom current = _source.Current;

return _spec.Generate(current.Random, current.Seed);
return _spec.Generate(_source);
}

}
4 changes: 2 additions & 2 deletions Dummies/AnyDerivation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ internal static T Invoke<T>(Func<T> 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);
Expand Down
4 changes: 1 addition & 3 deletions Dummies/AnyDouble.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,7 @@ public AnyDouble DifferentFrom(double value) {

/// <inheritdoc />
public double Generate() {
SeededRandom current = _source.Current;

return _spec.Generate(current.Random, current.Seed);
return _spec.Generate(_source);
}

}
8 changes: 5 additions & 3 deletions Dummies/AnyGenerationException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ internal AnyGenerationException(string message, int? seed) : base(message) {
}

/// <summary>
/// The seed of the random context the failing generation drew from, when it is known — pass it to
/// <c>Any.Reproducibly(seed, ...)</c> to replay the run. <c>null</c> 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
/// (<c>Any.Reproducibly(...)</c>) pass it to <c>Any.Reproducibly(seed, ...)</c> to replay the run; a value drawn
/// from an <c>Any.WithSeed(seed)</c> context already replays deterministically on its own. The failure message
/// states which of the two applies. <c>null</c> when the failing generator does not draw from one of the
/// library's random contexts.
/// </summary>
public int? Seed { get; }

Expand Down
4 changes: 1 addition & 3 deletions Dummies/AnyHalf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,7 @@ public AnyHalf DifferentFrom(Half value) {

/// <inheritdoc />
public Half Generate() {
SeededRandom current = _source.Current;

return (Half)_spec.Generate(current.Random, current.Seed);
return (Half)_spec.Generate(_source);
}

}
Expand Down
4 changes: 1 addition & 3 deletions Dummies/AnySingle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,7 @@ public AnySingle DifferentFrom(float value) {

/// <inheritdoc />
public float Generate() {
SeededRandom current = _source.Current;

return (float)_spec.Generate(current.Random, current.Seed);
return (float)_spec.Generate(_source);
}

}
7 changes: 2 additions & 5 deletions Dummies/CollectionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
9 changes: 6 additions & 3 deletions Dummies/ContinuousIntervalSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ internal bool Contains(double value) {
}

/// <summary>Draws one value satisfying the whole specification.</summary>
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)];
}
Expand All @@ -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."));
}

Expand Down
9 changes: 6 additions & 3 deletions Dummies/DecimalIntervalSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@

#endregion

private DecimalIntervalSpec(string typeName, Func<decimal, string> render,

Check warning on line 39 in Dummies/DecimalIntervalSpec.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Constructor has 9 parameters, which is greater than the 7 authorized.
decimal min, string? minConstraint,
decimal max, string? maxConstraint,
IReadOnlyList<decimal>? allowed, string? allowedConstraint,
Expand Down Expand Up @@ -134,7 +134,10 @@
}

/// <summary>Draws one value satisfying the whole specification.</summary>
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)];
}
Expand Down Expand Up @@ -165,8 +168,8 @@
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."));
}

Expand Down
16 changes: 16 additions & 0 deletions Dummies/RandomSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,19 @@
/// time — which is what lets a recipe built outside an <c>Any.Reproducibly(...)</c> scope generate
/// deterministically inside one.
/// </summary>
internal abstract class RandomSource {

Check warning on line 10 in Dummies/RandomSource.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Convert this 'abstract' class to an interface.

/// <summary>The seeded pseudo-random generator to draw from right now.</summary>
internal abstract SeededRandom Current { get; }

/// <summary>
/// The reproduction guidance to append to a generation-failure message, phrased for this kind of source. The
/// ambient source points at <c>Any.Reproducibly(seed, ...)</c>; a fixed <c>Any.WithSeed(...)</c> 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.
/// </summary>
internal abstract string ReplayHint(int seed);

}

/// <summary>A pseudo-random generator that remembers the seed it was created from.</summary>
Expand Down Expand Up @@ -70,6 +78,10 @@
}
}

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 {
Expand Down Expand Up @@ -109,6 +121,10 @@

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.";
}

}

/// <summary>
Expand Down Expand Up @@ -141,7 +157,7 @@
ulong rangeSize = (ulong)(maxInclusive - minInclusive) + 1UL;
ulong draw = random.NextUInt64();

// rangeSize is 0 only when the range spans the full ulong width, which int-derived bounds never do;

Check warning on line 160 in Dummies/RandomSource.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Remove this commented out code.
// guard anyway so the helper stays correct if reused with wider bounds.
if (rangeSize == 0UL) { return unchecked((long)draw); }

Expand Down
Loading