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
103 changes: 103 additions & 0 deletions Dummies.UnitTests/AnyNullableTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#region Usings declarations

using NFluent;

#endregion

namespace Dummies.UnitTests;

public sealed class AnyNullableTests {

#region Statics members declarations

private const int SampleCount = 200;

// Note: chaining OrNull twice (a nullable of a nullable) is a compile-time error — a Nullable<T> is not a
// struct and not a class, so neither OrNull overload applies. That guard needs no runtime test.

private static string Render(int? value) {
return value?.ToString() ?? "null";
}

#endregion

[Fact(DisplayName = "OrNull on a value type yields both null and non-null; the non-null values honour the inner constraints.")]
public void ValueTypeOrNullYieldsBothCases() {
IAny<int?> generator = Any.Int32().Between(1, 100).OrNull();

int nulls = 0;
int nonNulls = 0;
for (int i = 0; i < SampleCount; i++) {
int? value = generator.Generate();
if (value is null) {
nulls++;
} else {
nonNulls++;
Check.That(value.Value is >= 1 and <= 100).IsTrue();
}
}

Check.That(nulls).IsStrictlyGreaterThan(0);
Check.That(nonNulls).IsStrictlyGreaterThan(0);
}

[Fact(DisplayName = "OrNull on a reference type yields both null and non-null values satisfying the inner constraints.")]
public void ReferenceTypeOrNullYieldsBothCases() {
IAny<string?> generator = Any.String().NonEmpty().OrNull();

bool sawNull = false;
bool sawNonNull = false;
for (int i = 0; i < SampleCount; i++) {
string? value = generator.Generate();
if (value is null) {
sawNull = true;
} else {
sawNonNull = true;
Check.That(value).IsNotEmpty();
}
}

Check.That(sawNull).IsTrue();
Check.That(sawNonNull).IsTrue();
}

[Fact(DisplayName = "OrNull is reproducible: two same-seed contexts replay the same null/value sequence.")]
public void OrNullIsReproducibleUnderASeed() {
IAny<int?> first = Any.WithSeed(123).Int32().OrNull();
IAny<int?> second = Any.WithSeed(123).Int32().OrNull();

string sequenceOne = string.Join("|", Enumerable.Range(0, 30).Select(_ => Render(first.Generate())));
string sequenceTwo = string.Join("|", Enumerable.Range(0, 30).Select(_ => Render(second.Generate())));

Check.That(sequenceTwo).IsEqualTo(sequenceOne);
// The sequence exercises both branches — otherwise the reproducibility guarantee would be vacuous.
Check.That(sequenceOne).Contains("null");
}

[Fact(DisplayName = "OrNull composes with As to produce an optional value object.")]
public void OrNullComposesWithAs() {
IAny<OrderReference?> generator = Any.String().StartingWith("ORD-").WithLength(12).As(OrderReference.Create).OrNull();

bool sawNull = false;
bool sawNonNull = false;
for (int i = 0; i < SampleCount; i++) {
OrderReference? reference = generator.Generate();
if (reference is null) {
sawNull = true;
} else {
sawNonNull = true;
Check.That(reference.Value).StartsWith("ORD-");
}
}

Check.That(sawNull).IsTrue();
Check.That(sawNonNull).IsTrue();
}

[Fact(DisplayName = "OrNull validates its argument on both the value-type and reference-type overloads.")]
public void OrNullValidatesItsArgument() {
Check.ThatCode(() => ((IAny<int>)null!).OrNull()).Throws<ArgumentNullException>();
Check.ThatCode(() => ((IAny<string>)null!).OrNull()).Throws<ArgumentNullException>();
}

}
4 changes: 3 additions & 1 deletion Dummies.UnitTests/SeedReproducibilityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,13 @@ private static string Batch() {
Half tiny = Any.Half();
List<int> list = Any.ListOf(Any.Int32().Between(0, 9)).WithCount(4);
HashSet<int> set = Any.SetOf(Any.Int32().Between(0, 99)).WithCount(3);
int? maybe = Any.Int32().Between(0, 9).OrNull().Generate();

return string.Join("|", full, bounded, free, capped, shaped,
wide, unsigned, real, exact, flag, id, letter,
span.Ticks, instant.Ticks, huge, tiny,
string.Join("-", list), string.Join("-", set.OrderBy(value => value)));
string.Join("-", list), string.Join("-", set.OrderBy(value => value)),
maybe?.ToString() ?? "null");
}

