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
141 changes: 141 additions & 0 deletions Dummies.UnitTests/AnyCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,147 @@ public void ContainingFromAGenerator() {
}
}

[Fact(DisplayName = "Containing: a fixed value outside the element domain extends the effective cardinality (issue #188).")]
public void ContainingOutsideDomainExtendsCardinality() {
// The motivating case: {1, 2, 3} is satisfiable — 3 is supplied directly and lies outside the {1, 2} the
// generator can produce, so only two elements must be drawn from it.
for (int i = 0; i < SampleCount; i++) {
HashSet<int> set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Generate();
Check.That(set).Contains(1, 2, 3);
Check.That(set.Count).IsEqualTo(3);
}

// The same reasoning holds for a distinct list and for several out-of-domain values at once. Dictionary keys
// run through the very same CollectionState path, so the correction reaches them too — but AnyDictionary has
// no Containing surface, so its keys are gated purely by count (see DictionaryOfBehaves) and cannot exercise
// the out-of-domain case directly.
for (int i = 0; i < SampleCount; i++) {
HashSet<int> list = new(Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Distinct().Generate());
Check.That(list).Contains(1, 2, 3);

HashSet<int> quad = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(3).Containing(4).WithCount(4).Generate();
Check.That(quad).Contains(1, 2, 3, 4);
Check.That(quad.Count).IsEqualTo(4);
}
}

[Fact(DisplayName = "Containing: a value already inside the element domain does not inflate the cardinality, so an impossible count still conflicts.")]
public void ContainingInsideDomainDoesNotInflate() {
// 1 is already producible by the generator, so it adds no capacity: three distinct values over {1, 2} remain
// impossible and must still fail eagerly, naming the shortfall.
ConflictingAnyConstraintException conflict = Assert.Throws<ConflictingAnyConstraintException>(
() => Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).WithCount(3));
Check.That(conflict.Message).Contains("2 distinct value");

// Mixed: 1 is inside the domain, 5 is outside — effective capacity is 2 + 1 = 3. Four is over the top; three
// is exactly reachable as {1, 2, 5}.
Check.ThatCode(() => Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).Containing(5).WithCount(4)).Throws<ConflictingAnyConstraintException>();
for (int i = 0; i < SampleCount; i++) {
HashSet<int> set = Any.SetOf(Any.Int32().OneOf(1, 2)).Containing(1).Containing(5).WithCount(3).Generate();
Check.That(set).Contains(1, 2, 5);
}
}

[Fact(DisplayName = "Containing: the effective cardinality is order-independent across Distinct, Containing and the count.")]
public void EffectiveCardinalityIsOrderIndependent() {
// Every ordering of the same three constraints reaches the same verdict — accepted, because 3 is outside the
// domain — since Distinct() re-runs the whole validation on the accumulated state.
for (int i = 0; i < SampleCount; i++) {
Check.That(new HashSet<int>(Any.ListOf(Any.Int32().OneOf(1, 2)).WithCount(3).Containing(3).Distinct().Generate())).Contains(1, 2, 3);
Check.That(new HashSet<int>(Any.ListOf(Any.Int32().OneOf(1, 2)).Distinct().Containing(3).WithCount(3).Generate())).Contains(1, 2, 3);
Check.That(new HashSet<int>(Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(3).WithCount(3).Distinct().Generate())).Contains(1, 2, 3);
}

// And rejected whatever the order, because the contained value is inside the domain.
Check.ThatCode(() => Any.ListOf(Any.Int32().OneOf(1, 2)).WithCount(3).Containing(1).Distinct()).Throws<ConflictingAnyConstraintException>();
Check.ThatCode(() => Any.ListOf(Any.Int32().OneOf(1, 2)).Distinct().Containing(1).WithCount(3)).Throws<ConflictingAnyConstraintException>();
Check.ThatCode(() => Any.ListOf(Any.Int32().OneOf(1, 2)).Containing(1).WithCount(3).Distinct()).Throws<ConflictingAnyConstraintException>();
}

[Fact(DisplayName = "Containing: a near-maximum element cardinality plus an out-of-domain value does not overflow into a false conflict.")]
public void ContainingNearMaximumCardinalityDoesNotOverflow() {
// Between(0, long.MaxValue - 1) advertises long.MaxValue distinct values; the additive form base + extras
// would overflow to a negative and reject spuriously. The subtractive check stays correct.
for (int i = 0; i < SampleCount; i++) {
HashSet<long> set = Any.SetOf(Any.Int64().Between(0, long.MaxValue - 1)).Containing(-1L).WithCount(3).Generate();
Check.That(set).Contains(-1L);
Check.That(set.Count).IsEqualTo(3);
}
}

