Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions Dummies.UnitTests/AnyDateTimeOffsetOffsetTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#region Usings declarations

using NFluent;

#endregion

namespace Dummies.UnitTests;

/// <summary>
/// Behaviour of the <see cref="AnyDateTimeOffset" /> offset dimension — <c>WithOffset</c> pins it,
/// <c>WithOffsetBetween</c> draws it bounded, the default stays UTC, and values stay valid at the domain edges
/// because the instant is tightened before the offset is drawn.
/// </summary>
public sealed class AnyDateTimeOffsetOffsetTests {

private const int SampleCount = 200;

[Fact(DisplayName = "Offset: unconstrained, generated values carry UTC (zero) offset.")]
public void DefaultOffsetIsZero() {
for (int i = 0; i < SampleCount; i++) {
Check.That(Any.DateTimeOffset().Generate().Offset).IsEqualTo(TimeSpan.Zero);
}
}

[Fact(DisplayName = "WithOffset: every generated value carries the pinned offset.")]
public void WithOffsetPins() {
TimeSpan offset = TimeSpan.FromHours(2);
for (int i = 0; i < SampleCount; i++) {
Check.That(Any.DateTimeOffset().WithOffset(offset).Generate().Offset).IsEqualTo(offset);
}
}

[Fact(DisplayName = "WithOffsetBetween: offsets stay within the range, in whole minutes, and vary.")]
public void WithOffsetBetweenBounds() {
TimeSpan min = TimeSpan.FromHours(-5);
TimeSpan max = TimeSpan.FromHours(5);
HashSet<TimeSpan> seen = new();
for (int i = 0; i < SampleCount; i++) {
DateTimeOffset value = Any.DateTimeOffset().WithOffsetBetween(min, max).Generate();
Check.That(value.Offset >= min && value.Offset <= max).IsTrue();
Check.That(value.Offset.Ticks % TimeSpan.TicksPerMinute).IsEqualTo(0L);
seen.Add(value.Offset);
}

Check.That(seen.Count).IsStrictlyGreaterThan(1);
}

[Fact(DisplayName = "WithOffset: stays valid and after the floor at the top of the domain.")]
public void WithOffsetValidNearMaxValue() {
DateTimeOffset floor = DateTimeOffset.MaxValue.AddDays(-1);
for (int i = 0; i < SampleCount; i++) {
DateTimeOffset value = Any.DateTimeOffset().After(floor).WithOffset(TimeSpan.FromHours(14)).Generate();
Check.That(value.Offset).IsEqualTo(TimeSpan.FromHours(14));
Check.That(value.UtcTicks > floor.UtcTicks).IsTrue();
}
}

[Fact(DisplayName = "WithOffset: an instant window with no room for the offset conflicts eagerly.")]
public void WithOffsetImpossibleWindowConflicts() {
// The last 12h of the domain cannot host a +14h offset: the local ticks would overflow.
Check.ThatCode(() => Any.DateTimeOffset().After(DateTimeOffset.MaxValue.AddHours(-12)).WithOffset(TimeSpan.FromHours(14)))
.Throws<ConflictingAnyConstraintException>();
}

[Fact(DisplayName = "WithOffset: arguments are validated (whole minutes, ±14:00, ordered range).")]
public void WithOffsetArguments() {
Check.ThatCode(() => Any.DateTimeOffset().WithOffset(TimeSpan.FromSeconds(30))).Throws<ArgumentException>();
Check.ThatCode(() => Any.DateTimeOffset().WithOffset(TimeSpan.FromHours(15))).Throws<ArgumentOutOfRangeException>();
Check.ThatCode(() => Any.DateTimeOffset().WithOffsetBetween(TimeSpan.FromHours(2), TimeSpan.FromHours(-2))).Throws<ArgumentException>();
}

[Fact(DisplayName = "WithOffset: a second, different offset is rejected as already declared.")]
public void WithOffsetDeclaredOnce() {
Check.ThatCode(() => Any.DateTimeOffset().WithOffset(TimeSpan.FromHours(2)).WithOffset(TimeSpan.FromHours(3)))
.Throws<ConflictingAnyConstraintException>();
// The same offset twice is idempotent, not a conflict.
Check.That(Any.DateTimeOffset().WithOffset(TimeSpan.FromHours(2)).WithOffset(TimeSpan.FromHours(2)).Generate().Offset)
.IsEqualTo(TimeSpan.FromHours(2));
}

}
9 changes: 8 additions & 1 deletion Dummies.UnitTests/SurfaceParityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ private static string Signature(MethodInfo method) {
"Between", "OneOf", "Except", "DifferentFrom", "WithGranularity"
];

// AnyDateTimeOffset additionally exposes the offset dimension (WithOffset/WithOffsetBetween) — the only instant
// type carrying a second, offset dimension on top of the instant.
private static readonly string[] InstantWithGranularityAndOffsetAlgebra = [
"After", "AfterOrEqualTo", "Before", "BeforeOrEqualTo",
"Between", "OneOf", "Except", "DifferentFrom", "WithGranularity", "WithOffset", "WithOffsetBetween"
];

public static IEnumerable<object[]> Builders() {
// Signed integers carry MultipleOf; the binary floats do not; Decimal carries WithScale; TimeSpan (a signed
// magnitude) carries WithGranularity — the lattice constraint is what forks the former shared signed family.
Expand All @@ -158,7 +165,7 @@ public static IEnumerable<object[]> Builders() {
yield return [typeof(AnyUInt64), UnsignedIntegerAlgebra];

yield return [typeof(AnyDateTime), InstantWithGranularityAlgebra];
yield return [typeof(AnyDateTimeOffset), InstantWithGranularityAlgebra];
yield return [typeof(AnyDateTimeOffset), InstantWithGranularityAndOffsetAlgebra];

// The remaining scalar builders each carry their own deliberate set.
yield return [typeof(AnyBoolean), new[] { "True", "False", "DifferentFrom" }];
Expand Down
Loading