1use std::collections::{BTreeMap, BTreeSet};
15
16use kernel::Locator;
17
18use crate::sieve::CommandKey;
19
20#[derive(Debug, Default, Clone, PartialEq, Eq)]
23pub struct SieveHistory(BTreeMap<CommandKey, Locator>);
24
25impl SieveHistory {
26 #[must_use]
27 pub fn previous(&self, key: &CommandKey) -> Option<&Locator> {
28 self.0.get(key)
29 }
30
31 pub fn record(&mut self, key: CommandKey, original: Locator) {
32 self.0.insert(key, original);
33 }
34}
35
36const UNCHANGED_RUN_FOLDS_AT: usize = 2;
39
40fn unchanged_line(count: usize) -> String {
41 format!("[unchanged: {count} lines, as in the previous run]")
42}
43
44pub(crate) fn only_changes(current: &[String], previous: &[String]) -> Vec<String> {
50 let seen: BTreeSet<&str> = previous.iter().map(String::as_str).collect();
51 let mut out = Vec::with_capacity(current.len());
52 let mut run: Vec<&String> = Vec::new();
53 let flush = |run: &mut Vec<&String>, out: &mut Vec<String>| {
54 let bytes: usize = run.iter().map(|l| l.len().saturating_add(1)).sum();
55 let count = run.len();
56 if count >= UNCHANGED_RUN_FOLDS_AT && unchanged_line(count).len() < bytes {
57 out.push(unchanged_line(count));
58 } else {
59 out.extend(run.iter().map(|l| (*l).clone()));
60 }
61 run.clear();
62 };
63 for line in current {
64 if seen.contains(line.as_str()) {
65 run.push(line);
66 } else {
67 flush(&mut run, &mut out);
68 out.push(line.clone());
69 }
70 }
71 flush(&mut run, &mut out);
72 out
73}
74
75#[cfg(test)]
76#[allow(
77 clippy::unwrap_used,
78 clippy::expect_used,
79 clippy::panic,
80 reason = "test code"
81)]
82mod tests {
83 use super::*;
84
85 fn lines(text: &str) -> Vec<String> {
86 text.lines().map(str::to_owned).collect()
87 }
88
89 #[test]
90 fn only_the_new_lines_are_spelled_out() {
91 let previous = lines("a long enough line 1\na long enough line 2\na long enough line 3\nb");
92 let current =
93 lines("a long enough line 1\na long enough line 2\na long enough line 3\nNEW\nb");
94 let out = only_changes(¤t, &previous);
95 assert_eq!(
96 out,
97 vec!["[unchanged: 3 lines, as in the previous run]", "NEW", "b"]
98 );
99 }
100
101 #[test]
102 fn a_short_run_is_left_as_it_was_and_a_url_is_never_folded() {
103 let previous = lines("x\ny\nhttps://a.b/c\nhttps://d.e/f");
104 let current = previous.clone();
105 assert_eq!(only_changes(¤t, &previous), current);
106 }
107
108 #[test]
109 fn the_history_keeps_the_latest_per_key() {
110 let key = CommandKey::of(&kernel::ExecArm::Shell {
111 text: "ls".to_owned(),
112 });
113 let mut history = SieveHistory::default();
114 assert!(history.previous(&key).is_none());
115 let first = Locator::parse(&format!("cas:b3-{}", "0".repeat(64))).unwrap();
116 let second = Locator::parse(&format!("cas:b3-{}", "1".repeat(64))).unwrap();
117 history.record(key.clone(), first);
118 history.record(key.clone(), second.clone());
119 assert_eq!(history.previous(&key), Some(&second));
120 }
121}