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}