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>