tests.rsannotatedtests.rssource427 lines · 16.8 KB · raw
1//! The engine on a small domain of its own, a change to be reviewed, so that
2//! nothing here depends on any host's vocabulary.
3
4use super::*;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
7struct Change;
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
10enum F {
11    Code,
12    Tests,
13    Lines,
14    Reviewed,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18enum V {
19    Bool(bool),
20    Given,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24struct Review;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27struct Reject;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30enum N {
31    AskForTests,
32    Approve,
33}
34
35impl Domain for Change {
36    type Fact = F;
37    type Value = V;
38    type Effect = Review;
39    type End = Reject;
40    type Note = N;
41
42    fn facts() -> &'static [F] {
43        &[F::Code, F::Tests, F::Lines, F::Reviewed]
44    }
45    fn fact_name(fact: F) -> &'static str {
46        match fact {
47            F::Code => "touches code",
48            F::Tests => "touches tests",
49            F::Lines => "lines changed",
50            F::Reviewed => "reviewed",
51        }
52    }
53    fn values(fact: F) -> &'static [V] {
54        match fact {
55            F::Lines => &[V::Given],
56            _ => &[V::Bool(true), V::Bool(false)],
57        }
58    }
59    fn value_name(value: V) -> &'static str {
60        match value {
61            V::Bool(true) => "yes",
62            V::Bool(false) => "no",
63            V::Given => "known",
64        }
65    }
66    fn asked_for(fact: F) -> bool {
67        fact != F::Reviewed
68    }
69    fn teaches(_: Review) -> F {
70        F::Reviewed
71    }
72    fn effect_name(_: Review) -> String {
73        "review".to_owned()
74    }
75    fn end_name(_: Reject) -> String {
76        "reject".to_owned()
77    }
78    fn note_name(note: N) -> String {
79        match note {
80            N::AskForTests => "ask for tests",
81            N::Approve => "approve",
82        }
83        .to_owned()
84    }
85    fn parse_effect(name: &str) -> Result<Review, String> {
86        (name == "review").then_some(Review).ok_or_else(|| format!("no effect `{name}`"))
87    }
88    // Ends are not allowed in loaded rules: this domain keeps `reject` for its own.
89    fn parse_note(name: &str) -> Result<N, String> {
90        match name {
91            "ask for tests" => Ok(N::AskForTests),
92            "approve" => Ok(N::Approve),
93            _ => Err(format!("no note `{name}`")),
94        }
95    }
96}
97
98const YES: V = V::Bool(true);
99const NO: V = V::Bool(false);
100const CODE: Test<Change> = Test::Is(F::Code, YES);
101
102const RULES: &[Rule<'static, Change>] = &[
103    Rule { name: "no code", when: &[Test::Is(F::Code, NO)], then: Then::Note(N::Approve) },
104    Rule { name: "tests missing", when: &[CODE, Test::Is(F::Tests, NO)], then: Then::Note(N::AskForTests) },
105    Rule { name: "look at it", when: &[CODE, Test::Known(F::Lines)], then: Then::Do(Review) },
106    Rule { name: "turn it down", when: &[CODE, Test::Is(F::Reviewed, NO)], then: Then::End(Reject) },
107    Rule { name: "approved", when: &[CODE, Test::Is(F::Reviewed, YES)], then: Then::Note(N::Approve) },
108];
109
110fn known(facts: &[(F, V)]) -> Known<Change> {
111    let mut known = Known::default();
112    for (fact, value) in facts {
113        known.learn(*fact, *value);
114    }
115    known
116}
117
118fn network() -> Network<Change> {
119    Network::compile(RULES)
120}
121
122#[test]
123fn with_nothing_known_every_asked_fact_a_live_rule_waits_on_is_asked_at_once() {
124    // `reviewed` is waited on too, but an effect teaches it: it is never asked for.
125    assert_eq!(network().next(&Known::default()), Next::Ask(vec![F::Code, F::Tests, F::Lines]));
126}
127
128#[test]
129fn a_fact_no_live_rule_waits_on_is_not_asked() {
130    // No code: the four rules that need it fail, and the one left is decided.
131    let some = known(&[(F::Code, NO)]);
132    assert_eq!(network().next(&some), Next::Done);
133    assert_eq!(network().holding(&some), ["no code"]);
134    // Tests are untouched: only `lines changed` is still wanted, by "look at it".
135    let some = known(&[(F::Code, YES), (F::Tests, NO)]);
136    assert_eq!(network().next(&some), Next::Ask(vec![F::Lines]));
137}
138
139#[test]
140fn rules_share_the_tests_they_have_in_common() {
141    let network = network();
142    let tests: usize = RULES.iter().map(|rule| rule.when.len()).sum();
143    assert_eq!(tests, 9);
144    // Four rules begin with the same test, so it is one alpha and one join.
145    assert_eq!(network.alphas.iter().filter(|alpha| **alpha == CODE).count(), 1);
146    assert_eq!(network.joins.iter().filter(|join| join.depth == 1 && network.alphas[join.alpha] == CODE).count(), 1);
147    assert_eq!(network.terminals.len(), RULES.len());
148    assert!(network.joins.len() < tests);
149}
150
151#[test]
152fn of_the_rules_that_hold_the_first_with_something_to_do_decides() {
153    let network = network();
154    let mut all = known(&[(F::Code, YES), (F::Tests, YES), (F::Lines, V::Given)]);
155    let (by, next) = network.decide(&all);
156    assert_eq!((by.map(|rule| network.terminals[rule].name.as_str()), next), (Some("look at it"), Next::Do(Review)));
157    // Once the effect has taught its fact it is done, and the rules that follow decide.
158    all.learn(F::Reviewed, NO);
159    assert_eq!(network.next(&all), Next::End(Reject));
160    all.learn(F::Reviewed, YES);
161    assert_eq!(network.next(&all), Next::Done);
162    assert_eq!(network.holding(&all), ["look at it", "approved"]);
163}
164
165#[test]
166fn a_note_never_decides_a_step() {
167    let network = network();
168    let some = known(&[(F::Code, YES), (F::Tests, NO)]);
169    assert_eq!(network.holding(&some), ["tests missing"]);
170    assert_eq!(network.decide(&some).0, None);
171    assert!(network.held(&some).any(|terminal| terminal.then == Then::Note(N::AskForTests)));
172}
173
174#[test]
175fn a_failed_test_fails_everything_after_it_and_nothing_before() {
176    let network = network();
177    let some = known(&[(F::Code, YES), (F::Tests, YES)]);
178    let by_name = |name: &str| network.terminals.iter().find(|terminal| terminal.name == name).unwrap();
179    assert_eq!(network.join(by_name("tests missing").join, &some), State::Fails);
180    assert_eq!(network.join(by_name("look at it").join, &some), State::Waits);
181    let first = network.joins[by_name("look at it").join].left.unwrap();
182    assert_eq!(network.join(first, &some), State::Holds);
183}
184
185#[test]
186fn only_what_a_holding_rule_stands_on_is_used() {
187    let network = network();
188    let some = known(&[(F::Code, YES), (F::Tests, NO), (F::Lines, V::Given)]);
189    let (_, alphas) = network.used(&some);
190    let used: Vec<Test<Change>> = network.alphas.iter().zip(&alphas).filter(|(_, used)| **used).map(|(test, _)| *test).collect();
191    // "tests missing" and "look at it" both hold; "no code" and the rest do not.
192    assert_eq!(used, [CODE, Test::Is(F::Tests, NO), Test::Known(F::Lines)]);
193}
194
195/// Answers what a test sets, and counts its requests.
196struct Fixed {
197    facts: Vec<(F, V)>,
198    asks: Vec<Vec<F>>,
199    performed: Vec<Review>,
200    reviewed: V,
201}
202
203impl Host<Change> for Fixed {
204    fn ask(&mut self, facts: &[F]) -> Vec<(F, V)> {
205        self.asks.push(facts.to_vec());
206        self.facts.iter().filter(|(fact, _)| facts.contains(fact)).copied().collect()
207    }
208    fn perform(&mut self, effect: Review) -> Vec<(F, V)> {
209        self.performed.push(effect);
210        vec![(F::Reviewed, self.reviewed)]
211    }
212}
213
214fn fixed(reviewed: V) -> Fixed {
215    Fixed {
216        facts: vec![(F::Code, YES), (F::Tests, YES), (F::Lines, V::Given)],
217        asks: Vec::new(),
218        performed: Vec::new(),
219        reviewed,
220    }
221}
222
223#[test]
224fn a_run_asks_once_and_then_does_what_the_rules_say() {
225    let network = network();
226    let mut host = fixed(YES);
227    let run = network.run(Known::default(), &mut host);
228    assert_eq!(run.outcome, Outcome::Done);
229    assert_eq!(host.asks, [vec![F::Code, F::Tests, F::Lines]], "one request carries every question");
230    assert_eq!(host.performed, [Review]);
231    assert_eq!(network.holding(&run.known), ["look at it", "approved"]);
232    assert_eq!(run.trace.fired(), [2]);
233}
234
235#[test]
236fn a_run_stops_where_a_rule_ends_it() {
237    let network = network();
238    let run = network.run(Known::default(), &mut fixed(NO));
239    assert_eq!(run.outcome, Outcome::Ended(Reject));
240    assert_eq!(run.trace.fired(), [2, 3], "the effect, then the end");
241}
242
243#[test]
244fn a_run_stops_rather_than_loops_when_a_source_does_not_answer() {
245    let network = network();
246    let mut host = fixed(YES);
247    host.facts.retain(|(fact, _)| *fact != F::Lines);
248    let run = network.run(Known::default(), &mut host);
249    assert_eq!(run.outcome, Outcome::Stuck(vec![F::Lines]));
250    assert_eq!(host.asks.len(), 1);
251}
252
253#[test]
254fn the_trace_is_names_ids_and_counts_as_data() {
255    let network = network();
256    let run = network.run(Known::default(), &mut fixed(YES));
257    let records = run.trace.records();
258    let events: Vec<&str> = records.iter().map(|record| record.event).collect();
259    assert_eq!(events, ["asked", "learned", "fired", "learned", "done"]);
260    assert_eq!(records[0].facts, ["touches code", "touches tests", "lines changed"]);
261    assert_eq!(records[0].count, 3);
262    let fired = &records[2];
263    assert_eq!((fired.rule, fired.name.as_deref(), fired.then.as_deref()), (Some(2), Some("look at it"), Some("do review")));
264    assert_eq!(fired.facts, ["touches code", "lines changed"]);
265    assert_eq!(fired.values, ["yes", "known"]);
266    assert_eq!(records.iter().map(|record| record.step).collect::<Vec<_>>(), [0, 1, 2, 3, 4]);
267    let line = serde_json::to_string(fired).unwrap();
268    assert_eq!(
269        line,
270        r#"{"step":2,"event":"fired","rule":2,"name":"look at it","then":"do review","facts":["touches code","lines changed"],"values":["yes","known"],"count":2}"#
271    );
272}
273
274#[test]
275fn a_step_taken_by_hand_can_be_explained_the_same_way() {
276    let network = network();
277    let some = known(&[(F::Code, YES), (F::Tests, YES), (F::Lines, V::Given)]);
278    let (by, _) = network.decide(&some);
279    let firing = network.explain(by.unwrap(), &some);
280    assert_eq!((firing.rule, firing.name.as_str()), (2, "look at it"));
281    assert_eq!(firing.on, [(F::Code, YES), (F::Lines, V::Given)]);
282}
283
284// ## Rules as data
285
286const SAMPLE: &str = r#"{ "rules": [
287    { "name": "tests untouched",
288      "when": [ { "fact": "touches code", "is": "yes" }, { "fact": "touches tests", "is": "no" } ],
289      "then": { "note": "ask for tests" } },
290    { "name": "size known", "when": [ { "known": "lines changed" } ], "then": { "do": "review" } }
291] }"#;
292
293fn problems(json: &str, taken: &[&str]) -> Vec<Problem> {
294    load::<Change>(json, taken)
295        .expect_err("it should not load")
296        .into_iter()
297        .map(|error| match error {
298            LoadError::Rule { problem, .. } => problem,
299            LoadError::Syntax(message) => panic!("syntax: {message}"),
300        })
301        .collect()
302}
303
304fn one(when: &str, then: &str) -> String {
305    format!(r#"{{ "rules": [ {{ "name": "r", "when": [ {when} ], "then": {then} }} ] }}"#)
306}
307
308#[test]
309fn a_rules_file_loads_and_compiles_beside_the_rules_written_in_code() {
310    let loaded = load::<Change>(SAMPLE, &["no code"]).unwrap();
311    assert_eq!(loaded.len(), 2);
312    assert_eq!(loaded[0].when, [CODE, Test::Is(F::Tests, NO)]);
313    assert_eq!(loaded[1].then, Then::Do(Review));
314    // Compiled-in rules first, so they outrank the loaded ones.
315    let mut rules: Vec<Rule<Change>> = RULES.to_vec();
316    rules.extend(loaded.iter().map(LoadedRule::rule));
317    let network = Network::compile(&rules);
318    assert_eq!(network.terminals.len(), 7);
319    let all = known(&[(F::Code, YES), (F::Tests, NO)]);
320    assert_eq!(network.holding(&all), ["tests missing", "tests untouched"]);
321    // The loaded rule shares the compiled ones' first test.
322    assert_eq!(network.alphas.iter().filter(|alpha| **alpha == CODE).count(), 1);
323}
324
325#[test]
326fn what_the_domain_writes_as_code_loads_back_as_the_same_network() {
327    let specs = export(RULES);
328    let loaded = load_specs::<Change>(&specs, &[]);
329    // `turn it down` ends the query, which this domain does not allow a loaded rule to do.
330    let errors = loaded.err().unwrap();
331    assert_eq!(errors.len(), 1);
332    assert!(matches!(&errors[0], LoadError::Rule { name, problem: Problem::Refused { kind: "end", .. }, .. } if name == "turn it down"));
333
334    let allowed: Vec<Spec> = specs.into_iter().filter(|spec| spec.name != "turn it down").collect();
335    let loaded = load_specs::<Change>(&allowed, &[]).unwrap();
336    let rules: Vec<Rule<Change>> = loaded.iter().map(LoadedRule::rule).collect();
337    let direct: Vec<Rule<Change>> = RULES.iter().filter(|rule| rule.name != "turn it down").copied().collect();
338    let (a, b) = (Network::compile(&rules), Network::compile(&direct));
339    assert_eq!((a.alphas, a.joins), (b.alphas, b.joins));
340    let names = |network: &Network<Change>| network.terminals.iter().map(|t| (t.name.clone(), t.join, t.then)).collect::<Vec<_>>();
341    assert_eq!(names(&Network::compile(&rules)), names(&Network::compile(&direct)));
342}
343
344#[test]
345fn an_unknown_fact_fails_at_load() {
346    let json = one(r#"{ "fact": "touches docs", "is": "yes" }"#, r#"{ "note": "approve" }"#);
347    assert_eq!(problems(&json, &[]), [Problem::UnknownFact { test: 0, fact: "touches docs".to_owned() }]);
348    let json = one(r#"{ "known": "docs" }"#, r#"{ "note": "approve" }"#);
349    assert_eq!(problems(&json, &[]), [Problem::UnknownFact { test: 0, fact: "docs".to_owned() }]);
350}
351
352#[test]
353fn a_value_the_fact_cannot_have_fails_at_load() {
354    let json = one(r#"{ "fact": "touches code", "is": "maybe" }"#, r#"{ "note": "approve" }"#);
355    assert_eq!(
356        problems(&json, &[]),
357        [Problem::WrongValue {
358            test: 0,
359            fact: "touches code".to_owned(),
360            value: "maybe".to_owned(),
361            allowed: vec!["yes".to_owned(), "no".to_owned()]
362        }]
363    );
364    // `lines changed` is only ever known, so it cannot be tested as yes.
365    let json = one(r#"{ "fact": "lines changed", "is": "yes" }"#, r#"{ "note": "approve" }"#);
366    assert!(matches!(problems(&json, &[])[..], [Problem::WrongValue { .. }]));
367}
368
369#[test]
370fn an_effect_end_or_note_the_domain_does_not_allow_fails_at_load() {
371    let when = r#"{ "known": "lines changed" }"#;
372    assert!(matches!(&problems(&one(when, r#"{ "do": "deploy" }"#), &[])[..], [Problem::Refused { kind: "effect", name, .. }] if name == "deploy"));
373    assert!(matches!(&problems(&one(when, r#"{ "end": "reject" }"#), &[])[..], [Problem::Refused { kind: "end", .. }]));
374    assert!(matches!(&problems(&one(when, r#"{ "note": "merge" }"#), &[])[..], [Problem::Refused { kind: "note", .. }]));
375}
376
377#[test]
378fn a_malformed_rule_fails_at_load() {
379    let note = r#"{ "note": "approve" }"#;
380    // Neither `is` nor `known`; both; `is` without a fact.
381    assert_eq!(problems(&one(r#"{ "fact": "touches code" }"#, note), &[]), [Problem::MalformedTest { test: 0 }]);
382    assert_eq!(
383        problems(&one(r#"{ "fact": "touches code", "is": "yes", "known": "touches code" }"#, note), &[]),
384        [Problem::MalformedTest { test: 0 }]
385    );
386    assert_eq!(problems(&one(r#"{ "is": "yes" }"#, note), &[]), [Problem::MalformedTest { test: 0 }]);
387    // No tests, and a `then` that does two things or none.
388    assert_eq!(problems(&one("", note), &[]), [Problem::NoTests]);
389    let when = r#"{ "known": "lines changed" }"#;
390    assert_eq!(problems(&one(when, r#"{ "do": "review", "note": "approve" }"#), &[]), [Problem::MalformedThen]);
391    assert_eq!(problems(&one(when, "{}"), &[]), [Problem::MalformedThen]);
392}
393
394#[test]
395fn a_rule_name_is_unique_and_not_empty() {
396    let json = format!(
397        r#"{{ "rules": [ {a}, {a}, {b} ] }}"#,
398        a = r#"{ "name": "x", "when": [ { "known": "lines changed" } ], "then": { "note": "approve" } }"#,
399        b = r#"{ "name": " ", "when": [ { "known": "lines changed" } ], "then": { "note": "approve" } }"#,
400    );
401    assert_eq!(problems(&json, &[]), [Problem::DuplicateName, Problem::EmptyName]);
402    // A name the compiled-in rules already use is a duplicate too.
403    assert_eq!(problems(&one(r#"{ "known": "lines changed" }"#, r#"{ "note": "approve" }"#), &["r"]), [Problem::DuplicateName]);
404}
405
406#[test]
407fn every_problem_in_the_file_is_reported_not_the_first() {
408    let json = format!(
409        r#"{{ "rules": [ {bad_fact}, {bad_then} ] }}"#,
410        bad_fact = r#"{ "name": "a", "when": [ { "fact": "nope", "is": "yes" } ], "then": { "note": "approve" } }"#,
411        bad_then = r#"{ "name": "b", "when": [ { "known": "lines changed" } ], "then": { "do": "deploy" } }"#,
412    );
413    let errors = load::<Change>(&json, &[]).err().unwrap();
414    let rules: Vec<usize> = errors.iter().map(|error| match error { LoadError::Rule { rule, .. } => *rule, _ => 99 }).collect();
415    assert_eq!(rules, [0, 1]);
416    assert!(errors[0].to_string().contains("there is no fact \"nope\""), "{}", errors[0]);
417}
418
419#[test]
420fn text_that_is_not_the_format_fails_with_where() {
421    for json in ["", "[]", r#"{ "rules": 3 }"#, r#"{ "rules": [], "extra": 1 }"#, r#"{ "rules": [ { "name": "a", "when": [], "then": {}, "prio": 1 } ] }"#] {
422        let errors = load::<Change>(json, &[]).err().unwrap();
423        assert!(matches!(&errors[..], [LoadError::Syntax(_)]), "{json}: {errors:?}");
424    }
425    let LoadError::Syntax(message) = &load::<Change>("{ \"rules\": [ { ", &[]).err().unwrap()[0] else { panic!() };
426    assert!(message.contains("line 1 column"), "{message}");
427}