Time and events

A deadline, an order of events, or "every one of these ends" cannot be checked on a single value. These rules are about a sequence, so the test drives the real service through generated calls and judges the events it recorded.

internal sealed class Fulfilment(TimeProvider time, TimeSpan shipsAfter)
{
	public IReadOnlyList<Timestamped<FulfilmentEvent>> Log => _log;
	// ... Confirm() records Confirmed and Shipped, stamped from `time`
}
		

The service reads time only from the TimeProvider it is given. A service that calls DateTime.UtcNow or Task.Delay is invisible to a virtual clock, and a missed deadline can never happen in a test.

TemporalRuleSet<FulfilmentEvent>.From(
	("confirmed-ships-within-a-minute",
		Temporal<FulfilmentEvent>.AfterUntil(
			e => e is FulfilmentEvent.Confirmed,
			(_, next) => next is FulfilmentEvent.Shipped,
			TimeSpan.FromSeconds(60))));
		
await EventAutoTest.For(rules, script, time => new Fulfilment(time, shipsAfter), f => f.Log).RunAsync();
		

script lists the operations (Script.For<Fulfilment>().Step("Confirm", f => f.Confirm())). The test builds a fresh service per run on a manual clock, advances it by generated delays, and checks the rules against the log.

  • You write no delays. The test tries 0 and just either side of every rule's deadline, so a deadline is both met and missed within a few draws. A script that sets its own delays keeps them.
  • A rule fails as vacuous (see concepts) if no run contained its trigger, or none went past its deadline.
  • If the service exposes its events as an IAsyncEnumerable<Timestamped<T>>, use EventAutoTest.ForAsync(...).

Nothing here depends on Reactive Extensions. If your service is built on IObservable<T> and an Rx scheduler, the opt-in package Nullean.Entities.CsCheck.Reactive has the same script on a scheduler.

Temporal failed.

Rule broken confirmed-ships-within-a-minute Noticed at +60s What happened No matching event within 00:01:00 after Confirmed Events recorded +0s Confirmed +90s Shipped Calls made wait 1s, Confirm

Law What must follow something within a deadline does, however the time between calls is spent. Consequence A deadline is missed under timing the tests never tried, so a stuck state is reported as in progress for good.

Smallest case found, after 5 shrinks. To run this case again: .Seed("0i1vmP3ChLc5")