Project · Testing
Reservation contract lab
Run independent domain scenarios with a controlled UTC clock and an in-memory notice recorder. This is a console lab, not an installed test framework.
All C# source files below are loaded together in the playground. Files written by a run live in its virtual directory.
Program.cs
var checks = ReservationChecks.Run();
foreach (var check in checks) Console.WriteLine($"{check.Name}:{(check.Passed ? "PASS" : "FAIL")}");
Console.WriteLine($"passed:{checks.Count(check => check.Passed)}/{checks.Count}");
return checks.All(check => check.Passed) ? 0 : 1;
Domain/ReservationBook.cs
public sealed class ReservationBook
{
private readonly int capacity;
private readonly DateTimeOffset closesAt;
private readonly TimeProvider clock;
private readonly INoticeSink sink;
private readonly Dictionary<string, int> reservations = new(StringComparer.Ordinal);
public int Used { get; private set; }
public int ReservationCount => reservations.Count;
public ReservationBook(int capacity, DateTimeOffset closesAt, TimeProvider clock, INoticeSink sink)
{
if (capacity < 1) throw new ArgumentOutOfRangeException(nameof(capacity));
this.capacity = capacity; this.closesAt = closesAt;
this.clock = clock ?? throw new ArgumentNullException(nameof(clock));
this.sink = sink ?? throw new ArgumentNullException(nameof(sink));
}
public bool TryReserve(string code, int quantity)
{
if (string.IsNullOrWhiteSpace(code) || quantity < 1 || reservations.ContainsKey(code) || clock.GetUtcNow() >= closesAt || quantity > capacity - Used) return false;
reservations.Add(code, quantity);
Used += quantity;
sink.Send("reserved:" + code);
return true;
}
}
public interface INoticeSink { void Send(string message); }
Support/ManualClock.cs
public sealed class ManualClock : TimeProvider
{
private DateTimeOffset now;
public ManualClock(DateTimeOffset initial) { now = initial; }
public override DateTimeOffset GetUtcNow() => now;
public void Advance(TimeSpan amount)
{
if (amount < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(amount));
now += amount;
}
}
Support/RecordingSink.cs
public sealed class RecordingSink : INoticeSink
{
public List<string> Messages { get; } = new();
public void Send(string message) => Messages.Add(message);
}
Checks/CheckResult.cs
public sealed record CheckResult(string Name, bool Passed);
Checks/ReservationChecks.cs
public static class ReservationChecks
{
private sealed record Fixture(ReservationBook Book, ManualClock Clock, RecordingSink Sink);
private static Fixture Fresh()
{
var start = new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);
var clock = new ManualClock(start);
var sink = new RecordingSink();
return new Fixture(new ReservationBook(3, start.AddMinutes(1), clock, sink), clock, sink);
}
private static bool Unchanged(Fixture fixture) => fixture.Book.Used == 0 && fixture.Book.ReservationCount == 0 && fixture.Sink.Messages.Count == 0;
public static List<CheckResult> Run()
{
var results = new List<CheckResult>();
var valid = Fresh();
bool accepted = valid.Book.TryReserve("A", 2);
results.Add(new("valid", accepted && valid.Book.Used == 2 && valid.Book.ReservationCount == 1 && valid.Sink.Messages.SequenceEqual(new[] { "reserved:A" })));
var zero = Fresh();
results.Add(new("zero-no-effects", !zero.Book.TryReserve("A", 0) && Unchanged(zero)));
var excess = Fresh();
results.Add(new("excess-no-effects", !excess.Book.TryReserve("A", 4) && Unchanged(excess)));
var blank = Fresh();
results.Add(new("blank-no-effects", !blank.Book.TryReserve(" ", 1) && Unchanged(blank)));
var duplicate = Fresh();
bool arranged = duplicate.Book.TryReserve("A", 2);
bool rejected = !duplicate.Book.TryReserve("A", 1);
results.Add(new("duplicate-no-effects", arranged && rejected && duplicate.Book.Used == 2 && duplicate.Book.ReservationCount == 1 && duplicate.Sink.Messages.Count == 1));
var last = Fresh();
bool first = last.Book.TryReserve("A", 2);
bool final = last.Book.TryReserve("B", 1);
results.Add(new("last-unit", first && final && last.Book.Used == 3 && last.Sink.Messages.Count == 2));
var exact = Fresh();
exact.Clock.Advance(TimeSpan.FromMinutes(1));
results.Add(new("exact-deadline", !exact.Book.TryReserve("A", 1) && Unchanged(exact)));
var after = Fresh();
after.Clock.Advance(TimeSpan.FromSeconds(61));
results.Add(new("after-deadline", !after.Book.TryReserve("A", 1) && Unchanged(after)));
var before = Fresh();
before.Clock.Advance(TimeSpan.FromSeconds(59));
results.Add(new("before-deadline", before.Book.TryReserve("A", 1) && before.Book.Used == 1 && before.Sink.Messages.Count == 1));
var identity = Fresh();
bool upper = identity.Book.TryReserve("A", 1);
bool lower = identity.Book.TryReserve("a", 1);
results.Add(new("ordinal-identity", upper && lower && identity.Book.ReservationCount == 2 && identity.Book.Used == 2));
return results;
}
}
ReservationContracts.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>