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
14 changes: 7 additions & 7 deletions Dummies.UnitTests/AmbientSeedScopeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Dummies.UnitTests;
/// <summary>
/// The scope form of reproducibility: a handle a caller opens and disposes itself, for a test-framework adapter
/// that observes a test through before/after hooks and therefore has no delegate to wrap (ADR-0035). The seed
/// behaviour must match <c>Any.Reproducibly</c>; what the scope adds is the replay instruction a
/// behaviour must match <c>Any.Reproducibly</c>; what the scope adds is the replay snippet a
/// generation-failure diagnostic names, so a run pinned from outside the test body never advertises a call the
/// test does not contain.
/// </summary>
Expand Down Expand Up @@ -123,7 +123,7 @@ public async Task TheScopeDoesNotLeakAcrossExecutionContexts() {
Check.That(outside).IsNotEqualTo(inside);
}

[Fact(DisplayName = "Without a replay instruction, a generation failure names Any.Reproducibly.")]
[Fact(DisplayName = "Without a replay snippet, a generation failure names Any.Reproducibly.")]
public void WithoutAnInstructionTheFailureNamesTheDelegateRunner() {
AnyGenerationException caught;

Expand All @@ -137,7 +137,7 @@ public void WithoutAnInstructionTheFailureNamesTheDelegateRunner() {
Check.That(caught.Message).Contains("Any.Reproducibly(1234, ...)");
}

[Fact(DisplayName = "With a replay instruction, a generation failure names it instead of Any.Reproducibly.")]
[Fact(DisplayName = "With a replay snippet, a generation failure names it instead of Any.Reproducibly.")]
public void WithAnInstructionTheFailureNamesIt() {
AnyGenerationException caught;

Expand All @@ -152,7 +152,7 @@ public void WithAnInstructionTheFailureNamesIt() {
Check.That(caught.Message).Not.Contains("Any.Reproducibly");
}

[Fact(DisplayName = "The replay instruction also reaches the partial-replay guidance.")]
[Fact(DisplayName = "The replay snippet also reaches the partial-replay guidance.")]
public void TheInstructionReachesThePartialReplayGuidance() {
AnyGenerationException caught;

Expand All @@ -167,7 +167,7 @@ public void TheInstructionReachesThePartialReplayGuidance() {
Check.That(caught.Message).Not.Contains("Any.Reproducibly");
}

[Fact(DisplayName = "The replay instruction is scoped: it does not outlive the scope that supplied it.")]
[Fact(DisplayName = "The replay snippet is scoped: it does not outlive the scope that supplied it.")]
public void TheInstructionDoesNotOutliveItsScope() {
using (Any.UseSeed(1, "[Reproducible(Seed = 1)]")) { }

Expand All @@ -181,12 +181,12 @@ public void TheInstructionDoesNotOutliveItsScope() {
Check.That(caught.Message).Not.Contains("[Reproducible");
}

[Fact(DisplayName = "UseSeed rejects a null replay instruction.")]
[Fact(DisplayName = "UseSeed rejects a null replay snippet.")]
public void UseSeedRejectsANullInstruction() {
Check.ThatCode(() => Any.UseSeed(1, null!)).Throws<ArgumentNullException>();
}

[Theory(DisplayName = "UseSeed rejects a blank replay instruction.")]
[Theory(DisplayName = "UseSeed rejects a blank replay snippet.")]
[InlineData("")]
[InlineData(" ")]
public void UseSeedRejectsABlankInstruction(string instruction) {
Expand Down
2 changes: 1 addition & 1 deletion Dummies.Xunit.UnitTests/ReproducibleAttributeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public void AGenerationFailureNamesTheAttribute() {

Check.That(caught.Seed).IsEqualTo(2026);
Check.That(caught.Message).Contains("[Reproducible(Seed = 2026)]");
// The whole point of the replay instruction: this test contains no Any.Reproducibly call, so naming
// The whole point of the replay snippet: this test contains no Any.Reproducibly call, so naming
// one would send the reader to a call that is not there.
Check.That(caught.Message).Not.Contains("Any.Reproducibly");
}
Expand Down
2 changes: 1 addition & 1 deletion Dummies.Xunit/README.nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Pin it to replay:
[Fact, Reproducible(Seed = 1234)]
public void Order_reference_is_accepted() { /* ... */ }

The same instruction is what a generation failure names, so a diagnostic never
The same snippet is what a generation failure names, so a diagnostic never
points at a call the test does not contain.

## Where it applies
Expand Down
6 changes: 3 additions & 3 deletions Dummies.Xunit/ReproducibleAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public int Seed {
public override void Before(MethodInfo methodUnderTest, IXunitTest test) {
int seed = _seed ?? NewSeed();

Open.Value = new Scope(Any.UseSeed(seed, ReplayInstruction(seed)), seed, Open.Value);
Open.Value = new Scope(Any.UseSeed(seed, ReplaySnippet(seed)), seed, Open.Value);
}

/// <inheritdoc />
Expand All @@ -104,7 +104,7 @@ public override void After(MethodInfo methodUnderTest, IXunitTest test) {
/// What the reader must write to replay the run — the attribute with its seed, not the delegate runner the
/// ambient source names by default, because a test carrying this attribute contains no such call.
/// </summary>
private static string ReplayInstruction(int seed) {
private static string ReplaySnippet(int seed) {
return $"[Reproducible(Seed = {seed})]";
}

Expand All @@ -124,7 +124,7 @@ private static bool HasFailed() {
/// </summary>
internal static string? ReportFor(bool failed, int seed) {
return failed
? $"[Dummies] These arbitrary values were seeded with {seed}. Reproduce this run with {ReplayInstruction(seed)}."
? $"[Dummies] These arbitrary values were seeded with {seed}. Reproduce this run with {ReplaySnippet(seed)}."
: null;
}

Expand Down
38 changes: 22 additions & 16 deletions Dummies/Any.cs
Original file line number Diff line number Diff line change
Expand Up @@ -428,34 +428,40 @@ public static IDisposable UseSeed(int seed) {
}

/// <summary>
/// Pins the ambient random context to <paramref name="seed" /> and names, through
/// <paramref name="replayInstruction" />, what a reader must write to replay this run — the form a
/// test-framework adapter uses. A generation failure appends a reproduction guidance naming the mechanism that
/// actually replays the run; the default names <c>Any.Reproducibly(seed, ...)</c>, which is the wrong
/// instruction for a run pinned from outside the test body, where replaying means changing what the adapter
/// reads instead.
/// Pins the ambient random context to <paramref name="seed" /> and supplies the <b>replay snippet</b> — the
/// code a reader copies to replay this run — that generation-failure guidance will embed. This is the form a
/// test-framework adapter uses: the default snippet is <c>Any.Reproducibly(seed, ...)</c>, which points at a
/// call a test pinned from outside its own body does not contain, where replaying means changing what the
/// adapter reads instead.
/// </summary>
/// <remarks>
/// <para>
/// The instruction is quoted verbatim into the guidance, so pass what the reader must write — an attribute
/// with its seed argument, a runner setting — not a sentence about it. It is not validated beyond being
/// non-blank: a badly phrased instruction degrades the very diagnostic it is meant to improve.
/// A failure's guidance is one sentence embedding this snippet, so pass the code itself — an attribute with
/// its seed argument, a runner setting — not a sentence about it. It is quoted verbatim and validated only
/// for being non-blank: a badly phrased snippet degrades the very diagnostic it is meant to improve.
/// </para>
/// <example>
/// <code>
/// using (Any.UseSeed(1234, "[Reproducible(Seed = 1234)]")) { /* ... */ }
/// // A generation failure then reads:
/// // The arbitrary values were seeded with 1234; reproduce this run with [Reproducible(Seed = 1234)].
/// </code>
/// </example>
/// <para>
/// Everything else matches <see cref="UseSeed(int)" />: the scope flows with the execution context, nests,
/// and restores the previous ambient context when disposed.
/// </para>
/// </remarks>
/// <param name="seed">The seed pinning the ambient context's value sequence.</param>
/// <param name="replayInstruction">What a reader must write to replay this run, quoted verbatim into generation-failure guidance.</param>
/// <param name="replaySnippet">The code a reader copies to replay this run, quoted verbatim into generation-failure guidance.</param>
/// <returns>A handle that restores the previous ambient context when disposed.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="replayInstruction" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="replayInstruction" /> is empty or white space.</exception>
public static IDisposable UseSeed(int seed, string replayInstruction) {
if (replayInstruction is null) { throw new ArgumentNullException(nameof(replayInstruction)); }
if (replayInstruction.Trim().Length == 0) { throw new ArgumentException("The replay instruction must name what a reader writes to replay the run; pass a non-blank instruction, or use the overload without one to name Any.Reproducibly(seed, ...).", nameof(replayInstruction)); }
/// <exception cref="ArgumentNullException">Thrown when <paramref name="replaySnippet" /> is <c>null</c>.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="replaySnippet" /> is empty or white space.</exception>
public static IDisposable UseSeed(int seed, string replaySnippet) {
if (replaySnippet is null) { throw new ArgumentNullException(nameof(replaySnippet)); }
if (replaySnippet.Trim().Length == 0) { throw new ArgumentException("The replay snippet must be the code a reader copies to replay the run; pass a non-blank snippet, or use the overload without one to name Any.Reproducibly(seed, ...).", nameof(replaySnippet)); }

return AmbientRandomSource.UseSeed(seed, replayInstruction);
return AmbientRandomSource.UseSeed(seed, replaySnippet);
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion Dummies/AnyDerivation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ internal static T Invoke<T>(Func<T> invoke, RandomSource? source, bool reproduci
int? seed = source?.Current.Seed;
string message = $"Generation failed: {failure} ({exception.GetType().Name}: {exception.Message}).";
if (source is not null) {
message += $" {(reproducible ? source.ReplayHint(seed!.Value) : source.PartialReplayHint(seed!.Value))}";
message += $" {(reproducible ? source.ReplayGuidance(seed!.Value) : source.PartialReplayGuidance(seed!.Value))}";
}

throw new AnyGenerationException(message, seed, exception);
Expand Down
4 changes: 2 additions & 2 deletions Dummies/CollectionState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,8 @@ private static AnyGenerationException Exhausted(RandomSource source, int reached
// 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);
? source.ReplayGuidance(seed)
: source.PartialReplayGuidance(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);
Expand Down
2 changes: 1 addition & 1 deletion Dummies/ContinuousIntervalSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@

#endregion

private ContinuousIntervalSpec(string typeName, Func<double, string> render, Func<double, double> quantize, Func<double, double> nextUp,

Check warning on line 71 in Dummies/ContinuousIntervalSpec.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Constructor has 11 parameters, which is greater than the 7 authorized.
double min, string? minConstraint,
double max, string? maxConstraint,
IReadOnlyList<double>? allowed, string? allowedConstraint,
Expand Down Expand Up @@ -153,7 +153,7 @@
internal long? Cardinality {
get {
if (_effectiveAllowed is not null) { return _effectiveAllowed.Count; }
if (_min == _max) { return 1; }

Check warning on line 156 in Dummies/ContinuousIntervalSpec.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Do not check floating point equality with exact values, use a range instead.

return null;
}
Expand All @@ -179,7 +179,7 @@
return _effectiveAllowed[random.Next(_effectiveAllowed.Count)];
}

if (_min == _max) { return _min; }

Check warning on line 182 in Dummies/ContinuousIntervalSpec.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Do not check floating point equality with exact values, use a range instead.

// Sample around the midpoint so the span (max - min) never overflows to infinity on wide ranges.
double mid = _min / 2 + _max / 2;
Expand All @@ -194,7 +194,7 @@
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. {source.ReplayHint(current.Seed)}",
$"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. {source.ReplayGuidance(current.Seed)}",
current.Seed,
new InvalidOperationException("No representable value in range remains after applying the exclusions."));
}
Expand Down Expand Up @@ -232,7 +232,7 @@

private bool IsExcluded(double value) {
foreach (double excluded in _excluded) {
if (value.Equals(excluded)) { return true; }

Check warning on line 235 in Dummies/ContinuousIntervalSpec.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Do not check floating point equality with exact values, use a range instead.
}

return false;
Expand Down
4 changes: 2 additions & 2 deletions Dummies/DecimalIntervalSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@

#endregion

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

Check warning on line 58 in Dummies/DecimalIntervalSpec.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Constructor has 11 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 @@ -228,7 +228,7 @@
decimal? free = NudgeOnGrid(snapped, true) ?? NudgeOnGrid(snapped, false);
if (free is null) {
throw new AnyGenerationException(
$"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. {source.ReplayHint(current.Seed)}",
$"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. {source.ReplayGuidance(current.Seed)}",
current.Seed,
new InvalidOperationException("The grid nudge could not leave the excluded point within the allowed range."));
}
Expand All @@ -244,7 +244,7 @@
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. {source.ReplayHint(current.Seed)}",
$"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. {source.ReplayGuidance(current.Seed)}",
current.Seed,
new InvalidOperationException("The exclusion nudge could not leave the excluded point within the allowed range."));
}
Expand Down
2 changes: 1 addition & 1 deletion Dummies/PublicAPI/net8.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -492,7 +492,7 @@ static Dummies.Any.UInt32() -> Dummies.AnyUInt32!
static Dummies.Any.UInt64() -> Dummies.AnyUInt64!
static Dummies.Any.Uri() -> Dummies.AnyUri!
static Dummies.Any.UseSeed(int seed) -> System.IDisposable!
static Dummies.Any.UseSeed(int seed, string! replayInstruction) -> System.IDisposable!
static Dummies.Any.UseSeed(int seed, string! replaySnippet) -> System.IDisposable!
static Dummies.Any.WithSeed(int seed) -> Dummies.AnyContext!
static Dummies.AnyExtensions.As<TSource, TResult>(this Dummies.IAny<TSource>! generator, System.Func<TSource, TResult>! factory) -> Dummies.IAny<TResult>!
static Dummies.NullableExtensions.OrNull<T>(this Dummies.IAny<T>! generator) -> Dummies.IAny<T?>!
Expand Down
2 changes: 1 addition & 1 deletion Dummies/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ static Dummies.Any.UInt32() -> Dummies.AnyUInt32!
static Dummies.Any.UInt64() -> Dummies.AnyUInt64!
static Dummies.Any.Uri() -> Dummies.AnyUri!
static Dummies.Any.UseSeed(int seed) -> System.IDisposable!
static Dummies.Any.UseSeed(int seed, string! replayInstruction) -> System.IDisposable!
static Dummies.Any.UseSeed(int seed, string! replaySnippet) -> System.IDisposable!
static Dummies.Any.WithSeed(int seed) -> Dummies.AnyContext!
static Dummies.AnyExtensions.As<TSource, TResult>(this Dummies.IAny<TSource>! generator, System.Func<TSource, TResult>! factory) -> Dummies.IAny<TResult>!
static Dummies.NullableExtensions.OrNull<T>(this Dummies.IAny<T>! generator) -> Dummies.IAny<T?>!
Expand Down
Loading