Skip to main content

pacta_conformance/
lib.rs

1//! A backend-agnostic conformance suite for [`Registry`] implementations.
2//!
3//! The suite is generic over `Registry` and takes a constructor closure that
4//! returns a seeded backend, so it defines no seeding trait: a backend runs the
5//! suite from its own `#[cfg(test)]` module and keeps `pacta-conformance` a pure
6//! dev-dependency. Time is driven entirely through the trait by passing controlled
7//! [`Timestamp`] values, never a wall clock.
8
9#![forbid(unsafe_code)]
10#![warn(missing_docs)]
11
12use std::fmt::Debug;
13
14use pacta_contract::{Pact, Registry, Retainer, Timestamp};
15use uuid::Uuid;
16
17/// The lease duration, in milliseconds, the suite constructs every backend with.
18pub const LEASE_MILLIS: u64 = 1000;
19
20const DOCKET: &str = "conformance";
21
22fn at(millis: u64) -> Timestamp {
23    Timestamp::from_millis(millis)
24}
25
26fn a_pact_on(docket: &str) -> Pact {
27    Pact::new(
28        Uuid::new_v4(),
29        docket.to_string(),
30        "conformance".to_string(),
31        Vec::new(),
32    )
33}
34
35fn a_pact() -> Pact {
36    a_pact_on(DOCKET)
37}
38
39/// Run the full conformance suite against a backend built by `make`.
40///
41/// `make(pacts, lease_millis)` must return a fresh registry seeded with `pacts`
42/// and configured to lease claims for `lease_millis`. The suite calls it once per
43/// scenario. A failing assertion panics, failing the calling test.
44pub fn run<R, F>(make: F)
45where
46    R: Registry,
47    R::Error: Debug,
48    F: Fn(Vec<Pact>, u64) -> R,
49{
50    no_available_pact_returns_none(&make);
51    unrequested_docket_is_not_claimed(&make);
52    claim_returns_claim_with_lease(&make);
53    held_pact_not_reclaimable_before_expiry(&make);
54    expired_lease_lapses_and_reclaims_with_rotated_retainer(&make);
55    stale_retainer_settle_rejected_after_reclaim(&make);
56    late_fulfill_before_reclaim_succeeds(&make);
57    fulfill_settles_and_pact_not_claimable(&make);
58    breach_settles_terminally(&make);
59    released_pact_withheld_until_reclaimable(&make);
60    released_pact_reclaimable_at_its_instant(&make);
61    immediate_reclaim_reclaims_like_lapse(&make);
62    release_rotates_authority_from_prior_holder(&make);
63    heartbeat_extends_lease_preventing_lapse(&make);
64    heartbeat_on_lapsed_lease_rejected(&make);
65    heartbeat_unknown_retainer_rejected(&make);
66}
67
68fn no_available_pact_returns_none<R, F>(make: &F)
69where
70    R: Registry,
71    R::Error: Debug,
72    F: Fn(Vec<Pact>, u64) -> R,
73{
74    let registry = make(Vec::new(), LEASE_MILLIS);
75    assert!(
76        registry
77            .claim(&[DOCKET], at(0))
78            .expect("claim should not error")
79            .is_none(),
80        "an empty registry must yield no claim"
81    );
82}
83
84fn unrequested_docket_is_not_claimed<R, F>(make: &F)
85where
86    R: Registry,
87    R::Error: Debug,
88    F: Fn(Vec<Pact>, u64) -> R,
89{
90    let registry = make(vec![a_pact_on("other")], LEASE_MILLIS);
91    assert!(
92        registry
93            .claim(&[DOCKET], at(0))
94            .expect("claim should not error")
95            .is_none(),
96        "a pact on an unrequested docket must not be claimed"
97    );
98    assert!(
99        registry
100            .claim(&["other"], at(0))
101            .expect("claim should not error")
102            .is_some(),
103        "the same pact must be claimable from its own docket"
104    );
105}
106
107fn claim_returns_claim_with_lease<R, F>(make: &F)
108where
109    R: Registry,
110    R::Error: Debug,
111    F: Fn(Vec<Pact>, u64) -> R,
112{
113    let registry = make(vec![a_pact()], LEASE_MILLIS);
114    let claim = registry
115        .claim(&[DOCKET], at(100))
116        .expect("claim should not error")
117        .expect("a pact should be claimable");
118    assert_eq!(
119        claim.lease_expiry,
120        at(100 + LEASE_MILLIS),
121        "lease expiry must be now plus the lease duration"
122    );
123}
124
125fn held_pact_not_reclaimable_before_expiry<R, F>(make: &F)
126where
127    R: Registry,
128    R::Error: Debug,
129    F: Fn(Vec<Pact>, u64) -> R,
130{
131    let registry = make(vec![a_pact()], LEASE_MILLIS);
132    let _first = registry
133        .claim(&[DOCKET], at(0))
134        .expect("claim should not error")
135        .expect("a pact should be claimable");
136    assert!(
137        registry
138            .claim(&[DOCKET], at(500))
139            .expect("claim should not error")
140            .is_none(),
141        "a held pact must not be reclaimable before its lease expires"
142    );
143}
144
145fn expired_lease_lapses_and_reclaims_with_rotated_retainer<R, F>(make: &F)
146where
147    R: Registry,
148    R::Error: Debug,
149    F: Fn(Vec<Pact>, u64) -> R,
150{
151    let registry = make(vec![a_pact()], LEASE_MILLIS);
152    let first = registry
153        .claim(&[DOCKET], at(0))
154        .expect("claim should not error")
155        .expect("a pact should be claimable");
156    let second = registry
157        .claim(&[DOCKET], at(1500))
158        .expect("claim should not error")
159        .expect("an expired pact should be reclaimable through the claim path");
160    assert_ne!(
161        first.retainer.id(),
162        second.retainer.id(),
163        "reclaiming a lapsed pact must rotate the retainer"
164    );
165    assert_eq!(
166        second.lease_expiry,
167        at(1500 + LEASE_MILLIS),
168        "the reclaim must set a fresh lease"
169    );
170}
171
172fn stale_retainer_settle_rejected_after_reclaim<R, F>(make: &F)
173where
174    R: Registry,
175    R::Error: Debug,
176    F: Fn(Vec<Pact>, u64) -> R,
177{
178    let registry = make(vec![a_pact()], LEASE_MILLIS);
179    let first = registry
180        .claim(&[DOCKET], at(0))
181        .expect("claim should not error")
182        .expect("a pact should be claimable");
183    let _second = registry
184        .claim(&[DOCKET], at(1500))
185        .expect("claim should not error")
186        .expect("an expired pact should be reclaimable");
187    assert!(
188        registry.fulfill(&first.retainer).is_err(),
189        "the prior holder must not settle after a reclaim (at-least-once safety)"
190    );
191}
192
193fn late_fulfill_before_reclaim_succeeds<R, F>(make: &F)
194where
195    R: Registry,
196    R::Error: Debug,
197    F: Fn(Vec<Pact>, u64) -> R,
198{
199    let registry = make(vec![a_pact()], LEASE_MILLIS);
200    let claim = registry
201        .claim(&[DOCKET], at(0))
202        .expect("claim should not error")
203        .expect("a pact should be claimable");
204    // The lease has expired but nobody reclaimed; the holder's retainer still
205    // matches, so a late fulfill of genuinely-done work settles. No time involved.
206    assert!(
207        registry.fulfill(&claim.retainer).is_ok(),
208        "a late fulfill before any reclaim must settle"
209    );
210    assert!(
211        registry
212            .claim(&[DOCKET], at(9999))
213            .expect("claim should not error")
214            .is_none(),
215        "a settled pact must not be claimable"
216    );
217}
218
219fn fulfill_settles_and_pact_not_claimable<R, F>(make: &F)
220where
221    R: Registry,
222    R::Error: Debug,
223    F: Fn(Vec<Pact>, u64) -> R,
224{
225    let registry = make(vec![a_pact()], LEASE_MILLIS);
226    let claim = registry
227        .claim(&[DOCKET], at(0))
228        .expect("claim should not error")
229        .expect("a pact should be claimable");
230    registry
231        .fulfill(&claim.retainer)
232        .expect("fulfill should settle");
233    assert!(
234        registry
235            .claim(&[DOCKET], at(0))
236            .expect("claim should not error")
237            .is_none(),
238        "a fulfilled pact must not be claimable"
239    );
240}
241
242fn breach_settles_terminally<R, F>(make: &F)
243where
244    R: Registry,
245    R::Error: Debug,
246    F: Fn(Vec<Pact>, u64) -> R,
247{
248    let registry = make(vec![a_pact()], LEASE_MILLIS);
249    let claim = registry
250        .claim(&[DOCKET], at(0))
251        .expect("claim should not error")
252        .expect("a pact should be claimable");
253    registry
254        .breach(&claim.retainer)
255        .expect("breach should settle");
256    assert!(
257        registry
258            .claim(&[DOCKET], at(5000))
259            .expect("claim should not error")
260            .is_none(),
261        "a breached pact must not be claimable, even after its lease would have expired"
262    );
263}
264
265fn released_pact_withheld_until_reclaimable<R, F>(make: &F)
266where
267    R: Registry,
268    R::Error: Debug,
269    F: Fn(Vec<Pact>, u64) -> R,
270{
271    let registry = make(vec![a_pact()], LEASE_MILLIS);
272    let claim = registry
273        .claim(&[DOCKET], at(0))
274        .expect("claim should not error")
275        .expect("a pact should be claimable");
276    registry
277        .release(&claim.retainer, at(5000))
278        .expect("release should succeed for the current holder");
279    // at(3000) is past the original lease (1000) — a lapse would make it claimable —
280    // but the reclaimable instant (5000) is later, so release must withhold it.
281    assert!(
282        registry
283            .claim(&[DOCKET], at(3000))
284            .expect("claim should not error")
285            .is_none(),
286        "a released pact must not be claimable before its reclaimable instant"
287    );
288}
289
290fn released_pact_reclaimable_at_its_instant<R, F>(make: &F)
291where
292    R: Registry,
293    R::Error: Debug,
294    F: Fn(Vec<Pact>, u64) -> R,
295{
296    let registry = make(vec![a_pact()], LEASE_MILLIS);
297    let first = registry
298        .claim(&[DOCKET], at(0))
299        .expect("claim should not error")
300        .expect("a pact should be claimable");
301    registry
302        .release(&first.retainer, at(5000))
303        .expect("release should succeed");
304    let second = registry
305        .claim(&[DOCKET], at(5000))
306        .expect("claim should not error")
307        .expect("a released pact must be claimable at its reclaimable instant");
308    assert_ne!(
309        first.retainer.id(),
310        second.retainer.id(),
311        "reclaiming a released pact must rotate the retainer"
312    );
313}
314
315fn immediate_reclaim_reclaims_like_lapse<R, F>(make: &F)
316where
317    R: Registry,
318    R::Error: Debug,
319    F: Fn(Vec<Pact>, u64) -> R,
320{
321    let registry = make(vec![a_pact()], LEASE_MILLIS);
322    let claim = registry
323        .claim(&[DOCKET], at(0))
324        .expect("claim should not error")
325        .expect("a pact should be claimable");
326    registry
327        .release(&claim.retainer, at(0))
328        .expect("release with an immediate reclaim should succeed");
329    assert!(
330        registry
331            .claim(&[DOCKET], at(0))
332            .expect("claim should not error")
333            .is_some(),
334        "an immediate reclaim must make the pact claimable at once, as a voluntary lapse"
335    );
336}
337
338fn release_rotates_authority_from_prior_holder<R, F>(make: &F)
339where
340    R: Registry,
341    R::Error: Debug,
342    F: Fn(Vec<Pact>, u64) -> R,
343{
344    let registry = make(vec![a_pact()], LEASE_MILLIS);
345    let claim = registry
346        .claim(&[DOCKET], at(0))
347        .expect("claim should not error")
348        .expect("a pact should be claimable");
349    registry
350        .release(&claim.retainer, at(0))
351        .expect("release should succeed");
352    assert!(
353        registry.fulfill(&claim.retainer).is_err(),
354        "the prior holder must not settle after releasing (release rotates authority)"
355    );
356}
357
358fn heartbeat_extends_lease_preventing_lapse<R, F>(make: &F)
359where
360    R: Registry,
361    R::Error: Debug,
362    F: Fn(Vec<Pact>, u64) -> R,
363{
364    let registry = make(vec![a_pact()], LEASE_MILLIS);
365    let claim = registry
366        .claim(&[DOCKET], at(0))
367        .expect("claim should not error")
368        .expect("a pact should be claimable");
369    registry
370        .heartbeat(&claim.retainer, at(800))
371        .expect("an in-window heartbeat should extend the lease");
372    // The original lease (expiry 1000) would have lapsed by 1500, but the
373    // heartbeat pushed expiry to 1800, so the pact is still held.
374    assert!(
375        registry
376            .claim(&[DOCKET], at(1500))
377            .expect("claim should not error")
378            .is_none(),
379        "a heartbeat within the window must prevent a lapse"
380    );
381}
382
383fn heartbeat_on_lapsed_lease_rejected<R, F>(make: &F)
384where
385    R: Registry,
386    R::Error: Debug,
387    F: Fn(Vec<Pact>, u64) -> R,
388{
389    let registry = make(vec![a_pact()], LEASE_MILLIS);
390    let claim = registry
391        .claim(&[DOCKET], at(0))
392        .expect("claim should not error")
393        .expect("a pact should be claimable");
394    assert!(
395        registry.heartbeat(&claim.retainer, at(1200)).is_err(),
396        "a heartbeat after the lease expired must be rejected, forcing a re-claim"
397    );
398}
399
400fn heartbeat_unknown_retainer_rejected<R, F>(make: &F)
401where
402    R: Registry,
403    R::Error: Debug,
404    F: Fn(Vec<Pact>, u64) -> R,
405{
406    let registry = make(vec![a_pact()], LEASE_MILLIS);
407    let _claim = registry
408        .claim(&[DOCKET], at(0))
409        .expect("claim should not error")
410        .expect("a pact should be claimable");
411    let unknown = Retainer::new(Uuid::new_v4());
412    assert!(
413        registry.heartbeat(&unknown, at(100)).is_err(),
414        "a heartbeat with an unissued retainer must be rejected"
415    );
416}