diff --git a/JustDummies.PropertyTests/EnumCombinationProperties.cs b/JustDummies.PropertyTests/EnumCombinationProperties.cs
new file mode 100644
index 00000000..c90457a0
--- /dev/null
+++ b/JustDummies.PropertyTests/EnumCombinationProperties.cs
@@ -0,0 +1,128 @@
+#region Usings declarations
+
+using FsCheck;
+using FsCheck.Fluent;
+
+using JetBrains.Annotations;
+
+#endregion
+
+namespace JustDummies.PropertyTests;
+
+///
+/// Property-based tests for . Where the example-based suite pins
+/// the universe a handful of named enum shapes yield, these quantify over the constraints 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.
+///
+///
+/// The universe is fixed per enum type, so it is the constraint sets — not the type — that carry the input space.
+/// 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.
+///
+[TestSubject(typeof(AnyEnum<>))]
+public sealed class EnumCombinationProperties {
+
+ #region Statics members declarations
+
+ /// Every value AllowingCombinations() must be able to draw for .
+ private static readonly Permissions[] Universe = Enumerable.Range(0, 8).Select(bits => (Permissions)bits).ToArray();
+
+ ///
+ /// 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: Except and
+ /// OneOf both have to absorb a duplicate without changing the pool they compute.
+ ///
+ private static Gen 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(
+ () => Any.Enum().AllowingCombinations().Except(excluded));
+ }
+
+ return Expect.EveryDraw(Any.Enum().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 draws = Expect.Draws(Any.Enum().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().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 generator = excluded.Length == 0
+ ? Any.Enum()
+ : Any.Enum().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 first = Expect.Draws(Any.WithSeed(seed).Enum().AllowingCombinations(), 12);
+ List second = Expect.Draws(Any.WithSeed(seed).Enum().AllowingCombinations(), 12);
+
+ return first.SequenceEqual(second);
+ })
+ .QuickCheckThrowOnFailure();
+ }
+
+ [Flags]
+ private enum Permissions {
+
+ None = 0,
+ Read = 1,
+ Write = 2,
+ Exec = 4
+
+ }
+
+}
diff --git a/JustDummies.UnitTests/AnyDateTimeOffsetOffsetTests.cs b/JustDummies.UnitTests/AnyDateTimeOffsetOffsetTests.cs
index 91de606b..8cd6c087 100644
--- a/JustDummies.UnitTests/AnyDateTimeOffsetOffsetTests.cs
+++ b/JustDummies.UnitTests/AnyDateTimeOffsetOffsetTests.cs
@@ -69,6 +69,21 @@ public void WithOffsetArguments() {
Check.ThatCode(() => Any.DateTimeOffset().WithOffsetBetween(TimeSpan.FromHours(2), TimeSpan.FromHours(-2))).Throws();
}
+ [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)))
diff --git a/JustDummies.UnitTests/AnyEnumCombinationTests.cs b/JustDummies.UnitTests/AnyEnumCombinationTests.cs
new file mode 100644
index 00000000..9f62675c
--- /dev/null
+++ b/JustDummies.UnitTests/AnyEnumCombinationTests.cs
@@ -0,0 +1,207 @@
+#region Usings declarations
+
+using NFluent;
+
+#endregion
+
+namespace JustDummies.UnitTests;
+
+///
+/// The named cases of : 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
+/// JustDummies.PropertyTests.
+///
+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 seen = new();
+ for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum().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 seen = new();
+ for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum().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 seen = new();
+ for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum().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 seen = new();
+ for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Enum().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(
+ () => Any.Enum().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 generator = Any.Enum().AllowingCombinations().AllowingCombinations();
+
+ HashSet 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(
+ () => Any.Enum().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 generator = Any.Enum()
+ .AllowingCombinations()
+ .OneOf(Permissions.Read | Permissions.Write, Permissions.Exec);
+
+ HashSet 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 generator = Any.Enum().AllowingCombinations().Except(Permissions.Read);
+
+ HashSet 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 eight = Any.SetOf(Any.Enum().AllowingCombinations()).WithCount(8).Generate();
+ Check.That(eight.Count).IsEqualTo(8);
+
+ ConflictingAnyConstraintException conflict = Assert.Throws(
+ () => Any.SetOf(Any.Enum().AllowingCombinations()).WithCount(9).Generate());
+ Check.That(conflict.Message).Contains("9");
+
+ ConflictingAnyConstraintException capped = Assert.Throws(
+ () => Any.SetOf(Any.Enum()).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(
+ () => Any.Enum().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(
+ () => Any.Enum().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 Batch(int seed) {
+ AnyContext context = Any.WithSeed(seed);
+ AnyEnum generator = context.Enum().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
+
+ }
+
+}
diff --git a/JustDummies.UnitTests/SurfaceParityTests.cs b/JustDummies.UnitTests/SurfaceParityTests.cs
index 76f96f1d..b391e632 100644
--- a/JustDummies.UnitTests/SurfaceParityTests.cs
+++ b/JustDummies.UnitTests/SurfaceParityTests.cs
@@ -173,7 +173,9 @@ public static IEnumerable