Skip to main content

runtime/sieve/
diff.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! What this run already saw from the same command, and only what
7//! changed since. `cargo check` run ten times in a development loop
8//! prints nine outputs that are mostly the previous one; the model is
9//! better served by "this error is new" than by reading both.
10//!
11//! The history holds locators, never text: the original is in the CAS
12//! by invariant 3, and the previous output is read back from there.
13
14use std::collections::{BTreeMap, BTreeSet};
15
16use kernel::Locator;
17
18use crate::sieve::CommandKey;
19
20/// The last original this run saw per command key. Owned by whoever
21/// owns the run; one per run, never shared across runs.
22#[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
36/// A run of unchanged lines at least this long is said as a count.
37/// Below it the count line would be as long as what it replaces.
38const 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
44/// Current lines with every run of lines the previous output also
45/// contained collapsed to a count. Membership, not alignment: a line
46/// that moved is still not news. A protected line is not rewritten by
47/// this — it is counted, verbatim in the rest file, and was already
48/// read once — so a repeated diagnostic block folds whole.
49pub(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(&current, &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(&current, &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}