1#![forbid(unsafe_code)]
10#![warn(missing_docs)]
11
12use std::fmt::Debug;
13
14use pacta_contract::{Pact, Registry, Retainer, Timestamp};
15use uuid::Uuid;
16
17pub 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
39pub 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#[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 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 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 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 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 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(®);
170 let retainer = retainer.clone();
171 std::thread::spawn(move || block_on(reg.fulfill(&retainer)))
172 };
173 let b = {
174 let reg = Arc::clone(®);
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 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 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 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}