diff --git a/Dummies.UnitTests/AnyContinuousTests.cs b/Dummies.UnitTests/AnyContinuousTests.cs new file mode 100644 index 00000000..731fd747 --- /dev/null +++ b/Dummies.UnitTests/AnyContinuousTests.cs @@ -0,0 +1,119 @@ +#region Usings declarations + +using NFluent; + +#endregion + +namespace Dummies.UnitTests; + +public sealed class AnyContinuousTests { + + private const int SampleCount = 200; + + [Fact(DisplayName = "Double: unconstrained draws are always finite.")] + public void DoubleIsFinite() { + for (int i = 0; i < SampleCount; i++) { + double value = Any.Double().Generate(); + Check.That(double.IsNaN(value) || double.IsInfinity(value)).IsFalse(); + } + } + + [Fact(DisplayName = "Double: sign constraints are strict, Zero pins, NonZero excludes.")] + public void DoubleSignFamily() { + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.Double().Positive().Generate()).IsStrictlyGreaterThan(0d); + Check.That(Any.Double().Negative().Generate()).IsStrictlyLessThan(0d); + Check.That(Any.Double().NonZero().Generate()).IsNotEqualTo(0d); + } + Check.That(Any.Double().Zero().Generate()).IsEqualTo(0d); + Check.ThatCode(() => Any.Double().Zero().NonZero()).Throws(); + Check.ThatCode(() => Any.Double().Positive().Negative()).Throws(); + } + + [Fact(DisplayName = "Double: Between contains, GreaterThan is strict, and conflicts name both sides.")] + public void DoubleBounds() { + for (int i = 0; i < SampleCount; i++) { + double bounded = Any.Double().Between(1d, 2d).Generate(); + Check.That(bounded).IsGreaterOrEqualThan(1d); + Check.That(bounded).IsLessOrEqualThan(2d); + Check.That(Any.Double().GreaterThan(1d).LessThanOrEqualTo(2d).Generate()).IsStrictlyGreaterThan(1d); + } + + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.Double().GreaterThan(100d).LessThan(10d)); + Check.That(conflict.Message).Contains("LessThan(10)"); + Check.That(conflict.Message).Contains("GreaterThan(100)"); + Check.ThatCode(() => Any.Double().GreaterThan(double.MaxValue)).Throws(); + } + + [Fact(DisplayName = "Double: non-finite arguments are rejected as argument errors.")] + public void DoubleRejectsNonFinite() { + Check.ThatCode(() => Any.Double().GreaterThan(double.NaN)).Throws(); + Check.ThatCode(() => Any.Double().LessThan(double.PositiveInfinity)).Throws(); + Check.ThatCode(() => Any.Double().Between(double.NegativeInfinity, 0d)).Throws(); + Check.ThatCode(() => Any.Double().OneOf(1d, double.NaN)).Throws(); + Check.ThatCode(() => Any.Double().Between(10d, 1d)).Throws(); + } + + [Fact(DisplayName = "Double: OneOf stays within and Except/DifferentFrom never yield the excluded value.")] + public void DoubleSets() { + double[] allowed = [1.5d, 2.5d]; + for (int i = 0; i < SampleCount; i++) { + Check.That(allowed.Contains(Any.Double().OneOf(allowed).Generate())).IsTrue(); + Check.That(Any.Double().OneOf(allowed).Except(1.5d).Generate()).IsEqualTo(2.5d); + Check.That(Any.Double().OneOf(allowed).DifferentFrom(2.5d).Generate()).IsEqualTo(1.5d); + } + } + + [Fact(DisplayName = "Single: finite draws, strict signs, bounds contained, NaN rejected.")] + public void SingleBehaves() { + for (int i = 0; i < SampleCount; i++) { + float value = Any.Single().Generate(); + Check.That(float.IsNaN(value) || float.IsInfinity(value)).IsFalse(); + Check.That(Any.Single().Positive().Generate()).IsStrictlyGreaterThan(0f); + + float bounded = Any.Single().Between(1f, 2f).Generate(); + Check.That(bounded).IsGreaterOrEqualThan(1f); + Check.That(bounded).IsLessOrEqualThan(2f); + } + + Check.That(Any.Single().Zero().Generate()).IsEqualTo(0f); + Check.ThatCode(() => Any.Single().GreaterThan(float.NaN)).Throws(); + Check.ThatCode(() => Any.Single().GreaterThan(float.MaxValue)).Throws(); + Check.ThatCode(() => Any.Single().Positive().Negative()).Throws(); + } + + [Fact(DisplayName = "Decimal: strict signs, pinned zero, contained bounds, and strict GreaterThan via exclusion.")] + public void DecimalBehaves() { + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.Decimal().Positive().Generate()).IsStrictlyGreaterThan(0m); + Check.That(Any.Decimal().Negative().Generate()).IsStrictlyLessThan(0m); + + decimal bounded = Any.Decimal().Between(1m, 2m).Generate(); + Check.That(bounded).IsGreaterOrEqualThan(1m); + Check.That(bounded).IsLessOrEqualThan(2m); + Check.That(Any.Decimal().Between(1m, 2m).GreaterThan(1m).Generate()).IsStrictlyGreaterThan(1m); + } + + Check.That(Any.Decimal().Zero().Generate()).IsEqualTo(0m); + Check.ThatCode(() => Any.Decimal().Zero().NonZero()).Throws(); + Check.ThatCode(() => Any.Decimal().Between(10m, 1m)).Throws(); + + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.Decimal().GreaterThan(100m).LessThan(10m)); + Check.That(conflict.Message).Contains("LessThan(10)"); + Check.That(conflict.Message).Contains("GreaterThan(100)"); + } + + [Fact(DisplayName = "Continuous generators convert implicitly to their value type.")] + public void ImplicitConversions() { + double d = Any.Double().Between(1d, 2d); + float f = Any.Single().Between(1f, 2f); + decimal m = Any.Decimal().Between(1m, 2m); + + Check.That(d).IsGreaterOrEqualThan(1d); + Check.That(f).IsGreaterOrEqualThan(1f); + Check.That(m).IsGreaterOrEqualThan(1m); + } + +} diff --git a/Dummies.UnitTests/AnyModernTypeTests.cs b/Dummies.UnitTests/AnyModernTypeTests.cs new file mode 100644 index 00000000..9353b23f --- /dev/null +++ b/Dummies.UnitTests/AnyModernTypeTests.cs @@ -0,0 +1,106 @@ +#region Usings declarations + +using NFluent; + +#endregion + +namespace Dummies.UnitTests; + +public sealed class AnyModernTypeTests { + + private const int SampleCount = 200; + + private static readonly DateOnly AnchorDate = new(2026, 1, 1); + private static readonly TimeOnly AnchorTime = new(12, 0, 0); + + [Fact(DisplayName = "DateOnly: Between is inclusive and reached; After/Before are exclusive; conflicts surface.")] + public void DateOnlyBehaves() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { + DateOnly value = Any.DateOnly().Between(AnchorDate, AnchorDate.AddDays(2)).Generate(); + seen.Add(value); + Check.That(value >= AnchorDate && value <= AnchorDate.AddDays(2)).IsTrue(); + Check.That(Any.DateOnly().After(AnchorDate).Before(AnchorDate.AddDays(2)).Generate()).IsEqualTo(AnchorDate.AddDays(1)); + } + Check.That(seen.Contains(AnchorDate)).IsTrue(); + Check.That(seen.Contains(AnchorDate.AddDays(2))).IsTrue(); + + Check.ThatCode(() => Any.DateOnly().After(DateOnly.MaxValue)).Throws(); + Check.ThatCode(() => Any.DateOnly().Between(AnchorDate.AddDays(1), AnchorDate)).Throws(); + } + + [Fact(DisplayName = "DateOnly: OneOf/Except/DifferentFrom behave.")] + public void DateOnlySets() { + DateOnly[] allowed = [AnchorDate, AnchorDate.AddDays(7)]; + for (int i = 0; i < SampleCount; i++) { + Check.That(allowed.Contains(Any.DateOnly().OneOf(allowed).Generate())).IsTrue(); + Check.That(Any.DateOnly().OneOf(allowed).Except(AnchorDate).Generate()).IsEqualTo(AnchorDate.AddDays(7)); + Check.That(Any.DateOnly().OneOf(allowed).DifferentFrom(AnchorDate.AddDays(7)).Generate()).IsEqualTo(AnchorDate); + } + } + + [Fact(DisplayName = "TimeOnly: bounds behave and the exclusive window pins the middle tick.")] + public void TimeOnlyBehaves() { + for (int i = 0; i < SampleCount; i++) { + TimeOnly value = Any.TimeOnly().Between(AnchorTime, AnchorTime.Add(TimeSpan.FromMinutes(5))).Generate(); + Check.That(value >= AnchorTime && value <= AnchorTime.Add(TimeSpan.FromMinutes(5))).IsTrue(); + + TimeOnly middle = Any.TimeOnly().After(AnchorTime).Before(new TimeOnly(AnchorTime.Ticks + 2)).Generate(); + Check.That(middle.Ticks).IsEqualTo(AnchorTime.Ticks + 1); + } + + Check.ThatCode(() => Any.TimeOnly().After(TimeOnly.MaxValue)).Throws(); + } + + [Fact(DisplayName = "Int128: signs, pins, full-width variety, extremes and conflicts.")] + public void Int128Behaves() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { + seen.Add(Any.Int128().Generate()); + Check.That(Any.Int128().Positive().Generate() > 0).IsTrue(); + Check.That(Any.Int128().Negative().Generate() < 0).IsTrue(); + + Int128 bounded = Any.Int128().Between(1, 3).Generate(); + Check.That(bounded >= 1 && bounded <= 3).IsTrue(); + } + Check.That(seen.Count).IsStrictlyGreaterThan(1); + + Check.That(Any.Int128().Zero().Generate() == 0).IsTrue(); + Check.ThatCode(() => Any.Int128().GreaterThan(Int128.MaxValue)).Throws(); + Check.ThatCode(() => Any.Int128().Positive().Negative()).Throws(); + } + + [Fact(DisplayName = "UInt128: bounds, exclusivity and full-width variety.")] + public void UInt128Behaves() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { + seen.Add(Any.UInt128().Generate()); + + UInt128 bounded = Any.UInt128().Between(1, 3).Generate(); + Check.That(bounded >= 1 && bounded <= 3).IsTrue(); + Check.That(Any.UInt128().GreaterThan(5).LessThanOrEqualTo(6).Generate() == 6).IsTrue(); + } + Check.That(seen.Count).IsStrictlyGreaterThan(1); + + Check.That(Any.UInt128().Zero().Generate() == 0).IsTrue(); + Check.ThatCode(() => Any.UInt128().GreaterThan(UInt128.MaxValue)).Throws(); + } + + [Fact(DisplayName = "Half: finite draws, strict Positive, pinned Zero, contained bounds, argument checks.")] + public void HalfBehaves() { + for (int i = 0; i < SampleCount; i++) { + Half value = Any.Half().Generate(); + Check.That(Half.IsNaN(value) || Half.IsInfinity(value)).IsFalse(); + Check.That(Any.Half().Positive().Generate() > Half.Zero).IsTrue(); + + Half bounded = Any.Half().Between((Half)1f, (Half)2f).Generate(); + Check.That(bounded >= (Half)1f && bounded <= (Half)2f).IsTrue(); + } + + Check.That(Any.Half().Zero().Generate() == Half.Zero).IsTrue(); + Check.ThatCode(() => Any.Half().GreaterThan(Half.NaN)).Throws(); + Check.ThatCode(() => Any.Half().GreaterThan(Half.MaxValue)).Throws(); + Check.ThatCode(() => Any.Half().Positive().Negative()).Throws(); + } + +} diff --git a/Dummies.UnitTests/AnySetTypeTests.cs b/Dummies.UnitTests/AnySetTypeTests.cs new file mode 100644 index 00000000..8b49d306 --- /dev/null +++ b/Dummies.UnitTests/AnySetTypeTests.cs @@ -0,0 +1,140 @@ +#region Usings declarations + +using NFluent; + +#endregion + +namespace Dummies.UnitTests; + +public sealed class AnySetTypeTests { + + private const int SampleCount = 200; + + private enum OrderStatus { + + Draft, + Validated, + Cancelled + + } + + [Fact(DisplayName = "Bool: unconstrained draws hit both values; pins pin; contradictory pins conflict.")] + public void BoolBehaves() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Bool().Generate()); } + Check.That(seen.Count).IsEqualTo(2); + + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.Bool().True().Generate()).IsTrue(); + Check.That(Any.Bool().False().Generate()).IsFalse(); + Check.That(Any.Bool().DifferentFrom(true).Generate()).IsFalse(); + } + + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.Bool().True().False()); + Check.That(conflict.Message).Contains("False()"); + Check.That(conflict.Message).Contains("True()"); + + bool value = Any.Bool().True(); + Check.That(value).IsTrue(); + } + + [Fact(DisplayName = "Guid: unconstrained draws are non-empty, varied, and reproducible under a context seed.")] + public void GuidBehaves() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { + Guid value = Any.Guid().Generate(); + seen.Add(value); + Check.That(value).IsNotEqualTo(Guid.Empty); + } + Check.That(seen.Count).IsStrictlyGreaterThan(1); + + Check.That(Any.WithSeed(42).Guid().Generate()).IsEqualTo(Any.WithSeed(42).Guid().Generate()); + } + + [Fact(DisplayName = "Guid: Empty pins, NonEmpty excludes, and the pair conflicts in both orders.")] + public void GuidEmptyFamily() { + Check.That(Any.Guid().Empty().Generate()).IsEqualTo(Guid.Empty); + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.Guid().NonEmpty().Generate()).IsNotEqualTo(Guid.Empty); + } + + Check.ThatCode(() => Any.Guid().Empty().NonEmpty()).Throws(); + Check.ThatCode(() => Any.Guid().NonEmpty().Empty()).Throws(); + } + + [Fact(DisplayName = "Guid: OneOf stays within, exhausting it conflicts, DifferentFrom never yields the value.")] + public void GuidSets() { + Guid first = Guid.NewGuid(); + Guid second = Guid.NewGuid(); + + for (int i = 0; i < SampleCount; i++) { + Guid value = Any.Guid().OneOf(first, second).Generate(); + Check.That(value == first || value == second).IsTrue(); + Check.That(Any.Guid().OneOf(first, second).DifferentFrom(first).Generate()).IsEqualTo(second); + } + + Check.ThatCode(() => Any.Guid().OneOf(first).Except(first)).Throws(); + } + + [Fact(DisplayName = "Enum: unconstrained draws yield only declared members and reach all of them.")] + public void EnumDrawsDeclaredMembers() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { + OrderStatus value = Any.Enum().Generate(); + seen.Add(value); + Check.That(System.Enum.IsDefined(typeof(OrderStatus), value)).IsTrue(); + } + Check.That(seen.Count).IsEqualTo(3); + } + + [Fact(DisplayName = "Enum: OneOf restricts, Except removes, exhausting the pool conflicts.")] + public void EnumSets() { + for (int i = 0; i < SampleCount; i++) { + OrderStatus restricted = Any.Enum().OneOf(OrderStatus.Draft, OrderStatus.Validated).Generate(); + Check.That(restricted == OrderStatus.Draft || restricted == OrderStatus.Validated).IsTrue(); + Check.That(Any.Enum().Except(OrderStatus.Cancelled).Generate()).IsNotEqualTo(OrderStatus.Cancelled); + Check.That(Any.Enum().OneOf(OrderStatus.Draft, OrderStatus.Validated).DifferentFrom(OrderStatus.Draft).Generate()).IsEqualTo(OrderStatus.Validated); + } + + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.Enum().Except(OrderStatus.Draft, OrderStatus.Validated, OrderStatus.Cancelled)); + Check.That(conflict.Message).Contains("Except("); + } + + [Fact(DisplayName = "Enum: OneOf rejects undeclared numeric values — the declared-members-only contract holds.")] + public void EnumOneOfRejectsUndeclaredValues() { + ArgumentException rejected = Assert.Throws( + () => Any.Enum().OneOf((OrderStatus)42)); + Check.That(rejected.Message).Contains("42"); + Check.That(rejected.Message).Contains("OrderStatus"); + + Check.ThatCode(() => Any.Enum().OneOf(OrderStatus.Draft, (OrderStatus)42)).Throws(); + } + + [Fact(DisplayName = "Char: the default pool is ASCII letters and digits; families narrow it.")] + public void CharPools() { + for (int i = 0; i < SampleCount; i++) { + char value = Any.Char().Generate(); + Check.That(value is >= 'A' and <= 'Z' or >= 'a' and <= 'z' or >= '0' and <= '9').IsTrue(); + Check.That(Any.Char().Numeric().Generate() is >= '0' and <= '9').IsTrue(); + Check.That(Any.Char().Alpha().Generate() is >= 'A' and <= 'Z' or >= 'a' and <= 'z').IsTrue(); + Check.That(Any.Char().LowerCase().Generate() is >= 'A' and <= 'Z').IsFalse(); + Check.That(Any.Char().Alpha().UpperCase().Generate() is >= 'A' and <= 'Z').IsTrue(); + } + } + + [Fact(DisplayName = "Char: OneOf restricts, exclusions apply, and contradictions conflict.")] + public void CharSets() { + for (int i = 0; i < SampleCount; i++) { + char value = Any.Char().OneOf('a', 'b').Generate(); + Check.That(value == 'a' || value == 'b').IsTrue(); + Check.That(Any.Char().OneOf('a', 'b').DifferentFrom('a').Generate()).IsEqualTo('b'); + } + + Check.ThatCode(() => Any.Char().Numeric().Alpha()).Throws(); + Check.ThatCode(() => Any.Char().OneOf('a').Except('a')).Throws(); + Check.ThatCode(() => Any.Char().OneOf('a').Numeric()).Throws(); + } + +} diff --git a/Dummies.UnitTests/AnySignedIntegerTests.cs b/Dummies.UnitTests/AnySignedIntegerTests.cs new file mode 100644 index 00000000..1956f199 --- /dev/null +++ b/Dummies.UnitTests/AnySignedIntegerTests.cs @@ -0,0 +1,98 @@ +#region Usings declarations + +using NFluent; + +#endregion + +namespace Dummies.UnitTests; + +public sealed class AnySignedIntegerTests { + + private const int SampleCount = 200; + + [Fact(DisplayName = "SByte: Positive and Negative are strict, and contradict each other.")] + public void SByteSignConstraints() { + for (int i = 0; i < SampleCount; i++) { + Check.That((sbyte)Any.SByte().Positive().Generate()).IsStrictlyGreaterThan((sbyte)0); + Check.That((sbyte)Any.SByte().Negative().Generate()).IsStrictlyLessThan((sbyte)0); + } + + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.SByte().Positive().Negative()); + Check.That(conflict.Message).Contains("Negative()"); + Check.That(conflict.Message).Contains("Positive()"); + } + + [Fact(DisplayName = "SByte: Between is inclusive and reaches both bounds; extremes are generable.")] + public void SByteBounds() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { seen.Add(Any.SByte().Between(-1, 1).Generate()); } + Check.That(seen.Contains(-1)).IsTrue(); + Check.That(seen.Contains(1)).IsTrue(); + + Check.That(Any.SByte().LessThanOrEqualTo(sbyte.MinValue).Generate()).IsEqualTo(sbyte.MinValue); + Check.That(Any.SByte().GreaterThanOrEqualTo(sbyte.MaxValue).Generate()).IsEqualTo(sbyte.MaxValue); + Check.ThatCode(() => Any.SByte().GreaterThan(sbyte.MaxValue)).Throws(); + } + + [Fact(DisplayName = "Int16: Zero pins, NonZero excludes, and the pair conflicts.")] + public void Int16ZeroFamily() { + Check.That(Any.Int16().Zero().Generate()).IsEqualTo((short)0); + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.Int16().Between(-1, 1).NonZero().Generate()).IsNotEqualTo((short)0); + } + Check.ThatCode(() => Any.Int16().Zero().NonZero()).Throws(); + } + + [Fact(DisplayName = "Int16: GreaterThan and LessThan are exclusive bounds.")] + public void Int16ExclusiveBounds() { + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.Int16().GreaterThan(10).LessThanOrEqualTo(12).Generate()).IsGreaterOrEqualThan((short)11); + Check.That(Any.Int16().LessThan(10).GreaterThanOrEqualTo(8).Generate()).IsLessOrEqualThan((short)9); + } + } + + [Fact(DisplayName = "Int64: full-range generation works and crossed bounds conflict naming both sides.")] + public void Int64RangeAndConflicts() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { seen.Add(Any.Int64().Generate()); } + Check.That(seen.Count).IsStrictlyGreaterThan(1); + + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.Int64().GreaterThan(100L).LessThan(10L)); + Check.That(conflict.Message).Contains("LessThan(10)"); + Check.That(conflict.Message).Contains("GreaterThan(100)"); + } + + [Fact(DisplayName = "Int64: OneOf stays within the supplied values and Except never yields an excluded one.")] + public void Int64OneOfAndExcept() { + long[] allowed = [1L, 5L, 9L]; + for (int i = 0; i < SampleCount; i++) { + Check.That(allowed.Contains(Any.Int64().OneOf(allowed).Generate())).IsTrue(); + Check.That(Any.Int64().Between(1L, 3L).Except(2L).Generate()).IsNotEqualTo(2L); + Check.That(Any.Int64().Between(7L, 8L).DifferentFrom(7L).Generate()).IsEqualTo(8L); + } + } + + [Fact(DisplayName = "Int64: extremes are generable and arguments are validated.")] + public void Int64ExtremesAndArguments() { + Check.That(Any.Int64().LessThanOrEqualTo(long.MinValue).Generate()).IsEqualTo(long.MinValue); + Check.That(Any.Int64().GreaterThanOrEqualTo(long.MaxValue).Generate()).IsEqualTo(long.MaxValue); + Check.ThatCode(() => Any.Int64().GreaterThan(long.MaxValue)).Throws(); + Check.ThatCode(() => Any.Int64().Between(10L, 1L)).Throws(); + Check.ThatCode(() => Any.Int64().OneOf()).Throws(); + Check.ThatCode(() => Any.Int64().Except(null!)).Throws(); + } + + [Fact(DisplayName = "Signed integers convert implicitly to their value type.")] + public void ImplicitConversions() { + sbyte small = Any.SByte().Positive(); + short mid = Any.Int16().Negative(); + long wide = Any.Int64().Between(1L, 10L); + + Check.That((int)small).IsStrictlyGreaterThan(0); + Check.That((int)mid).IsStrictlyLessThan(0); + Check.That(wide).IsGreaterOrEqualThan(1L); + } + +} diff --git a/Dummies.UnitTests/AnyTimeTests.cs b/Dummies.UnitTests/AnyTimeTests.cs new file mode 100644 index 00000000..0d1e84ac --- /dev/null +++ b/Dummies.UnitTests/AnyTimeTests.cs @@ -0,0 +1,116 @@ +#region Usings declarations + +using NFluent; + +#endregion + +namespace Dummies.UnitTests; + +public sealed class AnyTimeTests { + + private const int SampleCount = 200; + + private static readonly DateTime Anchor = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + [Fact(DisplayName = "TimeSpan: Positive and Negative are strict against zero, and contradict each other.")] + public void TimeSpanSigns() { + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.TimeSpan().Positive().Generate() > TimeSpan.Zero).IsTrue(); + Check.That(Any.TimeSpan().Negative().Generate() < TimeSpan.Zero).IsTrue(); + } + + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.TimeSpan().Positive().Negative()); + Check.That(conflict.Message).Contains("Negative()"); + Check.That(conflict.Message).Contains("Positive()"); + } + + [Fact(DisplayName = "TimeSpan: Zero pins, Between is inclusive over a tiny tick window, GreaterThan is exclusive.")] + public void TimeSpanBounds() { + Check.That(Any.TimeSpan().Zero().Generate()).IsEqualTo(TimeSpan.Zero); + + HashSet ticks = new(); + for (int i = 0; i < SampleCount; i++) { + TimeSpan value = Any.TimeSpan().Between(TimeSpan.FromTicks(1), TimeSpan.FromTicks(3)).Generate(); + ticks.Add(value.Ticks); + Check.That(value.Ticks).IsGreaterOrEqualThan(1L); + Check.That(value.Ticks).IsLessOrEqualThan(3L); + + Check.That(Any.TimeSpan().GreaterThan(TimeSpan.FromTicks(5)).LessThanOrEqualTo(TimeSpan.FromTicks(6)).Generate().Ticks).IsEqualTo(6L); + } + Check.That(ticks.Contains(1L)).IsTrue(); + Check.That(ticks.Contains(3L)).IsTrue(); + } + + [Fact(DisplayName = "DateTime: every generated value carries Utc kind.")] + public void DateTimeIsUtc() { + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.DateTime().Generate().Kind == DateTimeKind.Utc).IsTrue(); + Check.That(Any.DateTime().Between(Anchor, Anchor.AddDays(1)).Generate().Kind == DateTimeKind.Utc).IsTrue(); + } + } + + [Fact(DisplayName = "DateTime: After and Before are exclusive — a three-tick window pins the middle tick.")] + public void DateTimeExclusiveWindow() { + for (int i = 0; i < SampleCount; i++) { + DateTime value = Any.DateTime().After(Anchor).Before(Anchor.AddTicks(2)).Generate(); + Check.That(value.Ticks).IsEqualTo(Anchor.Ticks + 1); + } + } + + [Fact(DisplayName = "DateTime: an impossible After/Before pair conflicts naming both sides; crossed Between is an argument error.")] + public void DateTimeConflicts() { + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.DateTime().After(Anchor).Before(Anchor)); + Check.That(conflict.Message).Contains("Before("); + Check.That(conflict.Message).Contains("After("); + + Check.ThatCode(() => Any.DateTime().Between(Anchor.AddDays(1), Anchor)).Throws(); + Check.ThatCode(() => Any.DateTime().After(DateTime.MaxValue)).Throws(); + } + + [Fact(DisplayName = "DateTime: OneOf yields only supplied instants and DifferentFrom always picks the other of two.")] + public void DateTimeSets() { + DateTime[] allowed = [Anchor, Anchor.AddDays(1)]; + for (int i = 0; i < SampleCount; i++) { + Check.That(allowed.Contains(Any.DateTime().OneOf(allowed).Generate())).IsTrue(); + Check.That(Any.DateTime().Between(Anchor, Anchor.AddTicks(1)).DifferentFrom(Anchor).Generate().Ticks).IsEqualTo(Anchor.Ticks + 1); + } + } + + [Fact(DisplayName = "DateTimeOffset: generated values carry a zero offset, and comparisons work by instant.")] + public void DateTimeOffsetIsUtc() { + DateTimeOffset start = new(Anchor, TimeSpan.Zero); + for (int i = 0; i < SampleCount; i++) { + DateTimeOffset value = Any.DateTimeOffset().Between(start, start.AddDays(1)).Generate(); + Check.That(value.Offset).IsEqualTo(TimeSpan.Zero); + } + + // A +02:00 bound constrains by UtcTicks: the exclusive three-tick window still pins the middle tick. + DateTimeOffset shifted = start.ToOffset(TimeSpan.FromHours(2)); + for (int i = 0; i < SampleCount; i++) { + DateTimeOffset value = Any.DateTimeOffset().After(shifted).Before(shifted.AddTicks(2)).Generate(); + Check.That(value.UtcTicks).IsEqualTo(start.UtcTicks + 1); + } + } + + [Fact(DisplayName = "DateTimeOffset: OneOf returns the supplied values as given, offset included.")] + public void DateTimeOffsetOneOfPreservesOffsets() { + DateTimeOffset supplied = new(2026, 7, 18, 10, 0, 0, TimeSpan.FromHours(2)); + for (int i = 0; i < SampleCount; i++) { + DateTimeOffset value = Any.DateTimeOffset().OneOf(supplied).Generate(); + Check.That(value).IsEqualTo(supplied); + Check.That(value.Offset).IsEqualTo(TimeSpan.FromHours(2)); + } + } + + [Fact(DisplayName = "DateTimeOffset: Except excludes by instant.")] + public void DateTimeOffsetExceptByInstant() { + DateTimeOffset start = new(Anchor, TimeSpan.Zero); + for (int i = 0; i < SampleCount; i++) { + DateTimeOffset value = Any.DateTimeOffset().Between(start, start.AddTicks(1)).Except(start).Generate(); + Check.That(value.UtcTicks).IsEqualTo(start.UtcTicks + 1); + } + } + +} diff --git a/Dummies.UnitTests/AnyUnsignedIntegerTests.cs b/Dummies.UnitTests/AnyUnsignedIntegerTests.cs new file mode 100644 index 00000000..b4271b6d --- /dev/null +++ b/Dummies.UnitTests/AnyUnsignedIntegerTests.cs @@ -0,0 +1,89 @@ +#region Usings declarations + +using NFluent; + +#endregion + +namespace Dummies.UnitTests; + +public sealed class AnyUnsignedIntegerTests { + + private const int SampleCount = 200; + + [Fact(DisplayName = "Byte: Between is inclusive and reaches both bounds; extremes are generable.")] + public void ByteBounds() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { + byte value = Any.Byte().Between(1, 3).Generate(); + seen.Add(value); + Check.That((int)value).IsGreaterOrEqualThan(1); + Check.That((int)value).IsLessOrEqualThan(3); + } + Check.That(seen.Contains(1)).IsTrue(); + Check.That(seen.Contains(3)).IsTrue(); + + Check.That(Any.Byte().LessThanOrEqualTo(0).Generate()).IsEqualTo((byte)0); + Check.That(Any.Byte().GreaterThanOrEqualTo(byte.MaxValue).Generate()).IsEqualTo(byte.MaxValue); + } + + [Fact(DisplayName = "Byte: Zero pins, NonZero excludes, the pair conflicts, and GreaterThan(max) conflicts.")] + public void ByteZeroAndConflicts() { + Check.That(Any.Byte().Zero().Generate()).IsEqualTo((byte)0); + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.Byte().Between(0, 1).NonZero().Generate()).IsEqualTo((byte)1); + } + Check.ThatCode(() => Any.Byte().Zero().NonZero()).Throws(); + Check.ThatCode(() => Any.Byte().GreaterThan(byte.MaxValue)).Throws(); + } + + [Fact(DisplayName = "UInt16 and UInt32: exclusive bounds behave and crossed bounds conflict.")] + public void MidWidthExclusiveBounds() { + for (int i = 0; i < SampleCount; i++) { + Check.That((int)Any.UInt16().GreaterThan(10).LessThanOrEqualTo(12).Generate()).IsGreaterOrEqualThan(11); + Check.That(Any.UInt32().LessThan(10u).GreaterThanOrEqualTo(8u).Generate()).IsLessOrEqualThan(9u); + } + + ConflictingAnyConstraintException conflict = Assert.Throws( + () => Any.UInt32().GreaterThan(100u).LessThan(10u)); + Check.That(conflict.Message).Contains("LessThan(10)"); + Check.That(conflict.Message).Contains("GreaterThan(100)"); + } + + [Fact(DisplayName = "UInt64: the full-width sampling path yields varied values and honors exclusions.")] + public void UInt64FullWidth() { + HashSet seen = new(); + for (int i = 0; i < SampleCount; i++) { seen.Add(Any.UInt64().Generate()); } + Check.That(seen.Count).IsStrictlyGreaterThan(1); + + for (int i = 0; i < SampleCount; i++) { + Check.That(Any.UInt64().Between(0UL, 2UL).Except(1UL).Generate()).IsNotEqualTo(1UL); + } + } + + [Fact(DisplayName = "UInt64: extremes are generable and OneOf/Except behave.")] + public void UInt64ExtremesAndSets() { + Check.That(Any.UInt64().GreaterThanOrEqualTo(ulong.MaxValue).Generate()).IsEqualTo(ulong.MaxValue); + Check.ThatCode(() => Any.UInt64().GreaterThan(ulong.MaxValue)).Throws(); + + ulong[] allowed = [1UL, 5UL]; + for (int i = 0; i < SampleCount; i++) { + Check.That(allowed.Contains(Any.UInt64().OneOf(allowed).Generate())).IsTrue(); + Check.That(Any.UInt64().Between(7UL, 8UL).DifferentFrom(7UL).Generate()).IsEqualTo(8UL); + } + Check.ThatCode(() => Any.UInt64().Between(10UL, 1UL)).Throws(); + } + + [Fact(DisplayName = "Unsigned integers convert implicitly to their value type.")] + public void ImplicitConversions() { + byte tiny = Any.Byte().Between(1, 10); + ushort mid = Any.UInt16().NonZero(); + uint wide = Any.UInt32().Between(1u, 10u); + ulong huge = Any.UInt64().Between(1UL, 10UL); + + Check.That((int)tiny).IsGreaterOrEqualThan(1); + Check.That((int)mid).IsStrictlyGreaterThan(0); + Check.That(wide).IsGreaterOrEqualThan(1u); + Check.That(huge).IsGreaterOrEqualThan(1UL); + } + +} diff --git a/Dummies.UnitTests/SeedReproducibilityTests.cs b/Dummies.UnitTests/SeedReproducibilityTests.cs index c7a4e4ec..138a2687 100644 --- a/Dummies.UnitTests/SeedReproducibilityTests.cs +++ b/Dummies.UnitTests/SeedReproducibilityTests.cs @@ -16,13 +16,26 @@ public sealed class SeedReproducibilityTests { private static string Batch() { // Explicitly typed locals: string.Join(params object[]) would otherwise box the generators and // call their ToString() instead of triggering the implicit conversions. - int full = Any.Int32(); - int bounded = Any.Int32().Between(1, 1000); - string free = Any.String(); - string capped = Any.String().NonEmpty().WithMaxLength(50); - string shaped = Any.String().StartingWith("ORD-").WithLength(12); - - return string.Join("|", full, bounded, free, capped, shaped); + int full = Any.Int32(); + int bounded = Any.Int32().Between(1, 1000); + string free = Any.String(); + string capped = Any.String().NonEmpty().WithMaxLength(50); + string shaped = Any.String().StartingWith("ORD-").WithLength(12); + long wide = Any.Int64(); + ulong unsigned = Any.UInt64(); + double real = Any.Double().Between(0d, 1000d); + decimal exact = Any.Decimal().Between(0m, 1000m); + bool flag = Any.Bool(); + Guid id = Any.Guid(); + char letter = Any.Char(); + TimeSpan span = Any.TimeSpan(); + DateTime instant = Any.DateTime(); + Int128 huge = Any.Int128(); + Half tiny = Any.Half(); + + return string.Join("|", full, bounded, free, capped, shaped, + wide, unsigned, real, exact, flag, id, letter, + span.Ticks, instant.Ticks, huge, tiny); } #endregion diff --git a/Dummies/Any.cs b/Dummies/Any.cs index e817a6ec..cd2fef83 100644 --- a/Dummies/Any.cs +++ b/Dummies/Any.cs @@ -56,9 +56,213 @@ public static AnyString String() { /// /// An integer generator to constrain fluently. public static AnyInt32 Int32() { - return new AnyInt32(AmbientRandomSource.Instance, Int32Spec.Unconstrained); + return AnyInt32.Create(AmbientRandomSource.Instance); } + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnySByte SByte() { + return AnySByte.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyByte Byte() { + return AnyByte.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyInt16 Int16() { + return AnyInt16.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyUInt16 UInt16() { + return AnyUInt16.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyUInt32 UInt32() { + return AnyUInt32.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyInt64 Int64() { + return AnyInt64.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyUInt64 UInt64() { + return AnyUInt64.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// full range unless constrained, negative durations included. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyTimeSpan TimeSpan() { + return AnyTimeSpan.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// any representable instant unless constrained; generated values carry Utc kind. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyDateTime DateTime() { + return AnyDateTime.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// any representable instant unless constrained; generated values carry a zero (UTC) offset. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyDateTimeOffset DateTimeOffset() { + return AnyDateTimeOffset.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// finite values only — NaN and infinities are never generated. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyDouble Double() { + return AnyDouble.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// finite values only — NaN and infinities are never generated. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnySingle Single() { + return AnySingle.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public static AnyDecimal Decimal() { + return AnyDecimal.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context — an even coin + /// flip unless pinned with True() or False(). + /// + /// A generator to constrain fluently. + public static AnyBool Bool() { + return AnyBool.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context — unlike + /// , reproducible inside an Any.Reproducibly(...) run, and for every + /// practical purpose never empty. + /// + /// A generator to constrain fluently. + public static AnyGuid Guid() { + return AnyGuid.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context — + /// uniformly across the enum's declared members, never an undeclared numeric value. + /// + /// The enum type to draw values from. + /// A generator to constrain fluently. + /// Thrown when declares no members. + public static AnyEnum Enum() + where TEnum : struct, Enum { + return AnyEnum.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context — ASCII letters + /// and digits unless constrained, mirroring 's character families. + /// + /// A generator to constrain fluently. + public static AnyChar Char() { + return AnyChar.Create(AmbientRandomSource.Instance); + } + +#if NET8_0_OR_GREATER + /// + /// Starts an arbitrary generator drawing from the ambient random context — any + /// representable date unless constrained. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public static AnyDateOnly DateOnly() { + return AnyDateOnly.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context — any + /// time of day unless constrained. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public static AnyTimeOnly TimeOnly() { + return AnyTimeOnly.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context — full + /// range unless constrained. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public static AnyInt128 Int128() { + return AnyInt128.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context — full + /// range unless constrained. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public static AnyUInt128 UInt128() { + return AnyUInt128.Create(AmbientRandomSource.Instance); + } + + /// + /// Starts an arbitrary generator drawing from the ambient random context — finite + /// values only. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public static AnyHalf Half() { + return AnyHalf.Create(AmbientRandomSource.Instance); + } +#endif + /// /// Creates an isolated, deterministic generation context: every generator created from it draws from a /// dedicated source seeded with , independent of the ambient context. Two contexts diff --git a/Dummies/AnyBool.cs b/Dummies/AnyBool.cs new file mode 100644 index 00000000..6ce6c277 --- /dev/null +++ b/Dummies/AnyBool.cs @@ -0,0 +1,86 @@ +namespace Dummies; + +/// +/// A fluent generator of arbitrary values. True() and False() pin the value — +/// mostly useful for symmetry when a test sweeps cases — and contradictory pins fail eagerly with a +/// naming both sides, like every other generator. +/// +public sealed class AnyBool : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator bool(AnyBool generator) { + return generator.Generate(); + } + + internal static AnyBool Create(RandomSource source) { + return new AnyBool(source, null, null); + } + + private static string V(bool value) { + return value ? "true" : "false"; + } + + #endregion + + #region Fields declarations + + private readonly bool? _pinned; + private readonly string? _pinnedConstraint; + private readonly RandomSource _source; + + #endregion + + private AnyBool(RandomSource source, bool? pinned, string? pinnedConstraint) { + _source = source; + _pinned = pinned; + _pinnedConstraint = pinnedConstraint; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Pins the value to true. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyBool True() { + return Pin(true, "True()"); + } + + /// Pins the value to false. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyBool False() { + return Pin(false, "False()"); + } + + /// + /// Requires the value to differ from — which, for a boolean, pins it to the + /// opposite. The name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyBool DifferentFrom(bool value) { + return Pin(!value, $"DifferentFrom({V(value)})"); + } + + /// + public bool Generate() { + return _pinned ?? _source.Current.Random.Next(2) == 0; + } + + private AnyBool Pin(bool value, string applying) { + if (_pinnedConstraint is not null && _pinned != value) { + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_pinnedConstraint} already pins the value to {V(_pinned!.Value)}."); + } + + return new AnyBool(_source, value, applying); + } + +} diff --git a/Dummies/AnyByte.cs b/Dummies/AnyByte.cs new file mode 100644 index 00000000..c969cc63 --- /dev/null +++ b/Dummies/AnyByte.cs @@ -0,0 +1,167 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// +public sealed class AnyByte : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator byte(AnyByte generator) { + return generator.Generate(); + } + + internal static AnyByte Create(RandomSource source) { + return new AnyByte(source, OrdinalIntervalSpec.Unconstrained("Byte", ordinal => V(Val(ordinal)), Ord(byte.MinValue), Ord(byte.MaxValue))); + } + + private static ulong Ord(byte value) { + return value; + } + + private static byte Val(ulong ordinal) { + return (byte)ordinal; + } + + private static string V(byte value) { + return value.ToString(CultureInfo.InvariantCulture); + } + + private static string Join(byte[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyByte(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte Zero() { + return new AnyByte(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte NonZero() { + return new AnyByte(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte GreaterThan(byte value) { + return new AnyByte(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte GreaterThanOrEqualTo(byte value) { + return new AnyByte(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte LessThan(byte value) { + return new AnyByte(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte LessThanOrEqualTo(byte value) { + return new AnyByte(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte Between(byte minimum, byte maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyByte(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte OneOf(params byte[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyByte(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte Except(params byte[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyByte(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyByte DifferentFrom(byte value) { + return new AnyByte(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public byte Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} diff --git a/Dummies/AnyChar.cs b/Dummies/AnyChar.cs new file mode 100644 index 00000000..3486a5de --- /dev/null +++ b/Dummies/AnyChar.cs @@ -0,0 +1,201 @@ +namespace Dummies; + +/// +/// A fluent generator of arbitrary values. Unconstrained, it draws from ASCII letters and +/// digits — the same readable default as 's filler — and the constraints mirror the +/// string character families: , , , +/// , , plus / / +/// . A combination that empties the pool fails eagerly with a +/// . +/// +public sealed class AnyChar : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator char(AnyChar generator) { + return generator.Generate(); + } + + internal static AnyChar Create(RandomSource source) { + return new AnyChar(source, null, null, null, null, null, null, []); + } + + private static string V(char value) { + return $"'{value}'"; + } + + private static string Join(char[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly IReadOnlyList? _allowed; + private readonly string? _allowedConstraint; + private readonly List _pool; + private readonly LetterCasing? _casing; + private readonly string? _casingConstraint; + private readonly CharacterSet? _charset; + private readonly string? _charsetConstraint; + private readonly IReadOnlyList _excluded; + private readonly RandomSource _source; + + #endregion + + private AnyChar(RandomSource source, + CharacterSet? charset, string? charsetConstraint, + LetterCasing? casing, string? casingConstraint, + IReadOnlyList? allowed, string? allowedConstraint, + IReadOnlyList excluded) { + _source = source; + _charset = charset; + _charsetConstraint = charsetConstraint; + _casing = casing; + _casingConstraint = casingConstraint; + _allowed = allowed; + _allowedConstraint = allowedConstraint; + _excluded = excluded; + // Materialized once here — "constrain once, draw many": Generate never refilters the pool. The full + // constant pool is the unconstrained start; MatchesCharset narrows it, so no per-charset pre-narrowing + // is needed. + IEnumerable candidates = allowed ?? (IEnumerable)(CharacterPools.UpperLetters + CharacterPools.LowerLetters + CharacterPools.Digits); + _pool = candidates.Where(character => MatchesCharset(character) && MatchesCasing(character) && !excluded.Contains(character)).ToList(); + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Restricts the character to ASCII letters only. Declared once per generator. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyChar Alpha() { + return WithCharset(CharacterSet.Alpha, "Alpha()"); + } + + /// Restricts the character to ASCII digits only. Declared once per generator. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyChar Numeric() { + return WithCharset(CharacterSet.Numeric, "Numeric()"); + } + + /// Restricts the character to ASCII letters and digits only. Declared once per generator. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyChar AlphaNumeric() { + return WithCharset(CharacterSet.AlphaNumeric, "AlphaNumeric()"); + } + + /// Requires an alphabetic character to be lowercase. Declared once per generator. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyChar LowerCase() { + return WithCasing(LetterCasing.Lower, "LowerCase()"); + } + + /// Requires an alphabetic character to be uppercase. Declared once per generator. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyChar UpperCase() { + return WithCasing(LetterCasing.Upper, "UpperCase()"); + } + + /// Requires the character to be one of the supplied values. Declared once per generator. + /// The allowed characters; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyChar OneOf(params char[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + string constraint = $"OneOf({Join(values)})"; + if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {constraint} because {_allowedConstraint} is already defined."); } + + return Validated(new AnyChar(_source, _charset, _charsetConstraint, _casing, _casingConstraint, values.Distinct().ToArray(), constraint, _excluded), constraint); + } + + /// Requires the character to be none of the supplied values. + /// The forbidden characters. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyChar Except(params char[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return WithExcluded(values, $"Except({Join(values)})"); + } + + /// + /// Requires the character to differ from — typically an existing value the test + /// already holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The character the generated character must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyChar DifferentFrom(char value) { + return WithExcluded([value], $"DifferentFrom({V(value)})"); + } + + /// + public char Generate() { + return _pool[_source.Current.Random.Next(_pool.Count)]; + } + + private AnyChar WithCharset(CharacterSet charset, string applying) { + if (_charsetConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_charsetConstraint} is already defined."); } + + return Validated(new AnyChar(_source, charset, applying, _casing, _casingConstraint, _allowed, _allowedConstraint, _excluded), applying); + } + + private AnyChar WithCasing(LetterCasing casing, string applying) { + if (_casingConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_casingConstraint} is already defined."); } + + return Validated(new AnyChar(_source, _charset, _charsetConstraint, casing, applying, _allowed, _allowedConstraint, _excluded), applying); + } + + private AnyChar WithExcluded(char[] values, string applying) { + List excluded = new(_excluded); + excluded.AddRange(values); + + return Validated(new AnyChar(_source, _charset, _charsetConstraint, _casing, _casingConstraint, _allowed, _allowedConstraint, excluded), applying); + } + + private AnyChar Validated(AnyChar candidate, string applying) { + if (candidate._pool.Count > 0) { return candidate; } + + string pool = candidate._allowedConstraint is null + ? "no character remains in the pool the declared constraints allow" + : $"no character {candidate._allowedConstraint} allows satisfies the constraints already defined"; + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {pool}."); + } + + private bool MatchesCharset(char character) { + return _charset switch { + CharacterSet.Alpha => CharacterPools.IsAsciiLetter(character), + CharacterSet.Numeric => CharacterPools.IsAsciiDigit(character), + CharacterSet.AlphaNumeric => CharacterPools.IsAsciiLetter(character) || CharacterPools.IsAsciiDigit(character), + _ => true + }; + } + + private bool MatchesCasing(char character) { + return _casing switch { + LetterCasing.Lower => character is not (>= 'A' and <= 'Z'), + LetterCasing.Upper => character is not (>= 'a' and <= 'z'), + _ => true + }; + } + +} diff --git a/Dummies/AnyContext.cs b/Dummies/AnyContext.cs index 38e88bc4..dddb9bae 100644 --- a/Dummies/AnyContext.cs +++ b/Dummies/AnyContext.cs @@ -49,7 +49,211 @@ public AnyString String() { /// /// An integer generator to constrain fluently. public AnyInt32 Int32() { - return new AnyInt32(_source, Int32Spec.Unconstrained); + return AnyInt32.Create(_source); } + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnySByte SByte() { + return AnySByte.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyByte Byte() { + return AnyByte.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyInt16 Int16() { + return AnyInt16.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyUInt16 UInt16() { + return AnyUInt16.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyUInt32 UInt32() { + return AnyUInt32.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyInt64 Int64() { + return AnyInt64.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyUInt64 UInt64() { + return AnyUInt64.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// full range unless constrained, negative durations included. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyTimeSpan TimeSpan() { + return AnyTimeSpan.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// any representable instant unless constrained; generated values carry Utc kind. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyDateTime DateTime() { + return AnyDateTime.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// any representable instant unless constrained; generated values carry a zero (UTC) offset. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyDateTimeOffset DateTimeOffset() { + return AnyDateTimeOffset.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// finite values only — NaN and infinities are never generated. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyDouble Double() { + return AnyDouble.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// finite values only — NaN and infinities are never generated. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnySingle Single() { + return AnySingle.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context — deterministic under this context's seed: + /// full range unless constrained. Same constraint algebra as . + /// + /// A generator to constrain fluently. + public AnyDecimal Decimal() { + return AnyDecimal.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — an even coin + /// flip unless pinned with True() or False(). + /// + /// A generator to constrain fluently. + public AnyBool Bool() { + return AnyBool.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — unlike + /// , reproducible inside an Any.Reproducibly(...) run, and for every + /// practical purpose never empty. + /// + /// A generator to constrain fluently. + public AnyGuid Guid() { + return AnyGuid.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — + /// uniformly across the enum's declared members, never an undeclared numeric value. + /// + /// The enum type to draw values from. + /// A generator to constrain fluently. + /// Thrown when declares no members. + public AnyEnum Enum() + where TEnum : struct, Enum { + return AnyEnum.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — ASCII letters + /// and digits unless constrained, mirroring 's character families. + /// + /// A generator to constrain fluently. + public AnyChar Char() { + return AnyChar.Create(_source); + } + +#if NET8_0_OR_GREATER + /// + /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — any + /// representable date unless constrained. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public AnyDateOnly DateOnly() { + return AnyDateOnly.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — any + /// time of day unless constrained. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public AnyTimeOnly TimeOnly() { + return AnyTimeOnly.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — full + /// range unless constrained. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public AnyInt128 Int128() { + return AnyInt128.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — full + /// range unless constrained. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public AnyUInt128 UInt128() { + return AnyUInt128.Create(_source); + } + + /// + /// Starts an arbitrary generator drawing from this context (deterministic under this context's seed) — finite + /// values only. Net8.0 target only, like the type itself. + /// + /// A generator to constrain fluently. + public AnyHalf Half() { + return AnyHalf.Create(_source); + } +#endif + } diff --git a/Dummies/AnyDateOnly.cs b/Dummies/AnyDateOnly.cs new file mode 100644 index 00000000..204dcc59 --- /dev/null +++ b/Dummies/AnyDateOnly.cs @@ -0,0 +1,158 @@ +#if NET8_0_OR_GREATER +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// Available on the net8.0 target only, like the type itself. There is deliberately no clock-relative +/// constraint: a reproducible test pins its reference dates explicitly with and +/// . +/// +public sealed class AnyDateOnly : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator DateOnly(AnyDateOnly generator) { + return generator.Generate(); + } + + internal static AnyDateOnly Create(RandomSource source) { + return new AnyDateOnly(source, OrdinalIntervalSpec.Unconstrained("DateOnly", ordinal => V(Val(ordinal)), Ord(DateOnly.MinValue), Ord(DateOnly.MaxValue))); + } + + private static ulong Ord(DateOnly value) { + return (ulong)value.DayNumber; + } + + private static DateOnly Val(ulong ordinal) { + return DateOnly.FromDayNumber((int)ordinal); + } + + private static string V(DateOnly value) { + return value.ToString("O", CultureInfo.InvariantCulture); + } + + private static string Join(DateOnly[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyDateOnly(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a date strictly after . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateOnly After(DateOnly date) { + return new AnyDateOnly(_source, _spec.WithMinimumAbove(Ord(date), $"After({V(date)})")); + } + + /// Requires a date at or after . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateOnly AfterOrEqualTo(DateOnly date) { + return new AnyDateOnly(_source, _spec.WithMinimum(Ord(date), $"AfterOrEqualTo({V(date)})")); + } + + /// Requires a date strictly before . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateOnly Before(DateOnly date) { + return new AnyDateOnly(_source, _spec.WithMaximumBelow(Ord(date), $"Before({V(date)})")); + } + + /// Requires a date at or before . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateOnly BeforeOrEqualTo(DateOnly date) { + return new AnyDateOnly(_source, _spec.WithMaximum(Ord(date), $"BeforeOrEqualTo({V(date)})")); + } + + /// Requires a date within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is after . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateOnly Between(DateOnly start, DateOnly end) { + if (start > end) { throw new ArgumentException($"The start ({V(start)}) must be at or before the end ({V(end)}).", nameof(start)); } + + string constraint = $"Between({V(start)}, {V(end)})"; + + return new AnyDateOnly(_source, _spec.WithMinimum(Ord(start), constraint).WithMaximum(Ord(end), constraint)); + } + + /// Requires the date to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateOnly OneOf(params DateOnly[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyDateOnly(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the date to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateOnly Except(params DateOnly[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyDateOnly(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the date to differ from — typically an existing value the test + /// already holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated date must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateOnly DifferentFrom(DateOnly value) { + return new AnyDateOnly(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public DateOnly Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} +#endif diff --git a/Dummies/AnyDateTime.cs b/Dummies/AnyDateTime.cs new file mode 100644 index 00000000..9a24e2c4 --- /dev/null +++ b/Dummies/AnyDateTime.cs @@ -0,0 +1,172 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as +/// : constraints express what the surrounding code requires of the value, never what the +/// test asserts; contradictory constraints fail eagerly with a +/// naming both sides; instances are immutable recipes, and each value is built to satisfy the constraints in one +/// draw. +/// +/// +/// Generated values carry ; constraints compare by , +/// ignoring the of the supplied bounds — exactly as 's own +/// comparison operators do. Values supplied to 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 and . +/// +public sealed class AnyDateTime : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is + /// expected. Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator DateTime(AnyDateTime generator) { + return generator.Generate(); + } + + internal static AnyDateTime Create(RandomSource source) { + return new AnyDateTime(source, OrdinalIntervalSpec.Unconstrained("DateTime", ordinal => V(Val(ordinal)), Ord(DateTime.MinValue), Ord(DateTime.MaxValue))); + } + + private static ulong Ord(DateTime value) { + return (ulong)value.Ticks; + } + + private static DateTime Val(ulong ordinal) { + return new DateTime((long)ordinal, DateTimeKind.Utc); + } + + private static string V(DateTime value) { + return value.ToString("O", CultureInfo.InvariantCulture); + } + + private static string Join(DateTime[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly IReadOnlyDictionary? _allowedOriginals; + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyDateTime(RandomSource source, OrdinalIntervalSpec spec, IReadOnlyDictionary? allowedOriginals = null) { + _source = source; + _spec = spec; + _allowedOriginals = allowedOriginals; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires an instant strictly after . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTime After(DateTime instant) { + return new AnyDateTime(_source, _spec.WithMinimumAbove(Ord(instant), $"After({V(instant)})"), _allowedOriginals); + } + + /// Requires an instant at or after . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTime AfterOrEqualTo(DateTime instant) { + return new AnyDateTime(_source, _spec.WithMinimum(Ord(instant), $"AfterOrEqualTo({V(instant)})"), _allowedOriginals); + } + + /// Requires an instant strictly before . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTime Before(DateTime instant) { + return new AnyDateTime(_source, _spec.WithMaximumBelow(Ord(instant), $"Before({V(instant)})"), _allowedOriginals); + } + + /// Requires an instant at or before . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTime BeforeOrEqualTo(DateTime instant) { + return new AnyDateTime(_source, _spec.WithMaximum(Ord(instant), $"BeforeOrEqualTo({V(instant)})"), _allowedOriginals); + } + + /// Requires an instant within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is after . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTime Between(DateTime start, DateTime end) { + if (start > end) { throw new ArgumentException($"The start ({V(start)}) must be at or before the end ({V(end)}).", nameof(start)); } + + string constraint = $"Between({V(start)}, {V(end)})"; + + return new AnyDateTime(_source, _spec.WithMinimum(Ord(start), constraint).WithMaximum(Ord(end), constraint), _allowedOriginals); + } + + /// Requires the instant to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTime OneOf(params DateTime[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + // 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 originals = new(); + foreach (DateTime value in values) { + if (!originals.ContainsKey(Ord(value))) { originals.Add(Ord(value), value); } + } + + return new AnyDateTime(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})"), originals); + } + + /// Requires the instant to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTime Except(params DateTime[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyDateTime(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})"), _allowedOriginals); + } + + /// + /// Requires the instant to differ from — typically an existing value the test + /// already holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated instant must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTime DifferentFrom(DateTime value) { + return new AnyDateTime(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})"), _allowedOriginals); + } + + /// + public DateTime Generate() { + ulong ordinal = _spec.GenerateOrdinal(_source.Current.Random); + if (_allowedOriginals is not null && _allowedOriginals.TryGetValue(ordinal, out DateTime original)) { return original; } + + return Val(ordinal); + } + +} diff --git a/Dummies/AnyDateTimeOffset.cs b/Dummies/AnyDateTimeOffset.cs new file mode 100644 index 00000000..0d60c069 --- /dev/null +++ b/Dummies/AnyDateTimeOffset.cs @@ -0,0 +1,178 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as +/// : constraints express what the surrounding code requires of the value, never what the +/// test asserts; contradictory constraints fail eagerly with a +/// naming both sides; instances are immutable recipes, and each value is built to satisfy the constraints in one +/// draw. +/// +/// +/// Generated values carry offset (UTC); constraints compare by +/// — the instant, not the local rendering — exactly as +/// 's own comparison operators do. Values supplied to are +/// returned as given, offset included. There is deliberately no clock-relative constraint (no "in the +/// past/future"): a reproducible test pins its reference instants explicitly with and +/// . +/// +public sealed class AnyDateTimeOffset : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a + /// is expected. Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator DateTimeOffset(AnyDateTimeOffset generator) { + return generator.Generate(); + } + + internal static AnyDateTimeOffset Create(RandomSource source) { + return new AnyDateTimeOffset(source, OrdinalIntervalSpec.Unconstrained("DateTimeOffset", ordinal => V(Val(ordinal)), Ord(DateTimeOffset.MinValue), Ord(DateTimeOffset.MaxValue)), null); + } + + private static ulong Ord(DateTimeOffset value) { + return (ulong)value.UtcTicks; + } + + private static DateTimeOffset Val(ulong ordinal) { + return new DateTimeOffset((long)ordinal, TimeSpan.Zero); + } + + private static string V(DateTimeOffset value) { + return value.ToString("O", CultureInfo.InvariantCulture); + } + + private static string Join(DateTimeOffset[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly IReadOnlyDictionary? _allowedOriginals; + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyDateTimeOffset(RandomSource source, OrdinalIntervalSpec spec, IReadOnlyDictionary? allowedOriginals) { + _source = source; + _spec = spec; + _allowedOriginals = allowedOriginals; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires an instant strictly after . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTimeOffset After(DateTimeOffset instant) { + return new AnyDateTimeOffset(_source, _spec.WithMinimumAbove(Ord(instant), $"After({V(instant)})"), _allowedOriginals); + } + + /// Requires an instant at or after . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTimeOffset AfterOrEqualTo(DateTimeOffset instant) { + return new AnyDateTimeOffset(_source, _spec.WithMinimum(Ord(instant), $"AfterOrEqualTo({V(instant)})"), _allowedOriginals); + } + + /// Requires an instant strictly before . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTimeOffset Before(DateTimeOffset instant) { + return new AnyDateTimeOffset(_source, _spec.WithMaximumBelow(Ord(instant), $"Before({V(instant)})"), _allowedOriginals); + } + + /// Requires an instant at or before . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTimeOffset BeforeOrEqualTo(DateTimeOffset instant) { + return new AnyDateTimeOffset(_source, _spec.WithMaximum(Ord(instant), $"BeforeOrEqualTo({V(instant)})"), _allowedOriginals); + } + + /// Requires an instant within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is after . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTimeOffset Between(DateTimeOffset start, DateTimeOffset end) { + if (start > end) { throw new ArgumentException($"The start ({V(start)}) must be at or before the end ({V(end)}).", nameof(start)); } + + string constraint = $"Between({V(start)}, {V(end)})"; + + return new AnyDateTimeOffset(_source, _spec.WithMinimum(Ord(start), constraint).WithMaximum(Ord(end), constraint), _allowedOriginals); + } + + /// + /// Requires the instant to be one of the supplied values — returned as given, offset included. Declared once + /// per generator. + /// + /// The allowed values; duplicates (same instant) are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTimeOffset OneOf(params DateTimeOffset[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + // 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 originals = new(); + foreach (DateTimeOffset value in values) { + if (!originals.ContainsKey(Ord(value))) { originals.Add(Ord(value), value); } + } + + return new AnyDateTimeOffset(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})"), originals); + } + + /// Requires the instant to be none of the supplied values (compared by instant). + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTimeOffset Except(params DateTimeOffset[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyDateTimeOffset(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})"), _allowedOriginals); + } + + /// + /// Requires the instant to differ from (compared by instant) — typically an existing + /// value the test already holds. Semantically equivalent to ; the name carries the intent + /// at the call site. + /// + /// The value the generated instant must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDateTimeOffset DifferentFrom(DateTimeOffset value) { + return new AnyDateTimeOffset(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})"), _allowedOriginals); + } + + /// + public DateTimeOffset Generate() { + ulong ordinal = _spec.GenerateOrdinal(_source.Current.Random); + if (_allowedOriginals is not null && _allowedOriginals.TryGetValue(ordinal, out DateTimeOffset original)) { return original; } + + return Val(ordinal); + } + +} diff --git a/Dummies/AnyDecimal.cs b/Dummies/AnyDecimal.cs new file mode 100644 index 00000000..948adfcc --- /dev/null +++ b/Dummies/AnyDecimal.cs @@ -0,0 +1,176 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes. Exclusive bounds are expressed as the inclusive bound plus a point +/// exclusion, since has no next-representable-value ladder. +/// +public sealed class AnyDecimal : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is + /// expected. Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator decimal(AnyDecimal generator) { + return generator.Generate(); + } + + internal static AnyDecimal Create(RandomSource source) { + return new AnyDecimal(source, DecimalIntervalSpec.Unconstrained("Decimal", V)); + } + + private static string V(decimal value) { + return value.ToString(CultureInfo.InvariantCulture); + } + + private static string Join(decimal[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly DecimalIntervalSpec _spec; + + #endregion + + private AnyDecimal(RandomSource source, DecimalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a value strictly greater than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal Positive() { + return new AnyDecimal(_source, _spec.WithMinimumAbove(0m, "Positive()")); + } + + /// Requires a value strictly less than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal Negative() { + return new AnyDecimal(_source, _spec.WithMaximumBelow(0m, "Negative()")); + } + + /// Pins the value to exactly zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal Zero() { + return new AnyDecimal(_source, _spec.WithMinimum(0m, "Zero()").WithMaximum(0m, "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal NonZero() { + return new AnyDecimal(_source, _spec.WithExcluded([0m], "NonZero()")); + } + + /// Requires a value strictly greater than — the inclusive bound plus a point exclusion. + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal GreaterThan(decimal value) { + return new AnyDecimal(_source, _spec.WithMinimumAbove(value, $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal GreaterThanOrEqualTo(decimal value) { + return new AnyDecimal(_source, _spec.WithMinimum(value, $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than — the inclusive bound plus a point exclusion. + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal LessThan(decimal value) { + return new AnyDecimal(_source, _spec.WithMaximumBelow(value, $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal LessThanOrEqualTo(decimal value) { + return new AnyDecimal(_source, _spec.WithMaximum(value, $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal Between(decimal minimum, decimal maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyDecimal(_source, _spec.WithMinimum(minimum, constraint).WithMaximum(maximum, constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal OneOf(params decimal[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyDecimal(_source, _spec.WithAllowed(values, $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal Except(params decimal[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyDecimal(_source, _spec.WithExcluded(values, $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDecimal DifferentFrom(decimal value) { + return new AnyDecimal(_source, _spec.WithExcluded([value], $"DifferentFrom({V(value)})")); + } + + /// + public decimal Generate() { + SeededRandom current = _source.Current; + + return _spec.Generate(current.Random, current.Seed); + } + +} diff --git a/Dummies/AnyDouble.cs b/Dummies/AnyDouble.cs new file mode 100644 index 00000000..16f93810 --- /dev/null +++ b/Dummies/AnyDouble.cs @@ -0,0 +1,189 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted. +/// +public sealed class AnyDouble : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator double(AnyDouble generator) { + return generator.Generate(); + } + + internal static AnyDouble Create(RandomSource source) { + return new AnyDouble(source, ContinuousIntervalSpec.Unconstrained("Double", V, value => value, ContinuousIntervalSpec.NextUp, -double.MaxValue, double.MaxValue)); + } + + private static string V(double value) { + return value.ToString("R", CultureInfo.InvariantCulture); + } + + private static string Join(double[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly ContinuousIntervalSpec _spec; + + #endregion + + private AnyDouble(RandomSource source, ContinuousIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a value strictly greater than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble Positive() { + return new AnyDouble(_source, _spec.WithMinimumAbove(0d, "Positive()")); + } + + /// Requires a value strictly less than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble Negative() { + return new AnyDouble(_source, _spec.WithMaximumBelow(0d, "Negative()")); + } + + /// Pins the value to exactly zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble Zero() { + return new AnyDouble(_source, _spec.WithMinimum(0d, "Zero()").WithMaximum(0d, "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble NonZero() { + return new AnyDouble(_source, _spec.WithExcluded([0d], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble GreaterThan(double value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnyDouble(_source, _spec.WithMinimumAbove(value, $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble GreaterThanOrEqualTo(double value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnyDouble(_source, _spec.WithMinimum(value, $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble LessThan(double value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnyDouble(_source, _spec.WithMaximumBelow(value, $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble LessThanOrEqualTo(double value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnyDouble(_source, _spec.WithMaximum(value, $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when a bound is not finite or is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble Between(double minimum, double maximum) { + ContinuousIntervalSpec.EnsureFinite(minimum, nameof(minimum)); + ContinuousIntervalSpec.EnsureFinite(maximum, nameof(maximum)); + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyDouble(_source, _spec.WithMinimum(minimum, constraint).WithMaximum(maximum, constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty or contains a non-finite value. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble OneOf(params double[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + foreach (double value in values) { ContinuousIntervalSpec.EnsureFinite(value, nameof(values)); } + + return new AnyDouble(_source, _spec.WithAllowed(values, $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty or contains a non-finite value. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble Except(params double[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + foreach (double value in values) { ContinuousIntervalSpec.EnsureFinite(value, nameof(values)); } + + return new AnyDouble(_source, _spec.WithExcluded(values, $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyDouble DifferentFrom(double value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnyDouble(_source, _spec.WithExcluded([value], $"DifferentFrom({V(value)})")); + } + + /// + public double Generate() { + SeededRandom current = _source.Current; + + return _spec.Generate(current.Random, current.Seed); + } + +} diff --git a/Dummies/AnyEnum.cs b/Dummies/AnyEnum.cs new file mode 100644 index 00000000..d8a53464 --- /dev/null +++ b/Dummies/AnyEnum.cs @@ -0,0 +1,136 @@ +namespace Dummies; + +/// +/// A fluent generator of arbitrary values, drawn uniformly from the enum's +/// declared members — never from undeclared numeric values. Constraints narrow the pool +/// (, , ), and a combination that empties it +/// fails eagerly with a naming both sides. +/// +/// The enum type to draw values from. +public sealed class AnyEnum : IAny, IHasRandomSource + where TEnum : struct, Enum { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a + /// is expected. Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator TEnum(AnyEnum generator) { + return generator.Generate(); + } + + // The declared-members set of an enum type is a process constant; cached once per closed generic type + // instead of reflecting on every Any.Enum() call. + private static readonly TEnum[] Declared = ((TEnum[])Enum.GetValues(typeof(TEnum))).Distinct().ToArray(); + + internal static AnyEnum Create(RandomSource source) { + if (Declared.Length == 0) { + throw new AnyGenerationException($"Cannot generate an arbitrary {typeof(TEnum).Name} value because the enum declares no members."); + } + + return new AnyEnum(source, Declared, null, null, []); + } + + private static string V(TEnum value) { + return value.ToString(); + } + + private static string Join(TEnum[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly IReadOnlyList? _allowed; + private readonly string? _allowedConstraint; + private readonly IReadOnlyList _declared; + private readonly IReadOnlyList _excluded; + private readonly List _pool; + private readonly RandomSource _source; + + #endregion + + private AnyEnum(RandomSource source, IReadOnlyList declared, + IReadOnlyList? allowed, string? allowedConstraint, IReadOnlyList excluded) { + _source = source; + _declared = declared; + _allowed = allowed; + _allowedConstraint = allowedConstraint; + _excluded = excluded; + // Materialized once here — "constrain once, draw many": Generate never refilters the pool. + _pool = (allowed ?? declared).Where(value => !excluded.Contains(value)).ToList(); + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires the value to be one of the supplied members. Declared once per generator. + /// The allowed members; duplicates are ignored. Every value must be a declared member of — the generator never yields undeclared numeric values, not even explicitly supplied ones. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty or contains a value that is not a declared member. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyEnum OneOf(params TEnum[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + foreach (TEnum value in values) { + if (!_declared.Contains(value)) { throw new ArgumentException($"The value {value} is not a declared member of {typeof(TEnum).Name}: the generator only ever yields declared members.", nameof(values)); } + } + + string constraint = $"OneOf({Join(values)})"; + if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {constraint} because {_allowedConstraint} is already defined."); } + + return Validated(new AnyEnum(_source, _declared, values.Distinct().ToArray(), constraint, _excluded), constraint); + } + + /// Requires the value to be none of the supplied members. + /// The forbidden members. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyEnum Except(params TEnum[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return WithExcluded(values, $"Except({Join(values)})"); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The member the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyEnum DifferentFrom(TEnum value) { + return WithExcluded([value], $"DifferentFrom({V(value)})"); + } + + /// + public TEnum Generate() { + return _pool[_source.Current.Random.Next(_pool.Count)]; + } + + private AnyEnum WithExcluded(TEnum[] values, string applying) { + List excluded = new(_excluded); + excluded.AddRange(values); + + return Validated(new AnyEnum(_source, _declared, _allowed, _allowedConstraint, excluded), applying); + } + + private AnyEnum Validated(AnyEnum candidate, string applying) { + if (candidate._pool.Count > 0) { return candidate; } + + string pool = candidate._allowedConstraint is null + ? $"no declared {typeof(TEnum).Name} member remains available" + : $"no value {candidate._allowedConstraint} allows remains available"; + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {pool}."); + } + +} diff --git a/Dummies/AnyGuid.cs b/Dummies/AnyGuid.cs new file mode 100644 index 00000000..2bd8b477 --- /dev/null +++ b/Dummies/AnyGuid.cs @@ -0,0 +1,167 @@ +namespace Dummies; + +/// +/// A fluent generator of arbitrary values, drawn from the seedable source — unlike +/// , a generated identifier is reproducible inside an +/// Any.Reproducibly(...) run. An unconstrained draw is, for every practical purpose, never +/// ; chain to make that requirement explicit, or +/// to pin the empty identifier. Contradictory constraints fail eagerly with a +/// naming both sides. +/// +public sealed class AnyGuid : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator Guid(AnyGuid generator) { + return generator.Generate(); + } + + internal static AnyGuid Create(RandomSource source) { + return new AnyGuid(source, null, null, null, null, []); + } + + private static string V(Guid value) { + return value.ToString("D"); + } + + private static string Join(Guid[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly IReadOnlyList? _allowed; + private readonly string? _allowedConstraint; + private readonly List? _effectiveAllowed; + private readonly IReadOnlyList _excluded; + private readonly Guid? _pinned; + private readonly string? _pinnedConstraint; + private readonly RandomSource _source; + + #endregion + + private AnyGuid(RandomSource source, Guid? pinned, string? pinnedConstraint, + IReadOnlyList? allowed, string? allowedConstraint, IReadOnlyList excluded) { + _source = source; + _pinned = pinned; + _pinnedConstraint = pinnedConstraint; + _allowed = allowed; + _allowedConstraint = allowedConstraint; + _excluded = excluded; + // Materialized once here — "constrain once, draw many": Generate never refilters the allow-list. + _effectiveAllowed = allowed?.Where(value => !excluded.Contains(value)).ToList(); + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires an identifier different from . + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyGuid NonEmpty() { + return WithExcluded([Guid.Empty], "NonEmpty()"); + } + + /// Pins the identifier to . + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyGuid Empty() { + return Validated(new AnyGuid(_source, Guid.Empty, "Empty()", _allowed, _allowedConstraint, _excluded), "Empty()"); + } + + /// Requires the identifier to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyGuid OneOf(params Guid[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + string constraint = $"OneOf({Join(values)})"; + if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {constraint} because {_allowedConstraint} is already defined."); } + + return Validated(new AnyGuid(_source, _pinned, _pinnedConstraint, values.Distinct().ToArray(), constraint, _excluded), constraint); + } + + /// Requires the identifier to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyGuid Except(params Guid[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return WithExcluded(values, $"Except({Join(values)})"); + } + + /// + /// Requires the identifier to differ from — typically an existing value the test + /// already holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated identifier must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyGuid DifferentFrom(Guid value) { + return WithExcluded([value], $"DifferentFrom({V(value)})"); + } + + /// + public Guid Generate() { + if (_pinned is Guid pinned) { return pinned; } + + Random random = _source.Current.Random; + if (_effectiveAllowed is not null) { + return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; + } + + byte[] bytes = new byte[16]; + random.NextBytes(bytes); + Guid candidate = new(bytes); + // Colliding with an excluded identifier has probability ~2^-122 per draw; walking the last byte is a + // deterministic, bounded escape — not a retry loop. + while (_excluded.Contains(candidate)) { + bytes[15]++; + candidate = new Guid(bytes); + } + + return candidate; + } + + private AnyGuid WithExcluded(Guid[] values, string applying) { + List excluded = new(_excluded); + excluded.AddRange(values); + + return Validated(new AnyGuid(_source, _pinned, _pinnedConstraint, _allowed, _allowedConstraint, excluded), applying); + } + + private AnyGuid Validated(AnyGuid candidate, string applying) { + if (candidate._pinned is Guid pinned) { + if (candidate._excluded.Contains(pinned)) { + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {candidate._pinnedConstraint} already pins the value to {V(pinned)}, which the exclusions forbid."); + } + if (candidate._allowed is not null && !candidate._allowed.Contains(pinned)) { + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {candidate._pinnedConstraint} already pins the value to {V(pinned)}, which {candidate._allowedConstraint} does not allow."); + } + + return candidate; + } + + if (candidate._effectiveAllowed is not null && candidate._effectiveAllowed.Count == 0) { + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no value {candidate._allowedConstraint} allows remains available."); + } + + return candidate; + } + +} diff --git a/Dummies/AnyHalf.cs b/Dummies/AnyHalf.cs new file mode 100644 index 00000000..2f27efc2 --- /dev/null +++ b/Dummies/AnyHalf.cs @@ -0,0 +1,207 @@ +#if NET8_0_OR_GREATER +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted. Available on +/// the net8.0 target only, like the type itself. +/// +public sealed class AnyHalf : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator Half(AnyHalf generator) { + return generator.Generate(); + } + + internal static AnyHalf Create(RandomSource source) { + return new AnyHalf(source, ContinuousIntervalSpec.Unconstrained("Half", value => V((Half)value), value => (double)(Half)value, value => NextUp((Half)value), -(double)Half.MaxValue, (double)Half.MaxValue)); + } + + private static string V(Half value) { + return value.ToString(null, CultureInfo.InvariantCulture); + } + + private static string Join(Half[] values) { + return string.Join(", ", values.Select(V)); + } + + /// The next representable half above — the exclusive-bound arithmetic. + private static double NextUp(Half value) { + short bits = BitConverter.HalfToInt16Bits(value); + if (bits >= 0) { bits++; } else if (bits == short.MinValue) { bits = 1; } else { bits--; } + + Half next = BitConverter.Int16BitsToHalf(bits); + + return Half.IsInfinity(next) ? double.PositiveInfinity : (double)next; + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly ContinuousIntervalSpec _spec; + + #endregion + + private AnyHalf(RandomSource source, ContinuousIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a value strictly greater than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf Positive() { + return new AnyHalf(_source, _spec.WithMinimumAbove(0d, "Positive()")); + } + + /// Requires a value strictly less than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf Negative() { + return new AnyHalf(_source, _spec.WithMaximumBelow(0d, "Negative()")); + } + + /// Pins the value to exactly zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf Zero() { + return new AnyHalf(_source, _spec.WithMinimum(0d, "Zero()").WithMaximum(0d, "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf NonZero() { + return new AnyHalf(_source, _spec.WithExcluded([0d], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf GreaterThan(Half value) { + ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); + + return new AnyHalf(_source, _spec.WithMinimumAbove((double)value, $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf GreaterThanOrEqualTo(Half value) { + ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); + + return new AnyHalf(_source, _spec.WithMinimum((double)value, $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf LessThan(Half value) { + ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); + + return new AnyHalf(_source, _spec.WithMaximumBelow((double)value, $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf LessThanOrEqualTo(Half value) { + ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); + + return new AnyHalf(_source, _spec.WithMaximum((double)value, $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when a bound is not finite or is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf Between(Half minimum, Half maximum) { + ContinuousIntervalSpec.EnsureFinite((double)minimum, nameof(minimum)); + ContinuousIntervalSpec.EnsureFinite((double)maximum, nameof(maximum)); + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyHalf(_source, _spec.WithMinimum((double)minimum, constraint).WithMaximum((double)maximum, constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty or contains a non-finite value. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf OneOf(params Half[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + foreach (Half value in values) { ContinuousIntervalSpec.EnsureFinite((double)value, nameof(values)); } + + return new AnyHalf(_source, _spec.WithAllowed(values.Select(value => (double)value).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty or contains a non-finite value. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf Except(params Half[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + foreach (Half value in values) { ContinuousIntervalSpec.EnsureFinite((double)value, nameof(values)); } + + return new AnyHalf(_source, _spec.WithExcluded(values.Select(value => (double)value).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyHalf DifferentFrom(Half value) { + ContinuousIntervalSpec.EnsureFinite((double)value, nameof(value)); + + return new AnyHalf(_source, _spec.WithExcluded([(double)value], $"DifferentFrom({V(value)})")); + } + + /// + public Half Generate() { + SeededRandom current = _source.Current; + + return (Half)_spec.Generate(current.Random, current.Seed); + } + +} +#endif diff --git a/Dummies/AnyInt128.cs b/Dummies/AnyInt128.cs new file mode 100644 index 00000000..b40047b0 --- /dev/null +++ b/Dummies/AnyInt128.cs @@ -0,0 +1,184 @@ +#if NET8_0_OR_GREATER +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// Available on the net8.0 target only, like the type itself. +/// +public sealed class AnyInt128 : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator Int128(AnyInt128 generator) { + return generator.Generate(); + } + + internal static AnyInt128 Create(RandomSource source) { + return new AnyInt128(source, WideIntervalSpec.Unconstrained("Int128", ordinal => V(Val(ordinal)), Ord(Int128.MinValue), Ord(Int128.MaxValue))); + } + + private static UInt128 Ord(Int128 value) { + return unchecked((UInt128)value) ^ (UInt128.One << 127); + } + + private static Int128 Val(UInt128 ordinal) { + return unchecked((Int128)(ordinal ^ (UInt128.One << 127))); + } + + private static string V(Int128 value) { + return value.ToString(null, CultureInfo.InvariantCulture); + } + + private static string Join(Int128[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly WideIntervalSpec _spec; + + #endregion + + private AnyInt128(RandomSource source, WideIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a value strictly greater than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 Positive() { + return new AnyInt128(_source, _spec.WithMinimum(Ord(1), "Positive()")); + } + + /// Requires a value strictly less than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 Negative() { + return new AnyInt128(_source, _spec.WithMaximum(Ord(-1), "Negative()")); + } + + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 Zero() { + return new AnyInt128(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 NonZero() { + return new AnyInt128(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 GreaterThan(Int128 value) { + return new AnyInt128(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 GreaterThanOrEqualTo(Int128 value) { + return new AnyInt128(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 LessThan(Int128 value) { + return new AnyInt128(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 LessThanOrEqualTo(Int128 value) { + return new AnyInt128(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 Between(Int128 minimum, Int128 maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyInt128(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 OneOf(params Int128[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyInt128(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 Except(params Int128[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyInt128(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt128 DifferentFrom(Int128 value) { + return new AnyInt128(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public Int128 Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} +#endif diff --git a/Dummies/AnyInt16.cs b/Dummies/AnyInt16.cs new file mode 100644 index 00000000..b47188f3 --- /dev/null +++ b/Dummies/AnyInt16.cs @@ -0,0 +1,181 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// +public sealed class AnyInt16 : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator short(AnyInt16 generator) { + return generator.Generate(); + } + + internal static AnyInt16 Create(RandomSource source) { + return new AnyInt16(source, OrdinalIntervalSpec.Unconstrained("Int16", ordinal => V(Val(ordinal)), Ord(short.MinValue), Ord(short.MaxValue))); + } + + private static ulong Ord(short value) { + return OrdinalMapping.FromInt64(value); + } + + private static short Val(ulong ordinal) { + return (short)OrdinalMapping.ToInt64(ordinal); + } + + private static string V(short value) { + return value.ToString(CultureInfo.InvariantCulture); + } + + private static string Join(short[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyInt16(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a value strictly greater than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 Positive() { + return new AnyInt16(_source, _spec.WithMinimum(Ord(1), "Positive()")); + } + + /// Requires a value strictly less than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 Negative() { + return new AnyInt16(_source, _spec.WithMaximum(Ord(-1), "Negative()")); + } + + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 Zero() { + return new AnyInt16(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 NonZero() { + return new AnyInt16(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 GreaterThan(short value) { + return new AnyInt16(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 GreaterThanOrEqualTo(short value) { + return new AnyInt16(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 LessThan(short value) { + return new AnyInt16(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 LessThanOrEqualTo(short value) { + return new AnyInt16(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 Between(short minimum, short maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyInt16(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 OneOf(params short[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyInt16(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 Except(params short[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyInt16(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt16 DifferentFrom(short value) { + return new AnyInt16(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public short Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} diff --git a/Dummies/AnyInt32.cs b/Dummies/AnyInt32.cs index 26c79827..f6718454 100644 --- a/Dummies/AnyInt32.cs +++ b/Dummies/AnyInt32.cs @@ -42,56 +42,68 @@ public static implicit operator int(AnyInt32 generator) { return generator.Generate(); } + internal static AnyInt32 Create(RandomSource source) { + return new AnyInt32(source, OrdinalIntervalSpec.Unconstrained("Int32", ordinal => V(Val(ordinal)), Ord(int.MinValue), Ord(int.MaxValue))); + } + + private static ulong Ord(int value) { + return OrdinalMapping.FromInt64(value); + } + + private static int Val(ulong ordinal) { + return (int)OrdinalMapping.ToInt64(ordinal); + } + private static string V(int value) { return value.ToString(CultureInfo.InvariantCulture); } private static string Join(int[] values) { - return string.Join(", ", values.Select(value => value.ToString(CultureInfo.InvariantCulture))); + return string.Join(", ", values.Select(V)); } #endregion #region Fields declarations - private readonly RandomSource _source; - private readonly Int32Spec _spec; + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; #endregion - internal AnyInt32(RandomSource source, Int32Spec spec) { + private AnyInt32(RandomSource source, OrdinalIntervalSpec spec) { _source = source; _spec = spec; } - RandomSource IHasRandomSource.Source => _source; + RandomSource? IHasRandomSource.Source => _source; /// Requires a value strictly greater than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyInt32 Positive() { - return new AnyInt32(_source, _spec.WithMinimum(1, "Positive()")); + return new AnyInt32(_source, _spec.WithMinimum(Ord(1), "Positive()")); } /// Requires a value strictly less than zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyInt32 Negative() { - return new AnyInt32(_source, _spec.WithMaximum(-1, "Negative()")); + return new AnyInt32(_source, _spec.WithMaximum(Ord(-1), "Negative()")); } /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyInt32 Zero() { - return new AnyInt32(_source, _spec.WithMinimum(0, "Zero()").WithMaximum(0, "Zero()")); + return new AnyInt32(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); } /// Requires a value different from zero. /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyInt32 NonZero() { - return new AnyInt32(_source, _spec.WithExcluded([0], "NonZero()")); + return new AnyInt32(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); } /// Requires a value strictly greater than . @@ -99,7 +111,7 @@ public AnyInt32 NonZero() { /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyInt32 GreaterThan(int value) { - return new AnyInt32(_source, _spec.WithMinimum((long)value + 1, $"GreaterThan({V(value)})")); + return new AnyInt32(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); } /// Requires a value greater than or equal to . @@ -107,7 +119,7 @@ public AnyInt32 GreaterThan(int value) { /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyInt32 GreaterThanOrEqualTo(int value) { - return new AnyInt32(_source, _spec.WithMinimum(value, $"GreaterThanOrEqualTo({V(value)})")); + return new AnyInt32(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); } /// Requires a value strictly less than . @@ -115,7 +127,7 @@ public AnyInt32 GreaterThanOrEqualTo(int value) { /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyInt32 LessThan(int value) { - return new AnyInt32(_source, _spec.WithMaximum((long)value - 1, $"LessThan({V(value)})")); + return new AnyInt32(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); } /// Requires a value less than or equal to . @@ -123,7 +135,7 @@ public AnyInt32 LessThan(int value) { /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyInt32 LessThanOrEqualTo(int value) { - return new AnyInt32(_source, _spec.WithMaximum(value, $"LessThanOrEqualTo({V(value)})")); + return new AnyInt32(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); } /// Requires a value within the inclusive range [, ]. @@ -137,7 +149,7 @@ public AnyInt32 Between(int minimum, int maximum) { string constraint = $"Between({V(minimum)}, {V(maximum)})"; - return new AnyInt32(_source, _spec.WithMinimum(minimum, constraint).WithMaximum(maximum, constraint)); + return new AnyInt32(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); } /// Requires the value to be one of the supplied values. Declared once per generator. @@ -150,7 +162,7 @@ public AnyInt32 OneOf(params int[] values) { if (values is null) { throw new ArgumentNullException(nameof(values)); } if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - return new AnyInt32(_source, _spec.WithAllowed(values, $"OneOf({Join(values)})")); + return new AnyInt32(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); } /// Requires the value to be none of the supplied values. @@ -163,7 +175,7 @@ public AnyInt32 Except(params int[] values) { if (values is null) { throw new ArgumentNullException(nameof(values)); } if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } - return new AnyInt32(_source, _spec.WithExcluded(values, $"Except({Join(values)})")); + return new AnyInt32(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); } /// @@ -174,12 +186,12 @@ public AnyInt32 Except(params int[] values) { /// A new generator carrying the added constraint. /// Thrown when the constraint contradicts a constraint already declared. public AnyInt32 DifferentFrom(int value) { - return new AnyInt32(_source, _spec.WithExcluded([value], $"DifferentFrom({V(value)})")); + return new AnyInt32(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); } /// public int Generate() { - return _spec.Generate(_source.Current.Random); + return Val(_spec.GenerateOrdinal(_source.Current.Random)); } } diff --git a/Dummies/AnyInt64.cs b/Dummies/AnyInt64.cs new file mode 100644 index 00000000..77b99519 --- /dev/null +++ b/Dummies/AnyInt64.cs @@ -0,0 +1,181 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// +public sealed class AnyInt64 : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator long(AnyInt64 generator) { + return generator.Generate(); + } + + internal static AnyInt64 Create(RandomSource source) { + return new AnyInt64(source, OrdinalIntervalSpec.Unconstrained("Int64", ordinal => V(Val(ordinal)), Ord(long.MinValue), Ord(long.MaxValue))); + } + + private static ulong Ord(long value) { + return OrdinalMapping.FromInt64(value); + } + + private static long Val(ulong ordinal) { + return OrdinalMapping.ToInt64(ordinal); + } + + private static string V(long value) { + return value.ToString(CultureInfo.InvariantCulture); + } + + private static string Join(long[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyInt64(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a value strictly greater than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 Positive() { + return new AnyInt64(_source, _spec.WithMinimum(Ord(1), "Positive()")); + } + + /// Requires a value strictly less than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 Negative() { + return new AnyInt64(_source, _spec.WithMaximum(Ord(-1), "Negative()")); + } + + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 Zero() { + return new AnyInt64(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 NonZero() { + return new AnyInt64(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 GreaterThan(long value) { + return new AnyInt64(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 GreaterThanOrEqualTo(long value) { + return new AnyInt64(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 LessThan(long value) { + return new AnyInt64(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 LessThanOrEqualTo(long value) { + return new AnyInt64(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 Between(long minimum, long maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyInt64(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 OneOf(params long[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyInt64(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 Except(params long[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyInt64(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyInt64 DifferentFrom(long value) { + return new AnyInt64(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public long Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} diff --git a/Dummies/AnySByte.cs b/Dummies/AnySByte.cs new file mode 100644 index 00000000..6f66f0a1 --- /dev/null +++ b/Dummies/AnySByte.cs @@ -0,0 +1,181 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// +public sealed class AnySByte : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator sbyte(AnySByte generator) { + return generator.Generate(); + } + + internal static AnySByte Create(RandomSource source) { + return new AnySByte(source, OrdinalIntervalSpec.Unconstrained("SByte", ordinal => V(Val(ordinal)), Ord(sbyte.MinValue), Ord(sbyte.MaxValue))); + } + + private static ulong Ord(sbyte value) { + return OrdinalMapping.FromInt64(value); + } + + private static sbyte Val(ulong ordinal) { + return (sbyte)OrdinalMapping.ToInt64(ordinal); + } + + private static string V(sbyte value) { + return value.ToString(CultureInfo.InvariantCulture); + } + + private static string Join(sbyte[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnySByte(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a value strictly greater than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte Positive() { + return new AnySByte(_source, _spec.WithMinimum(Ord(1), "Positive()")); + } + + /// Requires a value strictly less than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte Negative() { + return new AnySByte(_source, _spec.WithMaximum(Ord(-1), "Negative()")); + } + + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte Zero() { + return new AnySByte(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte NonZero() { + return new AnySByte(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte GreaterThan(sbyte value) { + return new AnySByte(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte GreaterThanOrEqualTo(sbyte value) { + return new AnySByte(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte LessThan(sbyte value) { + return new AnySByte(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte LessThanOrEqualTo(sbyte value) { + return new AnySByte(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte Between(sbyte minimum, sbyte maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnySByte(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte OneOf(params sbyte[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnySByte(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte Except(params sbyte[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnySByte(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySByte DifferentFrom(sbyte value) { + return new AnySByte(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public sbyte Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} diff --git a/Dummies/AnySingle.cs b/Dummies/AnySingle.cs new file mode 100644 index 00000000..7bd1a9ec --- /dev/null +++ b/Dummies/AnySingle.cs @@ -0,0 +1,199 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes. NaN and the infinities are never generated nor accepted. +/// +public sealed class AnySingle : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator float(AnySingle generator) { + return generator.Generate(); + } + + internal static AnySingle Create(RandomSource source) { + return new AnySingle(source, ContinuousIntervalSpec.Unconstrained("Single", value => V((float)value), value => (float)value, value => NextUp((float)value), -float.MaxValue, float.MaxValue)); + } + + private static string V(float value) { + return value.ToString("R", CultureInfo.InvariantCulture); + } + + private static string Join(float[] values) { + return string.Join(", ", values.Select(V)); + } + + /// The next representable float above — the exclusive-bound arithmetic. + private static double NextUp(float value) { + int bits = BitConverter.ToInt32(BitConverter.GetBytes(value), 0); + if (bits >= 0) { bits++; } else if (bits == int.MinValue) { bits = 1; } else { bits--; } + + float next = BitConverter.ToSingle(BitConverter.GetBytes(bits), 0); + + return float.IsInfinity(next) ? double.PositiveInfinity : next; + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly ContinuousIntervalSpec _spec; + + #endregion + + private AnySingle(RandomSource source, ContinuousIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a value strictly greater than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle Positive() { + return new AnySingle(_source, _spec.WithMinimumAbove(0d, "Positive()")); + } + + /// Requires a value strictly less than zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle Negative() { + return new AnySingle(_source, _spec.WithMaximumBelow(0d, "Negative()")); + } + + /// Pins the value to exactly zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle Zero() { + return new AnySingle(_source, _spec.WithMinimum(0d, "Zero()").WithMaximum(0d, "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle NonZero() { + return new AnySingle(_source, _spec.WithExcluded([0d], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle GreaterThan(float value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnySingle(_source, _spec.WithMinimumAbove(value, $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle GreaterThanOrEqualTo(float value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnySingle(_source, _spec.WithMinimum((double)value, $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle LessThan(float value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnySingle(_source, _spec.WithMaximumBelow(value, $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle LessThanOrEqualTo(float value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnySingle(_source, _spec.WithMaximum((double)value, $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when a bound is not finite or is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle Between(float minimum, float maximum) { + ContinuousIntervalSpec.EnsureFinite(minimum, nameof(minimum)); + ContinuousIntervalSpec.EnsureFinite(maximum, nameof(maximum)); + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnySingle(_source, _spec.WithMinimum((double)minimum, constraint).WithMaximum((double)maximum, constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty or contains a non-finite value. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle OneOf(params float[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + foreach (float value in values) { ContinuousIntervalSpec.EnsureFinite(value, nameof(values)); } + + return new AnySingle(_source, _spec.WithAllowed(values.Select(value => (double)value).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty or contains a non-finite value. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle Except(params float[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + foreach (float value in values) { ContinuousIntervalSpec.EnsureFinite(value, nameof(values)); } + + return new AnySingle(_source, _spec.WithExcluded(values.Select(value => (double)value).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when is not finite. + /// Thrown when the constraint contradicts a constraint already declared. + public AnySingle DifferentFrom(float value) { + ContinuousIntervalSpec.EnsureFinite(value, nameof(value)); + return new AnySingle(_source, _spec.WithExcluded([(double)value], $"DifferentFrom({V(value)})")); + } + + /// + public float Generate() { + SeededRandom current = _source.Current; + + return (float)_spec.Generate(current.Random, current.Seed); + } + +} diff --git a/Dummies/AnyTimeOnly.cs b/Dummies/AnyTimeOnly.cs new file mode 100644 index 00000000..861f8ef9 --- /dev/null +++ b/Dummies/AnyTimeOnly.cs @@ -0,0 +1,158 @@ +#if NET8_0_OR_GREATER +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// Available on the net8.0 target only, like the type itself. There is deliberately no clock-relative +/// constraint: a reproducible test pins its reference time of days explicitly with and +/// . +/// +public sealed class AnyTimeOnly : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator TimeOnly(AnyTimeOnly generator) { + return generator.Generate(); + } + + internal static AnyTimeOnly Create(RandomSource source) { + return new AnyTimeOnly(source, OrdinalIntervalSpec.Unconstrained("TimeOnly", ordinal => V(Val(ordinal)), Ord(TimeOnly.MinValue), Ord(TimeOnly.MaxValue))); + } + + private static ulong Ord(TimeOnly value) { + return (ulong)value.Ticks; + } + + private static TimeOnly Val(ulong ordinal) { + return new TimeOnly((long)ordinal); + } + + private static string V(TimeOnly value) { + return value.ToString("O", CultureInfo.InvariantCulture); + } + + private static string Join(TimeOnly[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyTimeOnly(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a time of day strictly after . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeOnly After(TimeOnly time) { + return new AnyTimeOnly(_source, _spec.WithMinimumAbove(Ord(time), $"After({V(time)})")); + } + + /// Requires a time of day at or after . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeOnly AfterOrEqualTo(TimeOnly time) { + return new AnyTimeOnly(_source, _spec.WithMinimum(Ord(time), $"AfterOrEqualTo({V(time)})")); + } + + /// Requires a time of day strictly before . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeOnly Before(TimeOnly time) { + return new AnyTimeOnly(_source, _spec.WithMaximumBelow(Ord(time), $"Before({V(time)})")); + } + + /// Requires a time of day at or before . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeOnly BeforeOrEqualTo(TimeOnly time) { + return new AnyTimeOnly(_source, _spec.WithMaximum(Ord(time), $"BeforeOrEqualTo({V(time)})")); + } + + /// Requires a time of day within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is after . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeOnly Between(TimeOnly start, TimeOnly end) { + if (start > end) { throw new ArgumentException($"The start ({V(start)}) must be at or before the end ({V(end)}).", nameof(start)); } + + string constraint = $"Between({V(start)}, {V(end)})"; + + return new AnyTimeOnly(_source, _spec.WithMinimum(Ord(start), constraint).WithMaximum(Ord(end), constraint)); + } + + /// Requires the time of day to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeOnly OneOf(params TimeOnly[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyTimeOnly(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the time of day to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeOnly Except(params TimeOnly[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyTimeOnly(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the time of day to differ from — typically an existing value the test + /// already holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated time of day must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeOnly DifferentFrom(TimeOnly value) { + return new AnyTimeOnly(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public TimeOnly Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} +#endif diff --git a/Dummies/AnyTimeSpan.cs b/Dummies/AnyTimeSpan.cs new file mode 100644 index 00000000..b09656f5 --- /dev/null +++ b/Dummies/AnyTimeSpan.cs @@ -0,0 +1,182 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as +/// : constraints express what the surrounding code requires of the value, never what the +/// test asserts; contradictory constraints fail eagerly with a +/// naming both sides; instances are immutable recipes, and each value is built to satisfy the constraints in one +/// draw. Unconstrained, it draws from the full range, negative durations included. +/// +public sealed class AnyTimeSpan : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is + /// expected. Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator TimeSpan(AnyTimeSpan generator) { + return generator.Generate(); + } + + internal static AnyTimeSpan Create(RandomSource source) { + return new AnyTimeSpan(source, OrdinalIntervalSpec.Unconstrained("TimeSpan", ordinal => V(Val(ordinal)), Ord(TimeSpan.MinValue), Ord(TimeSpan.MaxValue))); + } + + private static ulong Ord(TimeSpan value) { + return OrdinalMapping.FromInt64(value.Ticks); + } + + private static TimeSpan Val(ulong ordinal) { + return new TimeSpan(OrdinalMapping.ToInt64(ordinal)); + } + + private static string V(TimeSpan value) { + return value.ToString("c", CultureInfo.InvariantCulture); + } + + private static string Join(TimeSpan[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyTimeSpan(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Requires a duration strictly greater than . + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan Positive() { + return new AnyTimeSpan(_source, _spec.WithMinimumAbove(Ord(TimeSpan.Zero), "Positive()")); + } + + /// Requires a duration strictly less than . + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan Negative() { + return new AnyTimeSpan(_source, _spec.WithMaximumBelow(Ord(TimeSpan.Zero), "Negative()")); + } + + /// Pins the duration to exactly . + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan Zero() { + return new AnyTimeSpan(_source, _spec.WithMinimum(Ord(TimeSpan.Zero), "Zero()").WithMaximum(Ord(TimeSpan.Zero), "Zero()")); + } + + /// Requires a duration different from . + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan NonZero() { + return new AnyTimeSpan(_source, _spec.WithExcluded([Ord(TimeSpan.Zero)], "NonZero()")); + } + + /// Requires a duration strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan GreaterThan(TimeSpan value) { + return new AnyTimeSpan(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a duration greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan GreaterThanOrEqualTo(TimeSpan value) { + return new AnyTimeSpan(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a duration strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan LessThan(TimeSpan value) { + return new AnyTimeSpan(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a duration less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan LessThanOrEqualTo(TimeSpan value) { + return new AnyTimeSpan(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a duration within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan Between(TimeSpan minimum, TimeSpan maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyTimeSpan(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the duration to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan OneOf(params TimeSpan[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyTimeSpan(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the duration to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan Except(params TimeSpan[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyTimeSpan(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the duration to differ from — typically an existing value the test + /// already holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated duration must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyTimeSpan DifferentFrom(TimeSpan value) { + return new AnyTimeSpan(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public TimeSpan Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} diff --git a/Dummies/AnyUInt128.cs b/Dummies/AnyUInt128.cs new file mode 100644 index 00000000..20346c15 --- /dev/null +++ b/Dummies/AnyUInt128.cs @@ -0,0 +1,170 @@ +#if NET8_0_OR_GREATER +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// Available on the net8.0 target only, like the type itself. +/// +public sealed class AnyUInt128 : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator UInt128(AnyUInt128 generator) { + return generator.Generate(); + } + + internal static AnyUInt128 Create(RandomSource source) { + return new AnyUInt128(source, WideIntervalSpec.Unconstrained("UInt128", ordinal => V(Val(ordinal)), Ord(UInt128.MinValue), Ord(UInt128.MaxValue))); + } + + private static UInt128 Ord(UInt128 value) { + return value; + } + + private static UInt128 Val(UInt128 ordinal) { + return ordinal; + } + + private static string V(UInt128 value) { + return value.ToString(null, CultureInfo.InvariantCulture); + } + + private static string Join(UInt128[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly WideIntervalSpec _spec; + + #endregion + + private AnyUInt128(RandomSource source, WideIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 Zero() { + return new AnyUInt128(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 NonZero() { + return new AnyUInt128(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 GreaterThan(UInt128 value) { + return new AnyUInt128(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 GreaterThanOrEqualTo(UInt128 value) { + return new AnyUInt128(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 LessThan(UInt128 value) { + return new AnyUInt128(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 LessThanOrEqualTo(UInt128 value) { + return new AnyUInt128(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 Between(UInt128 minimum, UInt128 maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyUInt128(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 OneOf(params UInt128[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyUInt128(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 Except(params UInt128[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyUInt128(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt128 DifferentFrom(UInt128 value) { + return new AnyUInt128(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public UInt128 Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} +#endif diff --git a/Dummies/AnyUInt16.cs b/Dummies/AnyUInt16.cs new file mode 100644 index 00000000..2664c6d6 --- /dev/null +++ b/Dummies/AnyUInt16.cs @@ -0,0 +1,167 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// +public sealed class AnyUInt16 : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator ushort(AnyUInt16 generator) { + return generator.Generate(); + } + + internal static AnyUInt16 Create(RandomSource source) { + return new AnyUInt16(source, OrdinalIntervalSpec.Unconstrained("UInt16", ordinal => V(Val(ordinal)), Ord(ushort.MinValue), Ord(ushort.MaxValue))); + } + + private static ulong Ord(ushort value) { + return value; + } + + private static ushort Val(ulong ordinal) { + return (ushort)ordinal; + } + + private static string V(ushort value) { + return value.ToString(CultureInfo.InvariantCulture); + } + + private static string Join(ushort[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyUInt16(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 Zero() { + return new AnyUInt16(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 NonZero() { + return new AnyUInt16(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 GreaterThan(ushort value) { + return new AnyUInt16(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 GreaterThanOrEqualTo(ushort value) { + return new AnyUInt16(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 LessThan(ushort value) { + return new AnyUInt16(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 LessThanOrEqualTo(ushort value) { + return new AnyUInt16(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 Between(ushort minimum, ushort maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyUInt16(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 OneOf(params ushort[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyUInt16(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 Except(params ushort[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyUInt16(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt16 DifferentFrom(ushort value) { + return new AnyUInt16(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public ushort Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} diff --git a/Dummies/AnyUInt32.cs b/Dummies/AnyUInt32.cs new file mode 100644 index 00000000..574ea193 --- /dev/null +++ b/Dummies/AnyUInt32.cs @@ -0,0 +1,167 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// +public sealed class AnyUInt32 : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator uint(AnyUInt32 generator) { + return generator.Generate(); + } + + internal static AnyUInt32 Create(RandomSource source) { + return new AnyUInt32(source, OrdinalIntervalSpec.Unconstrained("UInt32", ordinal => V(Val(ordinal)), Ord(uint.MinValue), Ord(uint.MaxValue))); + } + + private static ulong Ord(uint value) { + return value; + } + + private static uint Val(ulong ordinal) { + return (uint)ordinal; + } + + private static string V(uint value) { + return value.ToString(CultureInfo.InvariantCulture); + } + + private static string Join(uint[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyUInt32(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 Zero() { + return new AnyUInt32(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 NonZero() { + return new AnyUInt32(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 GreaterThan(uint value) { + return new AnyUInt32(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 GreaterThanOrEqualTo(uint value) { + return new AnyUInt32(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 LessThan(uint value) { + return new AnyUInt32(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 LessThanOrEqualTo(uint value) { + return new AnyUInt32(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 Between(uint minimum, uint maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyUInt32(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 OneOf(params uint[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyUInt32(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 Except(params uint[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyUInt32(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt32 DifferentFrom(uint value) { + return new AnyUInt32(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public uint Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} diff --git a/Dummies/AnyUInt64.cs b/Dummies/AnyUInt64.cs new file mode 100644 index 00000000..d090de71 --- /dev/null +++ b/Dummies/AnyUInt64.cs @@ -0,0 +1,167 @@ +#region Usings declarations + +using System.Globalization; + +#endregion + +namespace Dummies; + +/// +/// A fluent generator of arbitrary values — the same contract as : +/// constraints express what the surrounding code requires of the value, never what the test asserts; +/// contradictory constraints fail eagerly with a naming both +/// sides; instances are immutable recipes, and each value is built to satisfy the constraints in one draw. +/// +public sealed class AnyUInt64 : IAny, IHasRandomSource { + + #region Statics members declarations + + /// + /// Generates the value — an can be used wherever a is expected. + /// Each conversion draws a fresh value. + /// + /// The generator to draw from. + /// An arbitrary value satisfying the generator's constraints. + public static implicit operator ulong(AnyUInt64 generator) { + return generator.Generate(); + } + + internal static AnyUInt64 Create(RandomSource source) { + return new AnyUInt64(source, OrdinalIntervalSpec.Unconstrained("UInt64", ordinal => V(Val(ordinal)), Ord(ulong.MinValue), Ord(ulong.MaxValue))); + } + + private static ulong Ord(ulong value) { + return value; + } + + private static ulong Val(ulong ordinal) { + return ordinal; + } + + private static string V(ulong value) { + return value.ToString(CultureInfo.InvariantCulture); + } + + private static string Join(ulong[] values) { + return string.Join(", ", values.Select(V)); + } + + #endregion + + #region Fields declarations + + private readonly RandomSource _source; + private readonly OrdinalIntervalSpec _spec; + + #endregion + + private AnyUInt64(RandomSource source, OrdinalIntervalSpec spec) { + _source = source; + _spec = spec; + } + + RandomSource? IHasRandomSource.Source => _source; + + /// Pins the value to exactly zero. Useful for symmetry with the other constraints when a test sweeps cases. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 Zero() { + return new AnyUInt64(_source, _spec.WithMinimum(Ord(0), "Zero()").WithMaximum(Ord(0), "Zero()")); + } + + /// Requires a value different from zero. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 NonZero() { + return new AnyUInt64(_source, _spec.WithExcluded([Ord(0)], "NonZero()")); + } + + /// Requires a value strictly greater than . + /// The exclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 GreaterThan(ulong value) { + return new AnyUInt64(_source, _spec.WithMinimumAbove(Ord(value), $"GreaterThan({V(value)})")); + } + + /// Requires a value greater than or equal to . + /// The inclusive lower bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 GreaterThanOrEqualTo(ulong value) { + return new AnyUInt64(_source, _spec.WithMinimum(Ord(value), $"GreaterThanOrEqualTo({V(value)})")); + } + + /// Requires a value strictly less than . + /// The exclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 LessThan(ulong value) { + return new AnyUInt64(_source, _spec.WithMaximumBelow(Ord(value), $"LessThan({V(value)})")); + } + + /// Requires a value less than or equal to . + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 LessThanOrEqualTo(ulong value) { + return new AnyUInt64(_source, _spec.WithMaximum(Ord(value), $"LessThanOrEqualTo({V(value)})")); + } + + /// Requires a value within the inclusive range [, ]. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// A new generator carrying the added constraint. + /// Thrown when is greater than . + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 Between(ulong minimum, ulong maximum) { + if (minimum > maximum) { throw new ArgumentException($"The minimum ({V(minimum)}) must be less than or equal to the maximum ({V(maximum)}).", nameof(minimum)); } + + string constraint = $"Between({V(minimum)}, {V(maximum)})"; + + return new AnyUInt64(_source, _spec.WithMinimum(Ord(minimum), constraint).WithMaximum(Ord(maximum), constraint)); + } + + /// Requires the value to be one of the supplied values. Declared once per generator. + /// The allowed values; duplicates are ignored. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 OneOf(params ulong[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyUInt64(_source, _spec.WithAllowed(values.Select(Ord).ToArray(), $"OneOf({Join(values)})")); + } + + /// Requires the value to be none of the supplied values. + /// The forbidden values. + /// A new generator carrying the added constraint. + /// Thrown when is null. + /// Thrown when is empty. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 Except(params ulong[] values) { + if (values is null) { throw new ArgumentNullException(nameof(values)); } + if (values.Length == 0) { throw new ArgumentException("At least one value is required.", nameof(values)); } + + return new AnyUInt64(_source, _spec.WithExcluded(values.Select(Ord).ToArray(), $"Except({Join(values)})")); + } + + /// + /// Requires the value to differ from — typically an existing value the test already + /// holds. Semantically equivalent to ; the name carries the intent at the call site. + /// + /// The value the generated value must differ from. + /// A new generator carrying the added constraint. + /// Thrown when the constraint contradicts a constraint already declared. + public AnyUInt64 DifferentFrom(ulong value) { + return new AnyUInt64(_source, _spec.WithExcluded([Ord(value)], $"DifferentFrom({V(value)})")); + } + + /// + public ulong Generate() { + return Val(_spec.GenerateOrdinal(_source.Current.Random)); + } + +} diff --git a/Dummies/CharacterPools.cs b/Dummies/CharacterPools.cs new file mode 100644 index 00000000..5011cbda --- /dev/null +++ b/Dummies/CharacterPools.cs @@ -0,0 +1,39 @@ +namespace Dummies; + +/// The character families a string or char generator can be restricted to. +internal enum CharacterSet { + + Alpha, + Numeric, + AlphaNumeric + +} + +/// The casing a string or char generator can impose on alphabetic characters. +internal enum LetterCasing { + + Lower, + Upper + +} + +/// +/// The ASCII pools and classification helpers shared by 's filler and +/// — one definition of "letters and digits", so the two generators can never drift +/// apart on what their default characters are. +/// +internal static class CharacterPools { + + internal const string UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + internal const string LowerLetters = "abcdefghijklmnopqrstuvwxyz"; + internal const string Digits = "0123456789"; + + internal static bool IsAsciiLetter(char character) { + return character is >= 'A' and <= 'Z' or >= 'a' and <= 'z'; + } + + internal static bool IsAsciiDigit(char character) { + return character is >= '0' and <= '9'; + } + +} diff --git a/Dummies/ContinuousIntervalSpec.cs b/Dummies/ContinuousIntervalSpec.cs new file mode 100644 index 00000000..b03898e7 --- /dev/null +++ b/Dummies/ContinuousIntervalSpec.cs @@ -0,0 +1,219 @@ +namespace Dummies; + +/// +/// The shared immutable engine behind the binary floating-point generators (, +/// , and Half on modern targets): an inclusive interval of finite doubles, an +/// optional allow-list, and point exclusions — each bound remembering the constraint that set it, so a conflict +/// message can name both sides. NaN and the infinities are never generated nor accepted: arbitrary test values +/// should cross invariants, not sabotage arithmetic. +/// +/// +/// +/// Narrower value types ride the double engine through a quantize step (for example +/// double → float): bounds are supplied already-representable in the narrow type, sampling happens in +/// double, and the drawn value is quantized then clamped back into the bounds. +/// +/// +/// Excluding a point from a continuum can only collide with a draw on a set of measure zero, but the engine +/// still guarantees the constraint: a colliding draw is nudged to the neighbouring representable value, a +/// bounded deterministic walk — not a retry loop. When the walk cannot stay within the bounds the generation +/// fails with an naming the seed. +/// +/// +internal sealed class ContinuousIntervalSpec { + + private const int NudgeBudget = 128; + + #region Statics members declarations + + internal static ContinuousIntervalSpec Unconstrained(string typeName, Func render, Func quantize, Func nextUp, double domainMin, double domainMax) { + return new ContinuousIntervalSpec(typeName, render, quantize, nextUp, domainMin, null, domainMax, null, null, null, []); + } + + /// Rejects NaN and the infinities — the shared argument guard of every floating-point generator. + internal static void EnsureFinite(double value, string parameterName) { + if (double.IsNaN(value) || double.IsInfinity(value)) { throw new ArgumentException("The value must be finite: NaN and infinities are never generated.", parameterName); } + } + + /// The next representable double above — the exclusive-bound arithmetic. + internal static double NextUp(double value) { + long bits = BitConverter.DoubleToInt64Bits(value); + if (bits >= 0L) { bits++; } else if (bits == long.MinValue) { bits = 1L; } else { bits--; } + + return BitConverter.Int64BitsToDouble(bits); + } + + /// The next representable double below . + internal static double NextDown(double value) { + return -NextUp(-value); + } + + #endregion + + #region Fields declarations + + private readonly IReadOnlyList? _allowed; + private readonly string? _allowedConstraint; + private readonly List? _effectiveAllowed; + private readonly IReadOnlyList _excluded; + private readonly Func _nextUp; + private readonly double _max; + private readonly string? _maxConstraint; + private readonly double _min; + private readonly string? _minConstraint; + private readonly Func _quantize; + private readonly Func _render; + private readonly string _typeName; + + #endregion + + private ContinuousIntervalSpec(string typeName, Func render, Func quantize, Func nextUp, + double min, string? minConstraint, + double max, string? maxConstraint, + IReadOnlyList? allowed, string? allowedConstraint, + IReadOnlyList excluded) { + _typeName = typeName; + _render = render; + _quantize = quantize; + _nextUp = nextUp; + _min = min; + _minConstraint = minConstraint; + _max = max; + _maxConstraint = maxConstraint; + _allowed = allowed; + _allowedConstraint = allowedConstraint; + _excluded = excluded; + // Materialized once here — "constrain once, draw many": Generate never refilters the allow-list. + _effectiveAllowed = allowed?.Where(value => value >= min && value <= max && !IsExcluded(value)).ToList(); + } + + /// Tightens the lower bound; a looser bound than the current one is a no-op. + internal ContinuousIntervalSpec WithMinimum(double minimum, string applying) { + if (double.IsInfinity(minimum)) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + if (minimum <= _min) { return this; } + + if (minimum > _max) { + if (_maxConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_maxConstraint} already requires values less than or equal to {_render(_max)}."); + } + + return Validated(new ContinuousIntervalSpec(_typeName, _render, _quantize, _nextUp, minimum, applying, _max, _maxConstraint, _allowed, _allowedConstraint, _excluded), applying); + } + + /// Tightens the upper bound; a looser bound than the current one is a no-op. + internal ContinuousIntervalSpec WithMaximum(double maximum, string applying) { + if (double.IsInfinity(maximum)) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + if (maximum >= _max) { return this; } + + if (maximum < _min) { + if (_minConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_minConstraint} already requires values greater than or equal to {_render(_min)}."); + } + + return Validated(new ContinuousIntervalSpec(_typeName, _render, _quantize, _nextUp, _min, _minConstraint, maximum, applying, _allowed, _allowedConstraint, _excluded), applying); + } + + /// Tightens the lower bound to strictly above — via the type's next representable value. + internal ContinuousIntervalSpec WithMinimumAbove(double bound, string applying) { + return WithMinimum(_nextUp(bound), applying); + } + + /// Tightens the upper bound to strictly below — via the type's next representable value. + internal ContinuousIntervalSpec WithMaximumBelow(double bound, string applying) { + return WithMaximum(-_nextUp(-bound), applying); + } + + /// Restricts the domain to an explicit allow-list; declared once per generator. + internal ContinuousIntervalSpec WithAllowed(double[] values, string applying) { + if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_allowedConstraint} is already defined."); } + + double[] distinct = values.Distinct().ToArray(); + + return Validated(new ContinuousIntervalSpec(_typeName, _render, _quantize, _nextUp, _min, _minConstraint, _max, _maxConstraint, distinct, applying, _excluded), applying); + } + + /// Adds values the generator must never produce. + internal ContinuousIntervalSpec WithExcluded(double[] values, string applying) { + List excluded = new(_excluded); + excluded.AddRange(values); + + return Validated(new ContinuousIntervalSpec(_typeName, _render, _quantize, _nextUp, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, excluded), applying); + } + + /// Draws one value satisfying the whole specification. + internal double Generate(Random random, int seed) { + if (_effectiveAllowed is not null) { + return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; + } + + if (_min == _max) { return _min; } + + // Sample around the midpoint so the span (max - min) never overflows to infinity on wide ranges. + double mid = _min / 2 + _max / 2; + double half = _max / 2 - _min / 2; + double candidate = Quantized(mid + (2 * random.NextDouble() - 1) * half); + + // A draw colliding with an excluded point (a measure-zero event) is walked to the neighbouring + // representable value — deterministic and bounded, not a retry loop. + int budget = NudgeBudget; + while (IsExcluded(candidate)) { + double next = Quantized(NextUp(candidate)); + if (next > _max || budget-- == 0) { + throw new AnyGenerationException( + $"Generation failed: no {_typeName} value near the drawn candidate satisfies the exclusions. The arbitrary values were seeded with {seed}; reproduce this run with Any.Reproducibly({seed}, ...).", + seed, + new InvalidOperationException("The exclusion nudge walked out of the allowed range.")); + } + + candidate = next; + } + + return candidate; + } + + private double Quantized(double value) { + double quantized = _quantize(value); + if (quantized < _min) { return _min; } + if (quantized > _max) { return _max; } + + return quantized; + } + + private bool IsExcluded(double value) { + foreach (double excluded in _excluded) { + if (value.Equals(excluded)) { return true; } + } + + return false; + } + + private ContinuousIntervalSpec Validated(ContinuousIntervalSpec candidate, string applying) { + if (candidate.IsSatisfiable()) { return candidate; } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {candidate.DescribeExhaustion()}."); + } + + private bool IsSatisfiable() { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Count > 0; } + if (_min < _max) { return true; } + + return !IsExcluded(_min); + } + + private string DescribeExhaustion() { + if (_allowed is not null) { + if (_excluded.Count > 0) { + return $"no value {_allowedConstraint} allows remains available"; + } + + return $"none of the values {_allowedConstraint} allows satisfies the constraints already defined"; + } + + string pinning = _minConstraint ?? _maxConstraint ?? "the declared bounds"; + + return $"{pinning} already pins the value to {_render(_min)}, which the exclusions forbid"; + } + +} diff --git a/Dummies/DecimalIntervalSpec.cs b/Dummies/DecimalIntervalSpec.cs new file mode 100644 index 00000000..871a528e --- /dev/null +++ b/Dummies/DecimalIntervalSpec.cs @@ -0,0 +1,186 @@ +namespace Dummies; + +/// +/// The immutable engine behind — the same algebra as +/// in arithmetic. has no +/// next-representable-value ladder, so exclusive bounds are expressed as an inclusive bound plus a point +/// exclusion, and a colliding draw is nudged by the smallest decimal increment within a bounded budget. +/// +internal sealed class DecimalIntervalSpec { + + private const int NudgeBudget = 128; + + private static readonly decimal SmallestStep = 0.0000000000000000000000000001m; + private static readonly decimal MaxFraction = 7.9228162514264337593543950335m; + + #region Statics members declarations + + internal static DecimalIntervalSpec Unconstrained(string typeName, Func render) { + return new DecimalIntervalSpec(typeName, render, decimal.MinValue, null, decimal.MaxValue, null, null, null, []); + } + + #endregion + + #region Fields declarations + + private readonly IReadOnlyList? _allowed; + private readonly string? _allowedConstraint; + private readonly List? _effectiveAllowed; + private readonly IReadOnlyList _excluded; + private readonly decimal _max; + private readonly string? _maxConstraint; + private readonly decimal _min; + private readonly string? _minConstraint; + private readonly Func _render; + private readonly string _typeName; + + #endregion + + private DecimalIntervalSpec(string typeName, Func render, + decimal min, string? minConstraint, + decimal max, string? maxConstraint, + IReadOnlyList? allowed, string? allowedConstraint, + IReadOnlyList excluded) { + _typeName = typeName; + _render = render; + _min = min; + _minConstraint = minConstraint; + _max = max; + _maxConstraint = maxConstraint; + _allowed = allowed; + _allowedConstraint = allowedConstraint; + _excluded = excluded; + // Materialized once here — "constrain once, draw many": Generate never refilters the allow-list. + _effectiveAllowed = allowed?.Where(value => value >= min && value <= max && !IsExcluded(value)).ToList(); + } + + /// Tightens the lower bound; a looser bound than the current one is a no-op. + internal DecimalIntervalSpec WithMinimum(decimal minimum, string applying) { + if (minimum <= _min) { return this; } + + if (minimum > _max) { + if (_maxConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_maxConstraint} already requires values less than or equal to {_render(_max)}."); + } + + return Validated(new DecimalIntervalSpec(_typeName, _render, minimum, applying, _max, _maxConstraint, _allowed, _allowedConstraint, _excluded), applying); + } + + /// Tightens the upper bound; a looser bound than the current one is a no-op. + internal DecimalIntervalSpec WithMaximum(decimal maximum, string applying) { + if (maximum >= _max) { return this; } + + if (maximum < _min) { + if (_minConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_minConstraint} already requires values greater than or equal to {_render(_min)}."); + } + + return Validated(new DecimalIntervalSpec(_typeName, _render, _min, _minConstraint, maximum, applying, _allowed, _allowedConstraint, _excluded), applying); + } + + /// Tightens the lower bound to strictly above — the inclusive bound plus a point exclusion. + internal DecimalIntervalSpec WithMinimumAbove(decimal bound, string applying) { + return WithMinimum(bound, applying).WithExcluded([bound], applying); + } + + /// Tightens the upper bound to strictly below — the inclusive bound plus a point exclusion. + internal DecimalIntervalSpec WithMaximumBelow(decimal bound, string applying) { + return WithMaximum(bound, applying).WithExcluded([bound], applying); + } + + /// Restricts the domain to an explicit allow-list; declared once per generator. + internal DecimalIntervalSpec WithAllowed(decimal[] values, string applying) { + if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_allowedConstraint} is already defined."); } + + decimal[] distinct = values.Distinct().ToArray(); + + return Validated(new DecimalIntervalSpec(_typeName, _render, _min, _minConstraint, _max, _maxConstraint, distinct, applying, _excluded), applying); + } + + /// Adds values the generator must never produce. + internal DecimalIntervalSpec WithExcluded(decimal[] values, string applying) { + List excluded = new(_excluded); + excluded.AddRange(values); + + return Validated(new DecimalIntervalSpec(_typeName, _render, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, excluded), applying); + } + + /// Draws one value satisfying the whole specification. + internal decimal Generate(Random random, int seed) { + if (_effectiveAllowed is not null) { + return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; + } + + if (_min == _max) { return _min; } + + // A uniform-enough fraction in [0, 1): 93 random bits over the full decimal mantissa scale. + decimal fraction = new decimal(random.Next(), random.Next(), random.Next(), false, 28) / MaxFraction; + // Sample around the midpoint so the span (max - min) never overflows on wide ranges. + decimal mid = _min / 2 + _max / 2; + decimal half = _max / 2 - _min / 2; + decimal candidate = Clamped(mid + (fraction * 2 - 1) * half); + + // A draw colliding with an excluded point is walked by the smallest decimal step — deterministic and + // bounded, not a retry loop. (At extreme magnitudes the step can vanish in rounding; the budget then + // fails the generation loudly instead of looping.) + int budget = NudgeBudget; + while (IsExcluded(candidate)) { + 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. The arbitrary values were seeded with {seed}; reproduce this run with Any.Reproducibly({seed}, ...).", + seed, + new InvalidOperationException("The exclusion nudge could not leave the excluded point within the allowed range.")); + } + + candidate = next; + } + + return candidate; + } + + private decimal Clamped(decimal value) { + if (value < _min) { return _min; } + if (value > _max) { return _max; } + + return value; + } + + private bool IsExcluded(decimal value) { + foreach (decimal excluded in _excluded) { + if (value == excluded) { return true; } + } + + return false; + } + + private DecimalIntervalSpec Validated(DecimalIntervalSpec candidate, string applying) { + if (candidate.IsSatisfiable()) { return candidate; } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {candidate.DescribeExhaustion()}."); + } + + private bool IsSatisfiable() { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Count > 0; } + if (_min < _max) { return true; } + + return !IsExcluded(_min); + } + + private string DescribeExhaustion() { + if (_allowed is not null) { + if (_excluded.Count > 0) { + return $"no value {_allowedConstraint} allows remains available"; + } + + return $"none of the values {_allowedConstraint} allows satisfies the constraints already defined"; + } + + string pinning = _minConstraint ?? _maxConstraint ?? "the declared bounds"; + + return $"{pinning} already pins the value to {_render(_min)}, which the exclusions forbid"; + } + +} diff --git a/Dummies/Dummies.csproj b/Dummies/Dummies.csproj index 08521aec..96b74101 100644 --- a/Dummies/Dummies.csproj +++ b/Dummies/Dummies.csproj @@ -1,8 +1,9 @@ - - netstandard2.0 + + netstandard2.0;net8.0 enable enable latest diff --git a/Dummies/Int32Spec.cs b/Dummies/Int32Spec.cs deleted file mode 100644 index b6d2fbb8..00000000 --- a/Dummies/Int32Spec.cs +++ /dev/null @@ -1,158 +0,0 @@ -#region Usings declarations - -using System.Globalization; - -#endregion - -namespace Dummies; - -/// -/// The immutable specification behind : an inclusive interval, an optional allow-list -/// (OneOf), and an exclusion list — each bound remembering the constraint that set it, so a conflict -/// message can name both sides. Every mutation returns a new specification and validates satisfiability eagerly: -/// an that exists can always generate. -/// -internal sealed class Int32Spec { - - #region Statics members declarations - - internal static readonly Int32Spec Unconstrained = new(int.MinValue, null, int.MaxValue, null, null, null, []); - - private static string V(long value) { - return value.ToString(CultureInfo.InvariantCulture); - } - - #endregion - - #region Fields declarations - - private readonly IReadOnlyList? _allowed; - private readonly string? _allowedConstraint; - private readonly IReadOnlyList _excluded; - private readonly long _max; - private readonly string? _maxConstraint; - private readonly long _min; - private readonly string? _minConstraint; - - #endregion - - private Int32Spec(long min, string? minConstraint, long max, string? maxConstraint, - IReadOnlyList? allowed, string? allowedConstraint, IReadOnlyList excluded) { - _min = min; - _minConstraint = minConstraint; - _max = max; - _maxConstraint = maxConstraint; - _allowed = allowed; - _allowedConstraint = allowedConstraint; - _excluded = excluded; - } - - /// Tightens the lower bound; a looser bound than the current one is a no-op. - internal Int32Spec WithMinimum(long minimum, string applying) { - if (minimum > int.MaxValue) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no Int32 value satisfies it."); } - if (minimum <= _min) { return this; } - - if (minimum > _max) { - if (_maxConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no Int32 value satisfies it."); } - - throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_maxConstraint} already requires values less than or equal to {V(_max)}."); - } - - return Validated(new Int32Spec(minimum, applying, _max, _maxConstraint, _allowed, _allowedConstraint, _excluded), applying); - } - - /// Tightens the upper bound; a looser bound than the current one is a no-op. - internal Int32Spec WithMaximum(long maximum, string applying) { - if (maximum < int.MinValue) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no Int32 value satisfies it."); } - if (maximum >= _max) { return this; } - - if (maximum < _min) { - if (_minConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no Int32 value satisfies it."); } - - throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_minConstraint} already requires values greater than or equal to {V(_min)}."); - } - - return Validated(new Int32Spec(_min, _minConstraint, maximum, applying, _allowed, _allowedConstraint, _excluded), applying); - } - - /// Restricts the domain to an explicit allow-list; declared once per generator. - internal Int32Spec WithAllowed(int[] values, string applying) { - if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_allowedConstraint} is already defined."); } - - int[] distinct = values.Distinct().ToArray(); - - return Validated(new Int32Spec(_min, _minConstraint, _max, _maxConstraint, distinct, applying, _excluded), applying); - } - - /// Adds values the generator must never produce. - internal Int32Spec WithExcluded(int[] values, string applying) { - List excluded = new(_excluded); - excluded.AddRange(values); - - return Validated(new Int32Spec(_min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, excluded), applying); - } - - /// Draws one value satisfying the whole specification — built directly, never generate-then-retry. - internal int Generate(Random random) { - if (_allowed is not null) { - List pool = EffectiveAllowed(); - - return pool[random.Next(pool.Count)]; - } - - List excluded = ExcludedInRangeSorted(); - long validCount = (_max - _min + 1) - excluded.Count; - long candidate = _min + random.NextInt64(0, validCount - 1); - // Map the drawn index onto the k-th non-excluded value of the interval: every excluded value at or - // below the candidate shifts it up by one. Sorted ascending, so a single pass suffices. - foreach (int value in excluded) { - if (candidate >= value) { candidate++; } - } - - return (int)candidate; - } - - private static Int32Spec Validated(Int32Spec candidate, string applying) { - if (candidate.CountCandidates() > 0) { return candidate; } - - throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {candidate.DescribeExhaustion()}."); - } - - private long CountCandidates() { - if (_allowed is not null) { return EffectiveAllowed().Count; } - - return (_max - _min + 1) - ExcludedInRangeSorted().Count; - } - - private List EffectiveAllowed() { - HashSet excluded = new(_excluded); - - return _allowed!.Where(value => value >= _min && value <= _max && !excluded.Contains(value)).ToList(); - } - - private List ExcludedInRangeSorted() { - List excluded = _excluded.Where(value => value >= _min && value <= _max).Distinct().ToList(); - excluded.Sort(); - - return excluded; - } - - private string DescribeExhaustion() { - if (_allowed is not null) { - if (_excluded.Count > 0 && _allowed.All(value => _excluded.Contains(value) || value < _min || value > _max)) { - return $"no value {_allowedConstraint} allows remains available"; - } - - return $"none of the values {_allowedConstraint} allows satisfies the constraints already defined"; - } - - if (_min == _max) { - string pinning = _minConstraint ?? _maxConstraint ?? "the declared bounds"; - - return $"{pinning} already pins the value to {V(_min)}"; - } - - return $"no value remains between {V(_min)} and {V(_max)} once the excluded values are removed"; - } - -} diff --git a/Dummies/OrdinalIntervalSpec.cs b/Dummies/OrdinalIntervalSpec.cs new file mode 100644 index 00000000..eeed43aa --- /dev/null +++ b/Dummies/OrdinalIntervalSpec.cs @@ -0,0 +1,212 @@ +namespace Dummies; + +/// +/// Order-preserving mappings between the discrete domains the generators expose and the unsigned 64-bit +/// ordinal space the shared interval engine works in. Every discrete type whose values fit 64 bits — +/// the integers, ticks-based time types, day numbers — maps onto [0, 2^64-1] so that one engine owns +/// bounds, exclusions, conflicts, and sampling for all of them. +/// +internal static class OrdinalMapping { + + private const ulong SignBit = 1UL << 63; + + /// Maps a signed 64-bit value to its ordinal: flips the sign bit, so ordering is preserved. + internal static ulong FromInt64(long value) { + return unchecked((ulong)value ^ SignBit); + } + + /// Maps an ordinal back to the signed 64-bit value it came from. + internal static long ToInt64(ulong ordinal) { + return unchecked((long)(ordinal ^ SignBit)); + } + +} + +/// +/// The shared immutable engine behind every discrete interval-shaped generator (integers, TimeSpan, +/// DateTime, ...): an inclusive interval of ordinals, an optional allow-list (OneOf), and an +/// exclusion list — each bound remembering the constraint that set it, so a conflict message can name both +/// sides. Every mutation returns a new specification and validates satisfiability eagerly: a generator that +/// exists can always generate, in one draw, with no retry. +/// +/// +/// The engine is domain-agnostic: each public generator supplies its type's display name (for "no Int64 value +/// satisfies it" messages), a renderer turning an ordinal back into a displayable value, and the ordinal bounds +/// of its domain. The conflict logic therefore lives once, and a fix to a message or an edge case reaches every +/// discrete type at the same time. +/// +internal sealed class OrdinalIntervalSpec { + + #region Statics members declarations + + internal static OrdinalIntervalSpec Unconstrained(string typeName, Func render, ulong domainMin, ulong domainMax) { + return new OrdinalIntervalSpec(typeName, render, domainMin, domainMax, + domainMin, null, domainMax, null, null, null, []); + } + + #endregion + + #region Fields declarations + + private readonly IReadOnlyList? _allowed; + private readonly string? _allowedConstraint; + private readonly ulong _domainMax; + private readonly List? _effectiveAllowed; + private readonly List _excludedInRange; + private readonly ulong _domainMin; + private readonly IReadOnlyList _excluded; + private readonly ulong _max; + private readonly string? _maxConstraint; + private readonly ulong _min; + private readonly string? _minConstraint; + private readonly Func _render; + private readonly string _typeName; + + #endregion + + private OrdinalIntervalSpec(string typeName, Func render, ulong domainMin, ulong domainMax, + ulong min, string? minConstraint, + ulong max, string? maxConstraint, + IReadOnlyList? allowed, string? allowedConstraint, + IReadOnlyList excluded) { + _typeName = typeName; + _render = render; + _domainMin = domainMin; + _domainMax = domainMax; + _min = min; + _minConstraint = minConstraint; + _max = max; + _maxConstraint = maxConstraint; + _allowed = allowed; + _allowedConstraint = allowedConstraint; + _excluded = excluded; + // Materialized once here — "constrain once, draw many": GenerateOrdinal never refilters or resorts. + _excludedInRange = excluded.Where(value => value >= min && value <= max).Distinct().ToList(); + _excludedInRange.Sort(); + if (allowed is not null) { + HashSet forbidden = new(excluded); + _effectiveAllowed = allowed.Where(value => value >= min && value <= max && !forbidden.Contains(value)).ToList(); + } + } + + /// Tightens the lower bound; a looser bound than the current one is a no-op. + internal OrdinalIntervalSpec WithMinimum(ulong minimum, string applying) { + if (minimum <= _min) { return this; } + + if (minimum > _max) { + if (_maxConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_maxConstraint} already requires values less than or equal to {_render(_max)}."); + } + + return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, minimum, applying, _max, _maxConstraint, _allowed, _allowedConstraint, _excluded), applying); + } + + /// Tightens the lower bound to strictly above — the exclusive form of . + internal OrdinalIntervalSpec WithMinimumAbove(ulong bound, string applying) { + if (bound == _domainMax) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + return WithMinimum(bound + 1, applying); + } + + /// Tightens the upper bound; a looser bound than the current one is a no-op. + internal OrdinalIntervalSpec WithMaximum(ulong maximum, string applying) { + if (maximum >= _max) { return this; } + + if (maximum < _min) { + if (_minConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_minConstraint} already requires values greater than or equal to {_render(_min)}."); + } + + return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, maximum, applying, _allowed, _allowedConstraint, _excluded), applying); + } + + /// Tightens the upper bound to strictly below — the exclusive form of . + internal OrdinalIntervalSpec WithMaximumBelow(ulong bound, string applying) { + if (bound == _domainMin) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + return WithMaximum(bound - 1, applying); + } + + /// Restricts the domain to an explicit allow-list; declared once per generator. + internal OrdinalIntervalSpec WithAllowed(ulong[] ordinals, string applying) { + if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_allowedConstraint} is already defined."); } + + ulong[] distinct = ordinals.Distinct().ToArray(); + + return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, distinct, applying, _excluded), applying); + } + + /// Adds values the generator must never produce. + internal OrdinalIntervalSpec WithExcluded(ulong[] ordinals, string applying) { + List excluded = new(_excluded); + excluded.AddRange(ordinals); + + return Validated(new OrdinalIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, excluded), applying); + } + + /// Draws one ordinal satisfying the whole specification — built directly, never generate-then-retry. + internal ulong GenerateOrdinal(Random random) { + if (_effectiveAllowed is not null) { + return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; + } + + List excluded = _excludedInRange; + if (IsFullWidth()) { + // The interval spans the whole ordinal space, so its size does not fit a ulong and the index + // mapping below cannot run. Draw anywhere and, in the astronomically rare case the draw hits an + // excluded value, walk to the next free ordinal — a deterministic, bounded step, not a retry loop. + ulong candidate = random.NextUInt64(); + while (excluded.Contains(candidate)) { candidate = unchecked(candidate + 1UL); } + + return candidate; + } + + ulong validCount = _max - _min + 1 - (ulong)excluded.Count; + ulong candidateOrdinal = _min + random.NextUInt64() % validCount; + // Map the drawn index onto the k-th non-excluded ordinal of the interval: every excluded ordinal at + // or below the candidate shifts it up by one. Sorted ascending, so a single pass suffices. + foreach (ulong value in excluded) { + if (candidateOrdinal >= value) { candidateOrdinal++; } + } + + return candidateOrdinal; + } + + private bool IsFullWidth() { + return _min == ulong.MinValue && _max == ulong.MaxValue; + } + + private OrdinalIntervalSpec Validated(OrdinalIntervalSpec candidate, string applying) { + if (candidate.IsSatisfiable()) { return candidate; } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {candidate.DescribeExhaustion()}."); + } + + private bool IsSatisfiable() { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Count > 0; } + if (IsFullWidth()) { return true; } + + return _max - _min + 1 - (ulong)_excludedInRange.Count > 0; + } + + private string DescribeExhaustion() { + if (_allowed is not null) { + if (_excluded.Count > 0) { + return $"no value {_allowedConstraint} allows remains available"; + } + + return $"none of the values {_allowedConstraint} allows satisfies the constraints already defined"; + } + + if (_min == _max) { + string pinning = _minConstraint ?? _maxConstraint ?? "the declared bounds"; + + return $"{pinning} already pins the value to {_render(_min)}"; + } + + return $"no value remains between {_render(_min)} and {_render(_max)} once the excluded values are removed"; + } + +} diff --git a/Dummies/README.nuget.md b/Dummies/README.nuget.md index 5dea7073..71ae308f 100644 --- a/Dummies/README.nuget.md +++ b/Dummies/README.nuget.md @@ -21,8 +21,18 @@ matter — and that is the point. ## What's inside -- **Fluent, typed generators** (`Any.String()`, `Any.Int32()`, ...) implementing - `IAny`, with implicit conversion to the generated type. +- **Fluent, typed generators** implementing `IAny`, with implicit conversion to + the generated type, across the .NET simple types: `String`, `Char`, every integer + width (`SByte`/`Byte`/`Int16`/`UInt16`/`Int32`/`UInt32`/`Int64`/`UInt64`), + `Double`/`Single`/`Decimal` (finite values only — never NaN or infinities), + `Bool`, `Guid`, `Enum` (declared members only), `TimeSpan`, `DateTime` (UTC) + and `DateTimeOffset`. On modern targets (`net8.0`) the surface extends to + `DateOnly`, `TimeOnly`, `Int128`, `UInt128` and `Half`; the package also targets + `netstandard2.0` for the widest reach. +- **Domain vocabulary where it belongs**: dates constrain with + `After`/`Before`/`Between`, quantities with `Positive`/`Between`/`NonZero`, + identities with `NonEmpty`/`DifferentFrom` — and deliberately no clock-relative + constraints: a reproducible test pins its reference instants explicitly. - **Values built to satisfy the constraints** — never generate-then-filter, no retry loops. - **Conflicting constraints fail fast** with a clear, actionable diff --git a/Dummies/RandomSource.cs b/Dummies/RandomSource.cs index 6b02e012..c6dac8d8 100644 --- a/Dummies/RandomSource.cs +++ b/Dummies/RandomSource.cs @@ -131,15 +131,15 @@ internal static class RandomSampling { /// ]. Unlike the upper bound is reachable, /// which matters for full-range and boundary draws. The draw maps 8 random bytes onto the range size; the /// modulo bias is at most 2^-32 for the ranges an can express — irrelevant for arbitrary - /// test values. + /// test values. Deliberately NOT named NextInt64: on the net8.0 leg the framework's own + /// Random.NextInt64(long, long) instance method — whose upper bound is EXCLUSIVE — would win + /// overload resolution over a same-named extension and silently change the semantics. /// - internal static long NextInt64(this Random random, long minInclusive, long maxInclusive) { + internal static long NextInt64Inclusive(this Random random, long minInclusive, long maxInclusive) { if (minInclusive > maxInclusive) { throw new ArgumentOutOfRangeException(nameof(maxInclusive), "The maximum must be greater than or equal to the minimum."); } - ulong rangeSize = (ulong)(maxInclusive - minInclusive) + 1UL; - byte[] bytes = new byte[8]; - random.NextBytes(bytes); - ulong draw = BitConverter.ToUInt64(bytes, 0); + ulong rangeSize = (ulong)(maxInclusive - minInclusive) + 1UL; + ulong draw = random.NextUInt64(); // rangeSize is 0 only when the range spans the full ulong width, which int-derived bounds never do; // guard anyway so the helper stays correct if reused with wider bounds. @@ -148,9 +148,18 @@ internal static long NextInt64(this Random random, long minInclusive, long maxIn return minInclusive + (long)(draw % rangeSize); } - /// Draws a uniform in the inclusive range — see . - internal static int NextInt32(this Random random, int minInclusive, int maxInclusive) { - return (int)random.NextInt64(minInclusive, maxInclusive); + /// Draws a uniform in the inclusive range — see . + internal static int NextInt32Inclusive(this Random random, int minInclusive, int maxInclusive) { + return (int)random.NextInt64Inclusive(minInclusive, maxInclusive); + } + + /// Draws 8 random bytes as a — the raw material of the ordinal sampling. + internal static ulong NextUInt64(this Random random) { + byte[] bytes = new byte[8]; + random.NextBytes(bytes); + + return BitConverter.ToUInt64(bytes, 0); } + } diff --git a/Dummies/StringSpec.cs b/Dummies/StringSpec.cs index 9846c033..753611bf 100644 --- a/Dummies/StringSpec.cs +++ b/Dummies/StringSpec.cs @@ -7,23 +7,6 @@ namespace Dummies; -/// The character families a string generator can be restricted to. -internal enum CharacterSet { - - Alpha, - Numeric, - AlphaNumeric - -} - -/// The casing a string generator can impose on alphabetic characters. -internal enum LetterCasing { - - Lower, - Upper - -} - /// /// The immutable specification behind : length bounds, anchored fragments (prefix, /// suffix, contained values), a character set and a letter casing — each remembering the constraint that set it, @@ -40,10 +23,6 @@ internal sealed class StringSpec { private const int DefaultLengthSpread = 16; - private const string UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - private const string LowerLetters = "abcdefghijklmnopqrstuvwxyz"; - private const string Digits = "0123456789"; - #region Statics members declarations internal static readonly StringSpec Unconstrained = new(null, null, 0, null, null, null, @@ -199,7 +178,7 @@ internal string Generate(Random random) { int effectiveMin = Math.Max(_minLength, required); // Long arithmetic: a huge declared minimum must saturate instead of overflowing past int.MaxValue. int effectiveMax = _maxLength ?? (int)Math.Min((long)effectiveMin + DefaultLengthSpread, int.MaxValue); - int length = _exactLength ?? random.NextInt32(effectiveMin, effectiveMax); + int length = _exactLength ?? random.NextInt32Inclusive(effectiveMin, effectiveMax); string pool = FillerPool(); int fillerLength = length - required; @@ -320,9 +299,9 @@ private int RequiredLength() { private static char? FirstOutsideCharset(string fragment, CharacterSet charset) { foreach (char character in fragment) { bool allowed = charset switch { - CharacterSet.Alpha => IsAsciiLetter(character), - CharacterSet.Numeric => IsAsciiDigit(character), - CharacterSet.AlphaNumeric => IsAsciiLetter(character) || IsAsciiDigit(character), + CharacterSet.Alpha => CharacterPools.IsAsciiLetter(character), + CharacterSet.Numeric => CharacterPools.IsAsciiDigit(character), + CharacterSet.AlphaNumeric => CharacterPools.IsAsciiLetter(character) || CharacterPools.IsAsciiDigit(character), _ => true }; if (!allowed) { return character; } @@ -340,25 +319,17 @@ private int RequiredLength() { return null; } - private static bool IsAsciiLetter(char character) { - return character is >= 'A' and <= 'Z' or >= 'a' and <= 'z'; - } - - private static bool IsAsciiDigit(char character) { - return character is >= '0' and <= '9'; - } - private string FillerPool() { string letters = _casing switch { - LetterCasing.Lower => LowerLetters, - LetterCasing.Upper => UpperLetters, - _ => UpperLetters + LowerLetters + LetterCasing.Lower => CharacterPools.LowerLetters, + LetterCasing.Upper => CharacterPools.UpperLetters, + _ => CharacterPools.UpperLetters + CharacterPools.LowerLetters }; return _charset switch { CharacterSet.Alpha => letters, - CharacterSet.Numeric => Digits, - _ => letters + Digits + CharacterSet.Numeric => CharacterPools.Digits, + _ => letters + CharacterPools.Digits }; } diff --git a/Dummies/WideIntervalSpec.cs b/Dummies/WideIntervalSpec.cs new file mode 100644 index 00000000..3ee476a3 --- /dev/null +++ b/Dummies/WideIntervalSpec.cs @@ -0,0 +1,185 @@ +#if NET8_0_OR_GREATER +namespace Dummies; + +/// +/// The 128-bit sibling of , backing and +/// : their ordinal space exceeds 64 bits, so the same algebra — descriptor-tracked +/// inclusive bounds, allow-list, exclusions, eager conflicts, one-draw generation — runs over +/// ordinals. Net8-only, like the types it serves. +/// +internal sealed class WideIntervalSpec { + + #region Statics members declarations + + internal static WideIntervalSpec Unconstrained(string typeName, Func render, UInt128 domainMin, UInt128 domainMax) { + return new WideIntervalSpec(typeName, render, domainMin, domainMax, domainMin, null, domainMax, null, null, null, []); + } + + private static UInt128 NextUInt128(Random random) { + return new UInt128(random.NextUInt64(), random.NextUInt64()); + } + + #endregion + + #region Fields declarations + + private readonly IReadOnlyList? _allowed; + private readonly string? _allowedConstraint; + private readonly UInt128 _domainMax; + private readonly List? _effectiveAllowed; + private readonly List _excludedInRange; + private readonly UInt128 _domainMin; + private readonly IReadOnlyList _excluded; + private readonly UInt128 _max; + private readonly string? _maxConstraint; + private readonly UInt128 _min; + private readonly string? _minConstraint; + private readonly Func _render; + private readonly string _typeName; + + #endregion + + private WideIntervalSpec(string typeName, Func render, UInt128 domainMin, UInt128 domainMax, + UInt128 min, string? minConstraint, + UInt128 max, string? maxConstraint, + IReadOnlyList? allowed, string? allowedConstraint, + IReadOnlyList excluded) { + _typeName = typeName; + _render = render; + _domainMin = domainMin; + _domainMax = domainMax; + _min = min; + _minConstraint = minConstraint; + _max = max; + _maxConstraint = maxConstraint; + _allowed = allowed; + _allowedConstraint = allowedConstraint; + _excluded = excluded; + // Materialized once here — "constrain once, draw many": GenerateOrdinal never refilters or resorts. + _excludedInRange = excluded.Where(value => value >= min && value <= max).Distinct().ToList(); + _excludedInRange.Sort(); + if (allowed is not null) { + HashSet forbidden = new(excluded); + _effectiveAllowed = allowed.Where(value => value >= min && value <= max && !forbidden.Contains(value)).ToList(); + } + } + + /// Tightens the lower bound; a looser bound than the current one is a no-op. + internal WideIntervalSpec WithMinimum(UInt128 minimum, string applying) { + if (minimum <= _min) { return this; } + + if (minimum > _max) { + if (_maxConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_maxConstraint} already requires values less than or equal to {_render(_max)}."); + } + + return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, minimum, applying, _max, _maxConstraint, _allowed, _allowedConstraint, _excluded), applying); + } + + /// Tightens the lower bound to strictly above — the exclusive form of . + internal WideIntervalSpec WithMinimumAbove(UInt128 bound, string applying) { + if (bound == _domainMax) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + return WithMinimum(bound + 1, applying); + } + + /// Tightens the upper bound; a looser bound than the current one is a no-op. + internal WideIntervalSpec WithMaximum(UInt128 maximum, string applying) { + if (maximum >= _max) { return this; } + + if (maximum < _min) { + if (_minConstraint is null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_minConstraint} already requires values greater than or equal to {_render(_min)}."); + } + + return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, maximum, applying, _allowed, _allowedConstraint, _excluded), applying); + } + + /// Tightens the upper bound to strictly below — the exclusive form of . + internal WideIntervalSpec WithMaximumBelow(UInt128 bound, string applying) { + if (bound == _domainMin) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because no {_typeName} value satisfies it."); } + + return WithMaximum(bound - 1, applying); + } + + /// Restricts the domain to an explicit allow-list; declared once per generator. + internal WideIntervalSpec WithAllowed(UInt128[] ordinals, string applying) { + if (_allowedConstraint is not null) { throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {_allowedConstraint} is already defined."); } + + UInt128[] distinct = ordinals.Distinct().ToArray(); + + return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, distinct, applying, _excluded), applying); + } + + /// Adds values the generator must never produce. + internal WideIntervalSpec WithExcluded(UInt128[] ordinals, string applying) { + List excluded = new(_excluded); + excluded.AddRange(ordinals); + + return Validated(new WideIntervalSpec(_typeName, _render, _domainMin, _domainMax, _min, _minConstraint, _max, _maxConstraint, _allowed, _allowedConstraint, excluded), applying); + } + + /// Draws one ordinal satisfying the whole specification — built directly, never generate-then-retry. + internal UInt128 GenerateOrdinal(Random random) { + if (_effectiveAllowed is not null) { + return _effectiveAllowed[random.Next(_effectiveAllowed.Count)]; + } + + List excluded = _excludedInRange; + if (IsFullWidth()) { + // Same escape as OrdinalIntervalSpec: the full 128-bit space has no representable size, so draw + // anywhere and walk off an excluded value deterministically. + UInt128 candidate = NextUInt128(random); + while (excluded.Contains(candidate)) { candidate = unchecked(candidate + 1); } + + return candidate; + } + + UInt128 size = _max - _min + 1 - (UInt128)excluded.Count; + UInt128 candidateOrdinal = _min + NextUInt128(random) % size; + foreach (UInt128 value in excluded) { + if (candidateOrdinal >= value) { candidateOrdinal++; } + } + + return candidateOrdinal; + } + + private bool IsFullWidth() { + return _min == UInt128.MinValue && _max == UInt128.MaxValue; + } + + private WideIntervalSpec Validated(WideIntervalSpec candidate, string applying) { + if (candidate.IsSatisfiable()) { return candidate; } + + throw new ConflictingAnyConstraintException($"Cannot apply {applying} because {candidate.DescribeExhaustion()}."); + } + + private bool IsSatisfiable() { + if (_effectiveAllowed is not null) { return _effectiveAllowed.Count > 0; } + if (IsFullWidth()) { return true; } + + return _max - _min + 1 - (UInt128)_excludedInRange.Count > 0; + } + + private string DescribeExhaustion() { + if (_allowed is not null) { + if (_excluded.Count > 0) { + return $"no value {_allowedConstraint} allows remains available"; + } + + return $"none of the values {_allowedConstraint} allows satisfies the constraints already defined"; + } + + if (_min == _max) { + string pinning = _minConstraint ?? _maxConstraint ?? "the declared bounds"; + + return $"{pinning} already pins the value to {_render(_min)}"; + } + + return $"no value remains between {_render(_min)} and {_render(_max)} once the excluded values are removed"; + } + +} +#endif