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
68/// Async conformance: hold an [`AsyncRegistry`](pacta_contract::AsyncRegistry) backend to the
69/// exact same scenarios as the sync suite.
70///
71/// The async runner reuses [`run`] rather than a duplicated scenario set: it adapts the async
72/// backend into the sync [`Registry`] by driving each operation to completion, so sync and async
73/// coverage cannot drift. This proves state-machine parity — the same bar the sync suite meets, which
74/// itself exercises no concurrency. The at-most-once invariant under concurrent contention is a
75/// separate, *portable* check (`run_async_contention`) that any async backend runs.
76///
77/// The adapter drives futures with a poll loop, so it fits backends whose futures make progress
78/// without an external reactor (the in-memory reference backend); a real-reactor durable backend
79/// proves itself against the same scenarios through its own async harness.
80#[cfg(feature = "async")]
81mod async_runner {
82    use core::future::Future;
83
84    use pacta_contract::AsyncRegistry;
85    use pacta_contract::{Claim, Pact, Registry, Retainer, Timestamp, Transition};
86
87    /// Drive a future to completion on the current thread with a no-op waker. Correct for futures
88    /// that make progress without an external reactor; keeps the crate dependency- and unsafe-free.
89    fn block_on<F: Future>(future: F) -> F::Output {
90        use core::task::{Context, Poll};
91
92        let mut future = core::pin::pin!(future);
93        let mut cx = Context::from_waker(core::task::Waker::noop());
94        loop {
95            match future.as_mut().poll(&mut cx) {
96                Poll::Ready(output) => return output,
97                Poll::Pending => core::hint::spin_loop(),
98            }
99        }
100    }
101
102    /// Adapts an [`AsyncRegistry`] into the sync [`Registry`] by blocking on each primitive, so the
103    /// async binding runs the sync suite verbatim. Because both bindings share one transition port,
104    /// the adapter forwards only the primitives (`claim`, `lease_millis`, `apply`); the four
105    /// transition ops come from the sync trait's default methods over `apply`.
106    struct BlockOn<R>(R);
107
108    impl<R: AsyncRegistry> Registry for BlockOn<R> {
109        type Error = R::Error;
110
111        fn claim(&self, dockets: &[&str], now: Timestamp) -> Result<Option<Claim>, Self::Error> {
112            block_on(self.0.claim(dockets, now))
113        }
114
115        fn lease_millis(&self) -> u64 {
116            self.0.lease_millis()
117        }
118
119        fn apply(
120            &self,
121            retainer: &Retainer,
122            transition: &Transition<'_>,
123        ) -> Result<(), Self::Error> {
124            block_on(self.0.apply(retainer, transition))
125        }
126    }
127
128    /// Run the full conformance suite against an async backend built by `make`.
129    ///
130    /// `make(pacts, lease_millis)` returns a fresh async registry seeded with `pacts`, exactly as
131    /// [`run`](crate::run) expects for the sync binding.
132    pub fn run_async<R, F>(make: F)
133    where
134        R: AsyncRegistry,
135        R::Error: core::fmt::Debug,
136        F: Fn(Vec<Pact>, u64) -> R,
137    {
138        crate::run(move |pacts, lease_millis| BlockOn(make(pacts, lease_millis)));
139    }
140
141    /// Verify the at-most-once invariant under concurrent contention, for any async backend.
142    ///
143    /// Two workers race a settlement on a single claimed pact; the transition port must apply it at
144    /// most once, so exactly one worker succeeds and the other resolves to a not-current-holder. The
145    /// check asserts this through the public op (never inspecting the backend's concurrency
146    /// mechanism), so it holds for a lock, a transaction, or a compare-and-set backend alike.
147    ///
148    /// Parallelism is real (OS threads); each thread drives its future to completion with
149    /// `block_on`, so a future never migrates across threads and **no `Send` bound on the future is
150    /// required** — the suite pulls no async runtime.
151    pub fn run_async_contention<R, F>(make: F)
152    where
153        R: AsyncRegistry + 'static,
154        R::Error: core::fmt::Debug + Send,
155        F: Fn(Vec<Pact>, u64) -> R,
156    {
157        use std::sync::Arc;
158
159        // Enough rounds that an interleaving loading the same state in both threads is hit, and a
160        // broken (non-atomic) `apply` would overwhelmingly double-apply on some round.
161        for _ in 0..2000 {
162            let reg = Arc::new(make(vec![crate::a_pact()], crate::LEASE_MILLIS));
163            let retainer = block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
164                .expect("claim should not error")
165                .expect("a pact should be claimable")
166                .retainer;
167
168            let a = {
169                let reg = Arc::clone(&reg);
170                let retainer = retainer.clone();
171                std::thread::spawn(move || block_on(reg.fulfill(&retainer)))
172            };
173            let b = {
174                let reg = Arc::clone(&reg);
175                let retainer = retainer.clone();
176                std::thread::spawn(move || block_on(reg.fulfill(&retainer)))
177            };
178            let (ra, rb) = (a.join().unwrap(), b.join().unwrap());
179
180            let winners = [ra.is_ok(), rb.is_ok()]
181                .into_iter()
182                .filter(|&ok| ok)
183                .count();
184            assert_eq!(
185                winners, 1,
186                "settlement must apply exactly once: a={ra:?} b={rb:?}"
187            );
188            assert!(
189                block_on(reg.claim(&[crate::DOCKET], crate::at(0)))
190                    .expect("claim should not error")
191                    .is_none(),
192                "a settled pact must not be claimable again"
193            );
194        }
195    }
196}
197
198#[cfg(feature = "async")]
199pub use async_runner::{run_async, run_async_contention};
200
201fn no_available_pact_returns_none<R, F>(make: &F)
202where
203    R: Registry,
204    R::Error: Debug,
205    F: Fn(Vec<Pact>, u64) -> R,
206{
207    let registry = make(Vec::new(), LEASE_MILLIS);
208    assert!(
209        registry
210            .claim(&[DOCKET], at(0))
211            .expect("claim should not error")
212            .is_none(),
213        "an empty registry must yield no claim"
214    );
215}
216
217fn unrequested_docket_is_not_claimed<R, F>(make: &F)
218where
219    R: Registry,
220    R::Error: Debug,
221    F: Fn(Vec<Pact>, u64) -> R,
222{
223    let registry = make(vec![a_pact_on("other")], LEASE_MILLIS);
224    assert!(
225        registry
226            .claim(&[DOCKET], at(0))
227            .expect("claim should not error")
228            .is_none(),
229        "a pact on an unrequested docket must not be claimed"
230    );
231    assert!(
232        registry
233            .claim(&["other"], at(0))
234            .expect("claim should not error")
235            .is_some(),
236        "the same pact must be claimable from its own docket"
237    );
238}
239
240fn claim_returns_claim_with_lease<R, F>(make: &F)
241where
242    R: Registry,
243    R::Error: Debug,
244    F: Fn(Vec<Pact>, u64) -> R,
245{
246    let registry = make(vec![a_pact()], LEASE_MILLIS);
247    let claim = registry
248        .claim(&[DOCKET], at(100))
249        .expect("claim should not error")
250        .expect("a pact should be claimable");
251    assert_eq!(
252        claim.lease_expiry,
253        at(100 + LEASE_MILLIS),
254        "lease expiry must be now plus the lease duration"
255    );
256}
257
258fn held_pact_not_reclaimable_before_expiry<R, F>(make: &F)
259where
260    R: Registry,
261    R::Error: Debug,
262    F: Fn(Vec<Pact>, u64) -> R,
263{
264    let registry = make(vec![a_pact()], LEASE_MILLIS);
265    let _first = registry
266        .claim(&[DOCKET], at(0))
267        .expect("claim should not error")
268        .expect("a pact should be claimable");
269    assert!(
270        registry
271            .claim(&[DOCKET], at(500))
272            .expect("claim should not error")
273            .is_none(),
274        "a held pact must not be reclaimable before its lease expires"
275    );
276}
277
278fn expired_lease_lapses_and_reclaims_with_rotated_retainer<R, F>(make: &F)
279where
280    R: Registry,
281    R::Error: Debug,
282    F: Fn(Vec<Pact>, u64) -> R,
283{
284    let registry = make(vec![a_pact()], LEASE_MILLIS);
285    let first = registry
286        .claim(&[DOCKET], at(0))
287        .expect("claim should not error")
288        .expect("a pact should be claimable");
289    let second = registry
290        .claim(&[DOCKET], at(1500))
291        .expect("claim should not error")
292        .expect("an expired pact should be reclaimable through the claim path");
293    assert_ne!(
294        first.retainer.id(),
295        second.retainer.id(),
296        "reclaiming a lapsed pact must rotate the retainer"
297    );
298    assert_eq!(
299        second.lease_expiry,
300        at(1500 + LEASE_MILLIS),
301        "the reclaim must set a fresh lease"
302    );
303}
304
305fn stale_retainer_settle_rejected_after_reclaim<R, F>(make: &F)
306where
307    R: Registry,
308    R::Error: Debug,
309    F: Fn(Vec<Pact>, u64) -> R,
310{
311    let registry = make(vec![a_pact()], LEASE_MILLIS);
312    let first = registry
313        .claim(&[DOCKET], at(0))
314        .expect("claim should not error")
315        .expect("a pact should be claimable");
316    let _second = registry
317        .claim(&[DOCKET], at(1500))
318        .expect("claim should not error")
319        .expect("an expired pact should be reclaimable");
320    assert!(
321        registry.fulfill(&first.retainer).is_err(),
322        "the prior holder must not settle after a reclaim (at-least-once safety)"
323    );
324}
325
326fn late_fulfill_before_reclaim_succeeds<R, F>(make: &F)
327where
328    R: Registry,
329    R::Error: Debug,
330    F: Fn(Vec<Pact>, u64) -> R,
331{
332    let registry = make(vec![a_pact()], LEASE_MILLIS);
333    let claim = registry
334        .claim(&[DOCKET], at(0))
335        .expect("claim should not error")
336        .expect("a pact should be claimable");
337    // The lease has expired but nobody reclaimed; the holder's retainer still
338    // matches, so a late fulfill of genuinely-done work settles. No time involved.
339    assert!(
340        registry.fulfill(&claim.retainer).is_ok(),
341        "a late fulfill before any reclaim must settle"
342    );
343    assert!(
344        registry
345            .claim(&[DOCKET], at(9999))
346            .expect("claim should not error")
347            .is_none(),
348        "a settled pact must not be claimable"
349    );
350}
351
352fn fulfill_settles_and_pact_not_claimable<R, F>(make: &F)
353where
354    R: Registry,
355    R::Error: Debug,
356    F: Fn(Vec<Pact>, u64) -> R,
357{
358    let registry = make(vec![a_pact()], LEASE_MILLIS);
359    let claim = registry
360        .claim(&[DOCKET], at(0))
361        .expect("claim should not error")
362        .expect("a pact should be claimable");
363    registry
364        .fulfill(&claim.retainer)
365        .expect("fulfill should settle");
366    assert!(
367        registry
368            .claim(&[DOCKET], at(0))
369            .expect("claim should not error")
370            .is_none(),
371        "a fulfilled pact must not be claimable"
372    );
373}
374
375fn breach_settles_terminally<R, F>(make: &F)
376where
377    R: Registry,
378    R::Error: Debug,
379    F: Fn(Vec<Pact>, u64) -> R,
380{
381    let registry = make(vec![a_pact()], LEASE_MILLIS);
382    let claim = registry
383        .claim(&[DOCKET], at(0))
384        .expect("claim should not error")
385        .expect("a pact should be claimable");
386    registry
387        .breach(&claim.retainer)
388        .expect("breach should settle");
389    assert!(
390        registry
391            .claim(&[DOCKET], at(5000))
392            .expect("claim should not error")
393            .is_none(),
394        "a breached pact must not be claimable, even after its lease would have expired"
395    );
396}
397
398fn released_pact_withheld_until_reclaimable<R, F>(make: &F)
399where
400    R: Registry,
401    R::Error: Debug,
402    F: Fn(Vec<Pact>, u64) -> R,
403{
404    let registry = make(vec![a_pact()], LEASE_MILLIS);
405    let claim = registry
406        .claim(&[DOCKET], at(0))
407        .expect("claim should not error")
408        .expect("a pact should be claimable");
409    registry
410        .release(&claim.retainer, at(5000))
411        .expect("release should succeed for the current holder");
412    // at(3000) is past the original lease (1000) — a lapse would make it claimable —
413    // but the reclaimable instant (5000) is later, so release must withhold it.
414    assert!(
415        registry
416            .claim(&[DOCKET], at(3000))
417            .expect("claim should not error")
418            .is_none(),
419        "a released pact must not be claimable before its reclaimable instant"
420    );
421}
422
423fn released_pact_reclaimable_at_its_instant<R, F>(make: &F)
424where
425    R: Registry,
426    R::Error: Debug,
427    F: Fn(Vec<Pact>, u64) -> R,
428{
429    let registry = make(vec![a_pact()], LEASE_MILLIS);
430    let first = registry
431        .claim(&[DOCKET], at(0))
432        .expect("claim should not error")
433        .expect("a pact should be claimable");
434    registry
435        .release(&first.retainer, at(5000))
436        .expect("release should succeed");
437    let second = registry
438        .claim(&[DOCKET], at(5000))
439        .expect("claim should not error")
440        .expect("a released pact must be claimable at its reclaimable instant");
441    assert_ne!(
442        first.retainer.id(),
443        second.retainer.id(),
444        "reclaiming a released pact must rotate the retainer"
445    );
446}
447
448fn immediate_reclaim_reclaims_like_lapse<R, F>(make: &F)
449where
450    R: Registry,
451    R::Error: Debug,
452    F: Fn(Vec<Pact>, u64) -> R,
453{
454    let registry = make(vec![a_pact()], LEASE_MILLIS);
455    let claim = registry
456        .claim(&[DOCKET], at(0))
457        .expect("claim should not error")
458        .expect("a pact should be claimable");
459    registry
460        .release(&claim.retainer, at(0))
461        .expect("release with an immediate reclaim should succeed");
462    assert!(
463        registry
464            .claim(&[DOCKET], at(0))
465            .expect("claim should not error")
466            .is_some(),
467        "an immediate reclaim must make the pact claimable at once, as a voluntary lapse"
468    );
469}
470
471fn release_rotates_authority_from_prior_holder<R, F>(make: &F)
472where
473    R: Registry,
474    R::Error: Debug,
475    F: Fn(Vec<Pact>, u64) -> R,
476{
477    let registry = make(vec![a_pact()], LEASE_MILLIS);
478    let claim = registry
479        .claim(&[DOCKET], at(0))
480        .expect("claim should not error")
481        .expect("a pact should be claimable");
482    registry
483        .release(&claim.retainer, at(0))
484        .expect("release should succeed");
485    assert!(
486        registry.fulfill(&claim.retainer).is_err(),
487        "the prior holder must not settle after releasing (release rotates authority)"
488    );
489}
490
491fn heartbeat_extends_lease_preventing_lapse<R, F>(make: &F)
492where
493    R: Registry,
494    R::Error: Debug,
495    F: Fn(Vec<Pact>, u64) -> R,
496{
497    let registry = make(vec![a_pact()], LEASE_MILLIS);
498    let claim = registry
499        .claim(&[DOCKET], at(0))
500        .expect("claim should not error")
501        .expect("a pact should be claimable");
502    registry
503        .heartbeat(&claim.retainer, at(800))
504        .expect("an in-window heartbeat should extend the lease");
505    // The original lease (expiry 1000) would have lapsed by 1500, but the
506    // heartbeat pushed expiry to 1800, so the pact is still held.
507    assert!(
508        registry
509            .claim(&[DOCKET], at(1500))
510            .expect("claim should not error")
511            .is_none(),
512        "a heartbeat within the window must prevent a lapse"
513    );
514}
515
516fn heartbeat_on_lapsed_lease_rejected<R, F>(make: &F)
517where
518    R: Registry,
519    R::Error: Debug,
520    F: Fn(Vec<Pact>, u64) -> R,
521{
522    let registry = make(vec![a_pact()], LEASE_MILLIS);
523    let claim = registry
524        .claim(&[DOCKET], at(0))
525        .expect("claim should not error")
526        .expect("a pact should be claimable");
527    assert!(
528        registry.heartbeat(&claim.retainer, at(1200)).is_err(),
529        "a heartbeat after the lease expired must be rejected, forcing a re-claim"
530    );
531}
532
533fn heartbeat_unknown_retainer_rejected<R, F>(make: &F)
534where
535    R: Registry,
536    R::Error: Debug,
537    F: Fn(Vec<Pact>, u64) -> R,
538{
539    let registry = make(vec![a_pact()], LEASE_MILLIS);
540    let _claim = registry
541        .claim(&[DOCKET], at(0))
542        .expect("claim should not error")
543        .expect("a pact should be claimable");
544    let unknown = Retainer::new(Uuid::new_v4());
545    assert!(
546        registry.heartbeat(&unknown, at(100)).is_err(),
547        "a heartbeat with an unissued retainer must be rejected"
548    );
549}