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
128 changes: 128 additions & 0 deletions JustDummies.PropertyTests/EnumCombinationProperties.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#region Usings declarations

using FsCheck;
using FsCheck.Fluent;

using JetBrains.Annotations;

#endregion

namespace JustDummies.PropertyTests;

/// <summary>
/// Property-based tests for <see cref="AnyEnum{TEnum}.AllowingCombinations" />. Where the example-based suite pins
/// the universe a handful of named enum shapes yield, these quantify over the <b>constraints</b> applied on top of
/// it: the allow-list and the exclusion set are drawn from the universe itself, so a draw escaping the universe, an
/// exclusion silently read as a bit mask, or a pool that empties without conflicting is found and shrunk.
/// </summary>
/// <remarks>
/// The universe is fixed per enum type, so it is the constraint sets — not the type — that carry the input space.
/// <see cref="Permissions" /> is used throughout because its four declared members give a universe of eight values:
/// small enough to enumerate in the assertion, wide enough that a subset drawn from it is rarely trivial.
/// </remarks>
[TestSubject(typeof(AnyEnum<>))]
public sealed class EnumCombinationProperties {

#region Statics members declarations

/// <summary>Every value <c>AllowingCombinations()</c> must be able to draw for <see cref="Permissions" />.</summary>
private static readonly Permissions[] Universe = Enumerable.Range(0, 8).Select(bits => (Permissions)bits).ToArray();

/// <summary>
/// A non-empty subset of the universe, in an arbitrary order and possibly with repetitions — an allow-list or
/// an exclusion set as a caller would write it. Repetitions are kept on purpose: <c>Except</c> and
/// <c>OneOf</c> both have to absorb a duplicate without changing the pool they compute.
/// </summary>
private static Gen<Permissions[]> Subsets() {
return Gen.NonEmptyListOf(Gen.Elements(Universe)).Select(values => values.ToArray());
}

#endregion

[Fact(DisplayName = "AllowingCombinations: every draw is a combination of declared members, for every exclusion set.")]
public void EveryDrawStaysInTheUniverse() {
Prop.ForAll(Subsets().ToArbitrary(),
excluded => {
// A subset drawn with repetition can cover the whole universe; only then is a conflict owed,
// so the property branches on the drawn values rather than on the call shape.
Permissions[] distinct = excluded.Distinct().ToArray();
if (distinct.Length == Universe.Length) {
return Expect.Throws<ConflictingAnyConstraintException>(
() => Any.Enum<Permissions>().AllowingCombinations().Except(excluded));
}

return Expect.EveryDraw(Any.Enum<Permissions>().AllowingCombinations().Except(excluded),
value => Universe.Contains(value) && !distinct.Contains(value));
})
.QuickCheckThrowOnFailure();
}

[Fact(DisplayName = "AllowingCombinations: exclusions compare by equality, never as a bit mask.")]
public void ExclusionsCompareByEquality() {
Prop.ForAll(Gen.Elements(Universe.Where(value => value != 0).ToArray()).ToArbitrary(),
excluded => {
// Every strict superset of the excluded value's bits is a DIFFERENT value, so it must remain
// reachable: reading Except as "no value carrying these bits" would make all of them vanish.
Permissions[] survivors = Universe.Where(value => value != excluded && (value & excluded) == excluded).ToArray();
if (survivors.Length == 0) { return true; }

List<Permissions> draws = Expect.Draws(Any.Enum<Permissions>().AllowingCombinations().Except(excluded), 200);

return draws.All(value => value != excluded) && survivors.Any(draws.Contains);
})
.QuickCheckThrowOnFailure();
}

[Fact(DisplayName = "AllowingCombinations: an allow-list of combinations is honoured exactly, for every subset.")]
public void AnAllowListOfCombinationsIsHonoured() {
Prop.ForAll(Subsets().ToArbitrary(),
allowed => {
Permissions[] distinct = allowed.Distinct().ToArray();

return Expect.EveryDraw(Any.Enum<Permissions>().AllowingCombinations().OneOf(allowed),
distinct.Contains);
})
.QuickCheckThrowOnFailure();
}

[Fact(DisplayName = "AllowingCombinations: the declared-members default is untouched, for every exclusion set.")]
public void TheDefaultRemainsDeclaredMembersOnly() {
Permissions[] declared = [Permissions.None, Permissions.Read, Permissions.Write, Permissions.Exec];

Prop.ForAll(Gen.Choose(0, 2).ToArbitrary(),
size => {
Permissions[] excluded = declared.Take(size).ToArray();
AnyEnum<Permissions> generator = excluded.Length == 0
? Any.Enum<Permissions>()
: Any.Enum<Permissions>().Except(excluded);

// Without the opt-in no combination is ever drawn, whatever else was declared — the contract
// AllowingCombinations() exists precisely to leave alone.
return Expect.EveryDraw(generator, value => declared.Contains(value) && !excluded.Contains(value));
})
.QuickCheckThrowOnFailure();
}

[Fact(DisplayName = "AllowingCombinations: two contexts on the same seed draw the same combinations, for every seed.")]
public void CombinationsAreReproducibleForEverySeed() {
Prop.ForAll(Generators.Seed().ToArbitrary(),
seed => {
List<Permissions> first = Expect.Draws(Any.WithSeed(seed).Enum<Permissions>().AllowingCombinations(), 12);
List<Permissions> second = Expect.Draws(Any.WithSeed(seed).Enum<Permissions>().AllowingCombinations(), 12);

return first.SequenceEqual(second);
})
.QuickCheckThrowOnFailure();
}

[Flags]
private enum Permissions {

None = 0,
Read = 1,
Write = 2,
Exec = 4

}

}
15 changes: 15 additions & 0 deletions JustDummies.UnitTests/AnyDateTimeOffsetOffsetTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,21 @@ public void WithOffsetArguments() {
Check.ThatCode(() => Any.DateTimeOffset().WithOffsetBetween(TimeSpan.FromHours(2), TimeSpan.FromHours(-2))).Throws<ArgumentException>();
}

