Skip to main content

ev/
selected.rs

1//! The external selected-list: which check refs the latest diff selected, and which declared
2//! triggers it changed. An affinity tool / CI writes results/selected.json; ev READS it and
3//! never recomputes affinity. Absent ⇒ L2 (silently-unbound) is not evaluated.
4use crate::store::Store;
5use serde_json::Value;
6
7#[derive(Debug, Clone, PartialEq, Default)]
8pub struct SelectedList {
9    pub commit: String,        // the diff's commit (informational)
10    pub changed: Vec<String>,  // declared triggers/paths the diff touched
11    pub selected: Vec<String>, // check refs the diff selected
12}
13
14fn str_array(obj: &serde_json::Map<String, Value>, k: &str) -> Result<Vec<String>, String> {
15    match obj.get(k) {
16        None => Ok(Vec::new()),
17        Some(Value::Array(a)) => a
18            .iter()
19            .map(|e| {
20                e.as_str()
21                    .map(|s| s.to_string())
22                    .ok_or(format!("selected-list.{k} element is not a string"))
23            })
24            .collect(),
25        Some(_) => Err(format!("selected-list.{k} must be an array")),
26    }
27}
28
29/// Strict parse of a selected-list value (closed schema; missing arrays default to empty).
30pub fn from_value(v: &Value) -> Result<SelectedList, String> {
31    let obj = v.as_object().ok_or("selected-list is not an object")?;
32    crate::tick::only_keys(obj, &["commit", "changed", "selected"], "selected-list")?;
33    let commit = obj
34        .get("commit")
35        .and_then(|x| x.as_str())
36        .unwrap_or("")
37        .to_string();
38    Ok(SelectedList {
39        commit,
40        changed: str_array(obj, "changed")?,
41        selected: str_array(obj, "selected")?,
42    })
43}
44
45/// Read results/selected.json, or None if it is absent. Errors on malformed JSON / schema.
46pub fn read(store: &Store) -> std::io::Result<Option<SelectedList>> {
47    let path = store.root.join("results").join("selected.json");
48    let text = match std::fs::read_to_string(&path) {
49        Ok(t) => t,
50        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
51        Err(e) => return Err(e),
52    };
53    let v: Value = serde_json::from_str(&text)
54        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
55    from_value(&v)
56        .map(Some)
57        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use crate::store::Store;
64
65    fn store() -> (std::path::PathBuf, Store) {
66        use std::sync::atomic::{AtomicU64, Ordering};
67        static N: AtomicU64 = AtomicU64::new(0);
68        let p = std::env::temp_dir().join(format!(
69            "ev-selected-{}-{}",
70            std::process::id(),
71            N.fetch_add(1, Ordering::Relaxed)
72        ));
73        let _ = std::fs::remove_dir_all(&p);
74        std::fs::create_dir_all(&p).unwrap();
75        let s = Store::at(&p);
76        s.init().unwrap();
77        (p, s)
78    }
79
80    #[test]
81    fn read_should_parse_the_list_when_results_selected_json_exists() {
82        // given: a store with a written selected-list
83        let (_p, s) = store();
84        std::fs::write(
85            s.root.join("results").join("selected.json"),
86            r#"{"commit":"d308afac1b2c3d4e5f60718293a4b5c6d7e8f901","changed":["pyproject.toml"],"selected":["pytest x"]}"#,
87        )
88        .unwrap();
89
90        // when: the selected-list is read
91        let sl = read(&s).unwrap().expect("present");
92
93        // then: its changed and selected sets round-trip
94        assert_eq!(sl.changed, vec!["pyproject.toml".to_string()]);
95        assert_eq!(sl.selected, vec!["pytest x".to_string()]);
96    }
97
98    #[test]
99    fn read_should_be_none_when_no_selected_list_exists() {
100        // given: a store with no selected-list
101        let (_p, s) = store();
102
103        // when: the selected-list is read
104        let sl = read(&s).unwrap();
105
106        // then: it is None (L2 simply not evaluated)
107        assert!(sl.is_none());
108    }
109
110    #[test]
111    fn from_value_should_reject_the_list_when_it_has_an_unknown_field() {
112        // given: a selected-list value with a field outside the closed schema
113        let v = serde_json::json!({ "commit": "x", "selected": [], "health": 1 });
114
115        // when: it is parsed
116        let parsed = from_value(&v);
117
118        // then: parsing fails
119        assert!(parsed.is_err());
120    }
121}