[Fact(DisplayName = "ContainingAny stays conservative: no eager false conflict, and an opaque shortfall surfaces at generation.")]
public void ContainingAnyDefersToGeneration() {
// The generator drawn from can yield a value outside the element domain, so the request cannot be proven
// impossible at declaration — a wide ContainingAny makes it genuinely satisfiable.
for (int i = 0; i < SampleCount; i++) {
HashSet<int> set = Any.SetOf(Any.Int32().OneOf(1, 2)).ContainingAny(Any.Int32().GreaterThan(100)).WithCount(3).Generate();
Check.That(set.Count).IsEqualTo(3);
Check.That(set).Contains(1, 2);
}

// When every source draws from the same two-value domain, three distinct values are impossible — but the
// overlap is opaque, so it is caught while drawing (a replayable AnyGenerationException) rather than as a
// false eager conflict.
Check.ThatCode(() => Any.SetOf(Any.Bool()).ContainingAny(Any.Bool()).ContainingAny(Any.Bool()).ContainingAny(Any.Bool()).Generate())
.Throws<AnyGenerationException>();
}

[Fact(DisplayName = "Containing under a merging comparer: an out-of-domain value is credited, and a comparer that merges it back is caught at generation.")]
public void ContainingUnderAMergingComparer() {
IEqualityComparer<int> modTen = new ModuloComparer(10);

// 15 is outside {1, 2, 3} and its residue class (5) is fresh too, so {1, 2, 3, 15} has four classes and
// generation succeeds.
for (int i = 0; i < SampleCount; i++) {
HashSet<int> set = Any.SetOf(Any.Int32().OneOf(1, 2, 3), modTen).Containing(15).WithCount(4).Generate();
Check.That(set.Count).IsEqualTo(4);
}

// 12 is outside {1, 2} by value, so it is still credited and the request is accepted eagerly — but 12 ≡ 2
// (mod 10) collapses it back into the domain, so three distinct-under-the-comparer values are impossible and
// the shortfall surfaces while drawing, never as a false eager conflict.
Check.ThatCode(() => Any.SetOf(Any.Int32().OneOf(1, 2), modTen).Containing(12).WithCount(3).Generate()).Throws<AnyGenerationException>();
}

[Fact(DisplayName = "The eager perimeter reaches every finite generator: decimal, floating-point and 128-bit allow-lists gate distinct collections too.")]
public void FiniteScalarGeneratorsGateEagerly() {
// A finite allow-list or a narrow range over decimal, double, single or Int128 now advertises its cardinality,
// so a count beyond it conflicts at declaration — the same promise integers and enums already kept, held
// across the whole knowable perimeter rather than only part of it.
Check.ThatCode(() => Any.SetOf(Any.Decimal().OneOf(1m, 2m)).WithCount(3)).Throws<ConflictingAnyConstraintException>();
Check.ThatCode(() => Any.SetOf(Any.Double().OneOf(1d, 2d)).WithCount(3)).Throws<ConflictingAnyConstraintException>();
Check.ThatCode(() => Any.SetOf(Any.Single().OneOf(1f, 2f)).WithCount(3)).Throws<ConflictingAnyConstraintException>();
Check.ThatCode(() => Any.SetOf(Any.Int128().Between(1, 3)).WithCount(5)).Throws<ConflictingAnyConstraintException>();

// Membership travels with cardinality: an out-of-domain contained value extends the effective domain...
for (int i = 0; i < SampleCount; i++) {
HashSet<decimal> set = Any.SetOf(Any.Decimal().OneOf(1m, 2m)).Containing(3m).WithCount(3).Generate();
Check.That(set).Contains(1m, 2m, 3m);
}

// ...while a contained value already inside it does not, so an impossible count still conflicts eagerly.
Check.ThatCode(() => Any.SetOf(Any.Decimal().OneOf(1m, 2m)).Containing(1m).WithCount(3)).Throws<ConflictingAnyConstraintException>();
Check.ThatCode(() => Any.SetOf(Any.Double().OneOf(1d, 2d)).Containing(2d).WithCount(3)).Throws<ConflictingAnyConstraintException>();
}