[Fact(DisplayName = "WithOffset: OneOf returns its values verbatim, offset included, in either order.")]
public void OneOfKeepsItsOwnOffset() {
// The accepted risk recorded in ADR-0037: OneOf is a terminal enumeration of exact values, so the offset
// dimension governs only the CONSTRUCTED draw and never rewrites a supplied value's own offset. Pinned here so
// the divergence stays a decision rather than becoming an unnoticed regression in either direction.
DateTimeOffset pinned = new(2020, 1, 1, 0, 0, 0, TimeSpan.Zero);
TimeSpan requested = TimeSpan.FromHours(5);

for (int i = 0; i < SampleCount; i++) {
Check.That(Any.DateTimeOffset().WithOffset(requested).OneOf(pinned).Generate()).IsEqualTo(pinned);
Check.That(Any.DateTimeOffset().WithOffset(requested).OneOf(pinned).Generate().Offset).IsEqualTo(pinned.Offset);
Check.That(Any.DateTimeOffset().OneOf(pinned).WithOffset(requested).Generate().Offset).IsEqualTo(pinned.Offset);
}
}

[Fact(DisplayName = "WithOffset: a second, different offset is rejected as already declared.")]
public void WithOffsetDeclaredOnce() {
Check.ThatCode(() => Any.DateTimeOffset().WithOffset(TimeSpan.FromHours(2)).WithOffset(TimeSpan.FromHours(3)))
Expand Down
207 changes: 207 additions & 0 deletions JustDummies.UnitTests/AnyEnumCombinationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
#region Usings declarations

using NFluent;

#endregion

namespace JustDummies.UnitTests;

/// <summary>
/// The named cases of <see cref="AnyEnum{TEnum}.AllowingCombinations" />: the exact universe a given enum shape
/// yields, the conflict wording, and the boundary between the declared-members default and the opt-in. The
/// universal half — that every draw belongs to the universe whatever the constraints — lives in
/// <c>JustDummies.PropertyTests</c>.
/// </summary>
public sealed class AnyEnumCombinationTests {

// Enough draws that an eight-value universe is exhausted with overwhelming probability, while a missing value is
// not attributed to bad luck. The assertions below are on the SET observed, so they are reachability claims.
private const int SampleCount = 2000;

[Flags]
private enum Permissions {

None = 0,
Read = 1,
Write = 2,
Exec = 4

}

// No zero member: the empty combination is not a value this enum defines.
[Flags]
private enum Sides {

Left = 1,
Right = 2

}

// A declared composite: ReadWrite is already Read | Write, so it must not widen the universe.
[Flags]
private enum Access {

Read = 1,
Write = 2,
ReadWrite = 3

}

private enum OrderStatus {

Draft,
Validated,
Cancelled

}

[Fact(DisplayName = "A [Flags] enum still draws only declared members until combinations are allowed.")]
public void FlagsEnumDrawsDeclaredMembersByDefault() {
HashSet<Permissions> seen = new();
for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum<Permissions>().Generate()); }

// The contract the opt-in exists to leave untouched: the default never depends on the [Flags] attribute, so a
// combination is unreachable until the test asks for one.
Check.That(seen).IsOnlyMadeOf(Permissions.None, Permissions.Read, Permissions.Write, Permissions.Exec);
}

