1use crate::consts_policy::LOOP_REPEAT_THRESHOLD;
12use crate::locator::B3Hash;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct ActionFingerprint(B3Hash);
18
19impl ActionFingerprint {
20 pub fn derive(action_canonical: &[u8]) -> ActionFingerprint {
21 ActionFingerprint(B3Hash::digest(action_canonical))
22 }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum StallVerdict {
28 Ok,
29 Stall { repeats: u32 },
30}
31
32pub fn observe(recent: &[ActionFingerprint]) -> StallVerdict {
37 let Some(last) = recent.last() else {
38 return StallVerdict::Ok;
39 };
40 let mut repeats: u32 = 0;
41 for print in recent.iter().rev() {
42 if print == last {
43 repeats = repeats.saturating_add(1);
44 } else {
45 break;
46 }
47 }
48 if repeats >= LOOP_REPEAT_THRESHOLD {
49 StallVerdict::Stall { repeats }
50 } else {
51 StallVerdict::Ok
52 }
53}
54
55#[cfg(test)]
56#[allow(
57 clippy::unwrap_used,
58 clippy::expect_used,
59 clippy::panic,
60 clippy::indexing_slicing,
61 reason = "test code"
62)]
63mod tests {
64 use super::*;
65 use proptest::prelude::*;
66
67 fn print(label: &str) -> ActionFingerprint {
68 ActionFingerprint::derive(label.as_bytes())
69 }
70
71 #[test]
72 fn empty_and_varied_histories_are_ok() {
73 assert_eq!(observe(&[]), StallVerdict::Ok);
74 assert_eq!(
75 observe(&[print("a"), print("b"), print("c")]),
76 StallVerdict::Ok
77 );
78 }
79
80 #[test]
81 fn three_identical_tail_actions_stall() {
82 let sample = [print("x"), print("loop"), print("loop"), print("loop")];
83 assert_eq!(observe(&sample), StallVerdict::Stall { repeats: 3 });
84 }
85
86 #[test]
87 fn a_broken_repetition_recovered_itself() {
88 let sample = [
91 print("loop"),
92 print("loop"),
93 print("fresh"),
94 print("loop"),
95 print("loop"),
96 ];
97 assert_eq!(observe(&sample), StallVerdict::Ok);
98 }
99
100 proptest! {
101 #[test]
103 fn stall_iff_tail_run_reaches_threshold(labels in proptest::collection::vec("[ab]", 1..12)) {
104 let prints: Vec<ActionFingerprint> =
105 labels.iter().map(|l| print(l)).collect();
106 let last = prints.last().unwrap();
107 let tail = prints.iter().rev().take_while(|p| *p == last).count();
108 let verdict = observe(&prints);
109 if tail >= usize::try_from(LOOP_REPEAT_THRESHOLD).unwrap() {
110 prop_assert_eq!(verdict, StallVerdict::Stall { repeats: u32::try_from(tail).unwrap() });
111 } else {
112 prop_assert_eq!(verdict, StallVerdict::Ok);
113 }
114 }
115 }
116}