[Fact(DisplayName = "A validated pin is a singleton domain: a distinct collection asking for more than one conflicts eagerly.")]
public void SingletonScalarDomainsGateEagerly() {
// Zero()/Between(x, x) pins the domain to a single value; asking a distinct collection for two is a fully
// knowable contradiction, so it must fail at declaration, not only while drawing.
Check.ThatCode(() => Any.SetOf(Any.Decimal().Zero()).WithCount(2)).Throws<ConflictingAnyConstraintException>();
Check.ThatCode(() => Any.SetOf(Any.Double().Between(1d, 1d)).WithCount(2)).Throws<ConflictingAnyConstraintException>();
Check.ThatCode(() => Any.SetOf(Any.Single().Zero()).WithCount(2)).Throws<ConflictingAnyConstraintException>();

// The singleton still generates at count one, and an out-of-domain contained value extends it as usual.
for (int i = 0; i < SampleCount; i++) {
HashSet<decimal> one = Any.SetOf(Any.Decimal().Zero()).WithCount(1).Generate();
Check.That(one).ContainsExactly(0m);

HashSet<decimal> two = Any.SetOf(Any.Decimal().Zero()).Containing(5m).WithCount(2).Generate();
Check.That(two).Contains(0m, 5m);
}
}

[Fact(DisplayName = "ArrayOf: produces an array of the requested size, distinct when asked.")]
public void ArrayOfProducesArrays() {
for (int i = 0; i < SampleCount; i++) {
Expand Down
7 changes: 5 additions & 2 deletions Dummies/AnyBool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Dummies;
/// mostly useful for symmetry when a test sweeps cases — and contradictory pins fail eagerly with a
/// <see cref="ConflictingAnyConstraintException" /> naming both sides, like every other generator.
/// </summary>
public sealed class AnyBool : IAny<bool>, IHasRandomSource, ICardinalityHint {
public sealed class AnyBool : IAny<bool>, IHasRandomSource, ICardinalityHint<bool> {

#region Statics members declarations

Expand Down Expand Up @@ -36,7 +36,10 @@ private AnyBool(RandomSource source, bool? pinned, string? pinnedConstraint) {
RandomSource? IHasRandomSource.Source => _source;

// Two distinct values unless a pin has already fixed one of them.
long? ICardinalityHint.DistinctCardinality => _pinned is null ? 2 : 1;
long? ICardinalityHint<bool>.DistinctCardinality => _pinned is null ? 2 : 1;

// A pin narrows the domain to that single value; unpinned, both booleans are producible.
bool ICardinalityHint<bool>.Contains(bool value) => _pinned is not bool pinned || pinned == value;

/// <summary>Pins the value to <c>true</c>.</summary>
/// <returns>A new generator carrying the added constraint.</returns>
Expand Down
6 changes: 4 additions & 2 deletions Dummies/AnyByte.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Dummies;
/// contradictory constraints fail eagerly with a <see cref="ConflictingAnyConstraintException" /> naming both
/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw.
/// </summary>
public sealed class AnyByte : IAny<byte>, IHasRandomSource, ICardinalityHint {
public sealed class AnyByte : IAny<byte>, IHasRandomSource, ICardinalityHint<byte> {

#region Statics members declarations

Expand Down Expand Up @@ -52,7 +52,9 @@ private AnyByte(RandomSource source, OrdinalIntervalSpec spec) {

RandomSource? IHasRandomSource.Source => _source;

long? ICardinalityHint.DistinctCardinality => _spec.Cardinality;
long? ICardinalityHint<byte>.DistinctCardinality => _spec.Cardinality;

bool ICardinalityHint<byte>.Contains(byte value) => _spec.Contains(Ord(value));

/// <summary>Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases.</summary>
/// <returns>A new generator carrying the added constraint.</returns>
Expand Down
7 changes: 5 additions & 2 deletions Dummies/AnyChar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
/// <see cref="DifferentFrom" />. A combination that empties the pool fails eagerly with a
/// <see cref="ConflictingAnyConstraintException" />.
/// </summary>
public sealed class AnyChar : IAny<char>, IHasRandomSource, ICardinalityHint {
public sealed class AnyChar : IAny<char>, IHasRandomSource, ICardinalityHint<char> {

#region Statics members declarations

Expand Down Expand Up @@ -40,7 +40,7 @@

#endregion

private AnyChar(RandomSource source,

Check warning on line 43 in Dummies/AnyChar.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Constructor has 8 parameters, which is greater than the 7 authorized.
CharacterSet? charset, string? charsetConstraint,
LetterCasing? casing, string? casingConstraint,
IReadOnlyList<char>? allowed, string? allowedConstraint,
Expand All @@ -63,7 +63,10 @@
RandomSource? IHasRandomSource.Source => _source;

// The pool is materialized once at construction, so its size is the exact number of characters drawable.
long? ICardinalityHint.DistinctCardinality => _pool.Count;
long? ICardinalityHint<char>.DistinctCardinality => _pool.Count;

// The pool is the exact draw set, so membership is a direct pool lookup.
bool ICardinalityHint<char>.Contains(char value) => _pool.Contains(value);

/// <summary>Restricts the character to ASCII letters only. Declared once per generator.</summary>
/// <returns>A new generator carrying the added constraint.</returns>
Expand Down Expand Up @@ -164,7 +167,7 @@
return Validated(new AnyChar(_source, _charset, _charsetConstraint, _casing, _casingConstraint, _allowed, _allowedConstraint, excluded), applying);
}

private AnyChar Validated(AnyChar candidate, string applying) {

Check warning on line 170 in Dummies/AnyChar.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Make 'Validated' a static method.
if (candidate._pool.Count > 0) { return candidate; }

string pool = candidate._allowedConstraint is null
Expand Down
6 changes: 4 additions & 2 deletions Dummies/AnyDateOnly.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace Dummies;
/// constraint: a reproducible test pins its reference dates explicitly with <see cref="After" /> and
/// <see cref="Before" />.
/// </summary>
public sealed class AnyDateOnly : IAny<DateOnly>, IHasRandomSource, ICardinalityHint {
public sealed class AnyDateOnly : IAny<DateOnly>, IHasRandomSource, ICardinalityHint<DateOnly> {

#region Statics members declarations

Expand Down Expand Up @@ -56,7 +56,9 @@ private AnyDateOnly(RandomSource source, OrdinalIntervalSpec spec) {

RandomSource? IHasRandomSource.Source => _source;

long? ICardinalityHint.DistinctCardinality => _spec.Cardinality;
long? ICardinalityHint<DateOnly>.DistinctCardinality => _spec.Cardinality;

bool ICardinalityHint<DateOnly>.Contains(DateOnly value) => _spec.Contains(Ord(value));

/// <summary>Requires a date strictly after <paramref name="date" />.</summary>
/// <param name="date">The exclusive lower bound.</param>
Expand Down
6 changes: 4 additions & 2 deletions Dummies/AnyDateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/// comparison operators do. Values supplied to <see cref="OneOf" /> are returned as given, Kind included. There is deliberately no clock-relative constraint (no "in the past/future"): a
/// reproducible test pins its reference instants explicitly with <see cref="After" /> and <see cref="Before" />.
/// </remarks>
public sealed class AnyDateTime : IAny<DateTime>, IHasRandomSource, ICardinalityHint {
public sealed class AnyDateTime : IAny<DateTime>, IHasRandomSource, ICardinalityHint<DateTime> {

#region Statics members declarations

Expand Down Expand Up @@ -61,7 +61,9 @@

RandomSource? IHasRandomSource.Source => _source;

long? ICardinalityHint.DistinctCardinality => _spec.Cardinality;
long? ICardinalityHint<DateTime>.DistinctCardinality => _spec.Cardinality;

bool ICardinalityHint<DateTime>.Contains(DateTime value) => _spec.Contains(Ord(value));

/// <summary>Requires an instant strictly after <paramref name="instant" />.</summary>
/// <param name="instant">The exclusive lower bound.</param>
Expand Down Expand Up @@ -122,7 +124,7 @@
// Remember the supplied values by instant, so generation returns them as given: the ordinal space
// only carries the ticks, and rebuilding from it would silently normalize the Kind to Utc.
Dictionary<ulong, DateTime> originals = new();
foreach (DateTime value in values) {

Check warning on line 127 in Dummies/AnyDateTime.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Loops should be simplified using the "Where" LINQ method
if (!originals.ContainsKey(Ord(value))) { originals.Add(Ord(value), value); }
}

Expand Down
6 changes: 4 additions & 2 deletions Dummies/AnyDateTimeOffset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
/// past/future"): a reproducible test pins its reference instants explicitly with <see cref="After" /> and
/// <see cref="Before" />.
/// </remarks>
public sealed class AnyDateTimeOffset : IAny<DateTimeOffset>, IHasRandomSource, ICardinalityHint {
public sealed class AnyDateTimeOffset : IAny<DateTimeOffset>, IHasRandomSource, ICardinalityHint<DateTimeOffset> {

#region Statics members declarations

Expand Down Expand Up @@ -63,7 +63,9 @@

RandomSource? IHasRandomSource.Source => _source;

long? ICardinalityHint.DistinctCardinality => _spec.Cardinality;
long? ICardinalityHint<DateTimeOffset>.DistinctCardinality => _spec.Cardinality;

bool ICardinalityHint<DateTimeOffset>.Contains(DateTimeOffset value) => _spec.Contains(Ord(value));

/// <summary>Requires an instant strictly after <paramref name="instant" />.</summary>
/// <param name="instant">The exclusive lower bound.</param>
Expand Down Expand Up @@ -127,7 +129,7 @@
// Remember the supplied values by instant, so generation returns them as given: the ordinal space
// only carries the instant, and rebuilding from it would silently normalize the offset to UTC.
Dictionary<ulong, DateTimeOffset> originals = new();
foreach (DateTimeOffset value in values) {

Check warning on line 132 in Dummies/AnyDateTimeOffset.cs

View workflow job for this annotation

GitHub Actions / SonarQube Cloud analysis

Loops should be simplified using the "Where" LINQ method
if (!originals.ContainsKey(Ord(value))) { originals.Add(Ord(value), value); }
}

Expand Down
6 changes: 5 additions & 1 deletion Dummies/AnyDecimal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace Dummies;
/// sides; instances are immutable recipes. Exclusive bounds are expressed as the inclusive bound plus a point
/// exclusion, since <see cref="decimal" /> has no next-representable-value ladder.
/// </summary>
public sealed class AnyDecimal : IAny<decimal>, IHasRandomSource {
public sealed class AnyDecimal : IAny<decimal>, IHasRandomSource, ICardinalityHint<decimal> {

#region Statics members declarations

Expand Down Expand Up @@ -45,6 +45,10 @@ private AnyDecimal(RandomSource source, DecimalIntervalSpec spec) {

RandomSource? IHasRandomSource.Source => _source;

long? ICardinalityHint<decimal>.DistinctCardinality => _spec.Cardinality;

bool ICardinalityHint<decimal>.Contains(decimal value) => _spec.Contains(value);

/// <summary>Requires a value strictly greater than zero.</summary>
/// <returns>A new generator carrying the added constraint.</returns>
/// <exception cref="ConflictingAnyConstraintException">Thrown when the constraint contradicts a constraint already declared.</exception>
Expand Down
4 changes: 2 additions & 2 deletions Dummies/AnyDerivation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ internal static class AnyDerivation {

/// <summary>
/// A conservative upper bound on the number of distinct values <paramref name="generator" /> yields, when it
/// advertises one through <see cref="ICardinalityHint" />; <c>null</c> when the domain is unbounded or unknown.
/// advertises one through <see cref="ICardinalityHint{T}" />; <c>null</c> when the domain is unbounded or unknown.
/// </summary>
internal static long? CardinalityOf<T>(IAny<T> generator) {
return (generator as ICardinalityHint)?.DistinctCardinality;
return (generator as ICardinalityHint<T>)?.DistinctCardinality;
}

/// <summary>
Expand Down
6 changes: 5 additions & 1 deletion Dummies/AnyDouble.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Dummies;
/// contradictory constraints fail eagerly with a <see cref="ConflictingAnyConstraintException" /> naming both
/// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted.
/// </summary>
public sealed class AnyDouble : IAny<double>, IHasRandomSource {
public sealed class AnyDouble : IAny<double>, IHasRandomSource, ICardinalityHint<double> {

#region Statics members declarations

Expand Down Expand Up @@ -44,6 +44,10 @@ private AnyDouble(RandomSource source, ContinuousIntervalSpec spec) {

RandomSource? IHasRandomSource.Source => _source;

long? ICardinalityHint<double>.DistinctCardinality => _spec.Cardinality;

bool ICardinalityHint<double>.Contains(double value) => _spec.Contains(value);

/// <summary>Requires a value strictly greater than zero.</summary>
/// <returns>A new generator carrying the added constraint.</returns>
/// <exception cref="ConflictingAnyConstraintException">Thrown when the constraint contradicts a constraint already declared.</exception>
Expand Down
Loading