[Fact(DisplayName = "AllowingCombinations: the universe is every combination, and the declared zero value.")]
public void CombinationsCoverTheWholeUniverse() {
HashSet<Permissions> seen = new();
for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum<Permissions>().AllowingCombinations().Generate()); }

Check.That(seen.Count).IsEqualTo(8);
for (int bits = 0; bits <= 7; bits++) { Check.That(seen).Contains((Permissions)bits); }
}

[Fact(DisplayName = "AllowingCombinations: an enum declaring no zero member never yields the empty combination.")]
public void CombinationsOmitZeroWhenItIsNotDeclared() {
HashSet<Sides> seen = new();
for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum<Sides>().AllowingCombinations().Generate()); }

Check.That(seen).IsOnlyMadeOf(Sides.Left, Sides.Right, Sides.Left | Sides.Right);
}

[Fact(DisplayName = "AllowingCombinations: a declared composite adds nothing — it already is a combination.")]
public void DeclaredCompositeDoesNotWidenTheUniverse() {
HashSet<Access> seen = new();
for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum<Access>().AllowingCombinations().Generate()); }

Check.That(seen).IsOnlyMadeOf(Access.Read, Access.Write, Access.ReadWrite);
}

[Fact(DisplayName = "AllowingCombinations: applying it to an enum that is not [Flags] conflicts, naming why.")]
public void CombinationsRequireAFlagsEnum() {
ConflictingAnyConstraintException conflict = Assert.Throws<ConflictingAnyConstraintException>(
() => Any.Enum<OrderStatus>().AllowingCombinations());

Check.That(conflict.Message).Contains("AllowingCombinations()");
Check.That(conflict.Message).Contains("OrderStatus");
Check.That(conflict.Message).Contains("[Flags]");
}

[Fact(DisplayName = "AllowingCombinations: applying it twice is a no-op, not a conflict.")]
public void CombinationsAreIdempotent() {
AnyEnum<Permissions> generator = Any.Enum<Permissions>().AllowingCombinations().AllowingCombinations();

HashSet<Permissions> seen = new();
for (int i = 0; i < SampleCount; i++) { seen.Add(generator.Generate()); }

// Idempotent, not cumulative: the universe is the same eight values a single application yields.
Check.That(seen.Count).IsEqualTo(8);
}

[Fact(DisplayName = "OneOf: a combination is refused before the opt-in, and the message names the missing one.")]
public void OneOfRefusesACombinationBeforeTheOptIn() {
ArgumentException error = Assert.Throws<ArgumentException>(
() => Any.Enum<Permissions>().OneOf(Permissions.Read | Permissions.Write));

Check.That(error.Message).Contains("AllowingCombinations()");
}

[Fact(DisplayName = "OneOf: a combination is accepted once combinations are allowed.")]
public void OneOfAcceptsACombinationAfterTheOptIn() {
AnyEnum<Permissions> generator = Any.Enum<Permissions>()
.AllowingCombinations()
.OneOf(Permissions.Read | Permissions.Write, Permissions.Exec);

HashSet<Permissions> seen = new();
for (int i = 0; i < SampleCount; i++) { seen.Add(generator.Generate()); }

Check.That(seen).IsOnlyMadeOf(Permissions.Read | Permissions.Write, Permissions.Exec);
}

[Fact(DisplayName = "Except: exclusions compare by equality, so a combination carrying an excluded bit survives.")]
public void ExclusionsCompareByEquality() {
AnyEnum<Permissions> generator = Any.Enum<Permissions>().AllowingCombinations().Except(Permissions.Read);

HashSet<Permissions> seen = new();
for (int i = 0; i < SampleCount; i++) { seen.Add(generator.Generate()); }

// Read itself is gone; Read | Write is a different value and stays drawable — Except is not a bit mask.
Check.That(seen).Not.Contains(Permissions.Read);
Check.That(seen).Contains(Permissions.Read | Permissions.Write);
Check.That(seen.Count).IsEqualTo(7);
}