#endregion
Expand Down
80 changes: 80 additions & 0 deletions Dummies/NullableExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace Dummies;

/// <summary>
/// Makes a value-type generator optionally <c>null</c>: <see cref="OrNull{T}" /> turns an
/// <see cref="IAny{T}" /> into an <see cref="IAny{T}" /> of <see cref="Nullable{T}" /> that yields
/// <c>null</c> on an even coin flip and, otherwise, a value satisfying the constraints declared upstream — the
/// dummy for an optional value-type field (<c>int?</c>, <c>DateTime?</c>, <c>Guid?</c>, an enum, ...).
/// </summary>
public static class NullableExtensions {

/// <summary>
/// Derives a generator that yields <c>null</c> about half the time and, otherwise, a value drawn from
/// <paramref name="generator" /> — so a test exercises both the present and the absent case without pinning
/// either. Reproducible under a seed, like every other draw.
/// </summary>
/// <remarks>
/// <para>
/// The null-versus-value decision draws from the same random context as the wrapped generator, so an
/// <c>Any.Reproducibly(...)</c> run replays it exactly. A <c>null</c> draw does not consume a value from
/// the wrapped generator.
/// </para>
/// <example>
/// <code>
/// int? discount = Any.Int32().Between(0, 100).OrNull().Generate();
/// </code>
/// </example>
/// </remarks>
/// <param name="generator">The generator of the non-null values.</param>
/// <typeparam name="T">The underlying value type.</typeparam>
/// <returns>A generator of <see cref="Nullable{T}" />.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="generator" /> is <c>null</c>.</exception>
public static IAny<T?> OrNull<T>(this IAny<T> generator)
where T : struct {
if (generator is null) { throw new ArgumentNullException(nameof(generator)); }

RandomSource? source = AnyDerivation.SourceOf(generator);

return new DerivedAny<T?>(source, () => {
RandomSource working = source ?? AmbientRandomSource.Instance;

return working.Current.Random.Next(2) == 0 ? (T?)null : generator.Generate();
});
}

}

/// <summary>
/// Makes a reference-type generator optionally <c>null</c> — the sibling of
/// <see cref="NullableExtensions.OrNull{T}" /> for the reference-type case (a nullable string, or an optional
/// value object produced through <c>As</c>). It lives in its own class because a single overloaded
/// <c>OrNull</c> constrained once to <c>struct</c> and once to <c>class</c> would collide.
/// </summary>
public static class NullableReferenceExtensions {

/// <summary>
/// Derives a generator that yields <c>null</c> about half the time and, otherwise, a value drawn from
/// <paramref name="generator" /> — the dummy for an optional reference-type field.
/// </summary>
/// <remarks>
/// The null-versus-value decision draws from the same random context as the wrapped generator, so a
/// reproducible run replays it exactly; a <c>null</c> draw does not consume a value from the wrapped generator.
/// </remarks>
/// <param name="generator">The generator of the non-null values.</param>
/// <typeparam name="T">The underlying reference type.</typeparam>
/// <returns>A generator that is sometimes <c>null</c>.</returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="generator" /> is <c>null</c>.</exception>
public static IAny<T?> OrNull<T>(this IAny<T> generator)
where T : class {
if (generator is null) { throw new ArgumentNullException(nameof(generator)); }

RandomSource? source = AnyDerivation.SourceOf(generator);

return new DerivedAny<T?>(source, () => {
RandomSource working = source ?? AmbientRandomSource.Instance;

return working.Current.Random.Next(2) == 0 ? (T?)null : generator.Generate();
});
}

}
3 changes: 3 additions & 0 deletions Dummies/README.nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ matter — and that is the point.
`WithCount`/`NonEmpty`/`Distinct`/`Containing`. A distinct collection asked for more
elements than its element domain can produce fails fast, just like any other
conflict; `Any.PairOf`/`TripleOf` pair generators into value tuples.
- **Optional values**: `.OrNull()` turns any generator into one that is `null` about
half the time and otherwise a constrained value — the dummy for an optional field,
for value types (`int?`, `Guid?`, ...) and reference types alike.
- **Reproducible runs**: wrap a test in `Any.Reproducibly(...)` and a failing run
reports the seed to replay; `Any.WithSeed(seed)` gives an isolated, deterministic
context.
Expand Down