[Fact(DisplayName = "AllowingCombinations: the widened universe feeds the distinct-collection cardinality check.")]
public void CombinationsWidenTheCardinalityHint() {
// Eight distinct values exist, so eight are obtainable and nine conflict eagerly — the same check that caps a
// declared-members draw at four.
HashSet<Permissions> eight = Any.SetOf(Any.Enum<Permissions>().AllowingCombinations()).WithCount(8).Generate();
Check.That(eight.Count).IsEqualTo(8);

ConflictingAnyConstraintException conflict = Assert.Throws<ConflictingAnyConstraintException>(
() => Any.SetOf(Any.Enum<Permissions>().AllowingCombinations()).WithCount(9).Generate());
Check.That(conflict.Message).Contains("9");

ConflictingAnyConstraintException capped = Assert.Throws<ConflictingAnyConstraintException>(
() => Any.SetOf(Any.Enum<Permissions>()).WithCount(5).Generate());
Check.That(capped.Message).Contains("5");
}

[Fact(DisplayName = "AllowingCombinations: excluding the whole universe conflicts, naming both sides.")]
public void ExcludingTheWholeUniverseConflicts() {
Permissions[] everything = Enumerable.Range(0, 8).Select(bits => (Permissions)bits).ToArray();

ConflictingAnyConstraintException conflict = Assert.Throws<ConflictingAnyConstraintException>(
() => Any.Enum<Permissions>().AllowingCombinations().Except(everything));

Check.That(conflict.Message).Contains("Except(");
Check.That(conflict.Message).Contains("Permissions");
}

[Fact(DisplayName = "AllowingCombinations: an enum with too many members to enumerate is refused, naming the ceiling.")]
public void TooManyMembersIsRefused() {
ConflictingAnyConstraintException conflict = Assert.Throws<ConflictingAnyConstraintException>(
() => Any.Enum<Wide>().AllowingCombinations());

Check.That(conflict.Message).Contains("AllowingCombinations()");
Check.That(conflict.Message).Contains("21");
Check.That(conflict.Message).Contains("20");
Check.That(conflict.Message).Contains("OneOf");
}

[Fact(DisplayName = "AllowingCombinations: a seeded context replays the same combinations.")]
public void CombinationsReplayUnderASeed() {
List<Permissions> Batch(int seed) {
AnyContext context = Any.WithSeed(seed);
AnyEnum<Permissions> generator = context.Enum<Permissions>().AllowingCombinations();

return Enumerable.Range(0, 20).Select(_ => generator.Generate()).ToList();
}

Check.That(Batch(4242)).ContainsExactly(Batch(4242));
}

// Twenty-one single-bit members: one past the ceiling AllowingCombinations() will enumerate.
[Flags]
private enum Wide {

B00 = 1 << 0, B01 = 1 << 1, B02 = 1 << 2, B03 = 1 << 3, B04 = 1 << 4, B05 = 1 << 5, B06 = 1 << 6,
B07 = 1 << 7, B08 = 1 << 8, B09 = 1 << 9, B10 = 1 << 10, B11 = 1 << 11, B12 = 1 << 12, B13 = 1 << 13,
B14 = 1 << 14, B15 = 1 << 15, B16 = 1 << 16, B17 = 1 << 17, B18 = 1 << 18, B19 = 1 << 19, B20 = 1 << 20

}

}
4 changes: 3 additions & 1 deletion JustDummies.UnitTests/SurfaceParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ public static IEnumerable<object[]> Builders() {
// The remaining scalar builders each carry their own deliberate set.
yield return [typeof(AnyBoolean), new[] { "True", "False", "DifferentFrom" }];
yield return [typeof(AnyGuid), new[] { "NonEmpty", "Empty", "OneOf", "Except", "DifferentFrom" }];
yield return [typeof(AnyEnum<DayOfWeek>), new[] { "OneOf", "Except", "DifferentFrom" }];
// AnyEnum adds AllowingCombinations, the opt-in widening the draw from the declared members to their
// combinations — meaningful only for a [Flags] enum, hence a constraint rather than a second factory.
yield return [typeof(AnyEnum<DayOfWeek>), new[] { "AllowingCombinations", "OneOf", "Except", "DifferentFrom" }];
yield return [typeof(AnyChar), new[] { "Alpha", "AlphaNumeric", "Numeric", "UpperCase", "LowerCase", "OneOf", "Except", "DifferentFrom" }];

// AnyString carries the exclusion pair Except/DifferentFrom (met by a bounded redraw, since strings are not
Expand Down
Loading