//! `flowrsc twin test`: a spec's verdict as an exit status a CI job reads, and as JUnit a CI test //! view reads. Without the `firmexplain` feature it says what is missing. use clap::Parser; use flowrs_cli::{Cli, run}; fn flowrsc(args: &[&str]) -> (i32, String, String) { let cli = Cli::try_parse_from(std::iter::once("flowrsc").chain(args.iter().copied())) .unwrap_or_else(|e| panic!("{args:?}: {e}")); let (mut out, mut err) = (Vec::new(), Vec::new()); let code = run(&cli, &mut out, &mut err); ( code, String::from_utf8_lossy(&out).into_owned(), String::from_utf8_lossy(&err).into_owned(), ) } #[cfg(not(feature = "firmexplain"))] #[test] fn a_build_with_no_twins_says_where_to_find_one() { let (code, out, err) = flowrsc(&["twin", "test", "spec.yaml"]); assert_eq!(code, 2); assert!(out.is_empty()); assert!(err.contains("--features firmexplain"), "{err}"); } #[cfg(feature = "firmexplain")] mod with_twins { use super::*; use std::path::Path; /// One twin CPU per process, and the tests here run on threads. static CPU: std::sync::Mutex<()> = std::sync::Mutex::new(()); fn one_at_a_time() -> std::sync::MutexGuard<'static, ()> { CPU.lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } const F4: &str = concat!( env!("CARGO_MANIFEST_DIR"), "/../flowrs-renode/tests/firmware/f4_demo.bin" ); fn spec(dir: &Path, name: &str, expect: &str) -> String { let path = dir.join(name); std::fs::write( &path, format!( "schema: flowrs.oracle/1\nboard: stm32f4\nimage: {F4}\nbudget: {{cycles: 8000000}}\nexpect:\n{expect}" ), ) .unwrap(); path.display().to_string() } #[test] fn each_verdict_is_its_own_exit_status() { let _cpu = one_at_a_time(); let dir = tempfile::tempdir().unwrap(); let d = dir.path(); let cases = [ ("pass.yaml", " - serial: {contains: \"f4 up\"}\n", 0), ("fail.yaml", " - serial: {contains: \"f7 up\"}\n", 1), ("empty.yaml", " []\n", 2), ( "inconclusive.yaml", " - serial: {contains: \"f4 up\"}\n - register: {path: SDIO.POWER, equals: 0}\n", 4, ), ]; for (name, expect, want) in cases { let path = spec(d, name, expect); let (code, out, err) = flowrsc(&["twin", "test", &path]); assert_eq!(code, want, "{name}: {out}{err}"); } // A spec error says its code. let (_, _, err) = flowrsc(&["twin", "test", &spec(d, "empty2.yaml", " []\n")]); assert!(err.contains("oracle_required"), "{err}"); // A twin that runs in the browser is a run that cannot be made here. let uno = d.join("uno.yaml"); std::fs::write( &uno, format!( "schema: flowrs.oracle/1\nboard: uno\nimage: {F4}\nexpect:\n - ended: budget\n" ), ) .unwrap(); let (code, _, err) = flowrsc(&["twin", "test", &uno.display().to_string()]); assert_eq!(code, 3, "{err}"); assert!(err.contains("runs in the browser"), "{err}"); // A Wokwi scenario on a board with no twin: unsupported, nothing run. (The STM32C031: // flowRS has no STM32C0 twin. The Pico 2 was the example until work/fixups3 put it on // the RP2350 twin.) let scenario = d.join("scenario.yaml"); std::fs::write( &scenario, "name: x\nversion: 1\nsteps:\n - wait-serial: hi\n", ) .unwrap(); std::fs::write( d.join("diagram.json"), r#"{"version":1,"parts":[{"type":"board-st-nucleo-c031c6","id":"nucleo","attrs":{}}],"connections":[]}"#, ) .unwrap(); let (code, out, err) = flowrsc(&[ "twin", "test", &scenario.display().to_string(), "--image", F4, ]); assert_eq!(code, 5, "{out}{err}"); assert!(out.starts_with("UNSUPPORTED"), "{out}"); } #[test] fn its_junit_reads_back_with_each_clause_and_what_it_came_to() { let _cpu = one_at_a_time(); let dir = tempfile::tempdir().unwrap(); let path = spec( dir.path(), "mixed.yaml", " - serial: {contains: \"f4 up\"}\n - serial: {contains: \"f7 up\"}\n \ - register: {path: SDIO.POWER, equals: 0}\n", ); let junit = dir.path().join("out.xml"); let (code, out, _) = flowrsc(&[ "twin", "test", &path, "--junit", &junit.display().to_string(), "--json", ]); assert_eq!(code, 1); let v: serde_json::Value = serde_json::from_str(&out).unwrap(); assert_eq!(v["decision"], "fail"); assert_eq!(v["schema"], "flowrs.verdict/1"); let xml = std::fs::read_to_string(&junit).unwrap(); let (run, _) = flowrs_record::junit::from_xml(&xml).unwrap(); // Each clause a case, found by its id in the name (the reader keeps the phase, // `clauses`, as a prefix; a case's recorded value reads back beside it). let all: Vec<(String, flowrs_record::Outcome)> = run .measurements() .map(|m| (m.name.clone(), m.outcome)) .collect(); let of = |id: &str| { all.iter() .find(|(n, _)| n.contains(&format!("{id} "))) .map(|(n, o)| (n.clone(), *o)) .unwrap_or_else(|| panic!("no case for {id} in {all:?}")) }; use flowrs_record::Outcome::{Fail, Pass, Skip}; assert_eq!(of("C1").1, Pass, "{xml}"); assert_eq!(of("C2").1, Fail, "{xml}"); assert_eq!(of("C3").1, Skip, "{xml}"); assert!(of("C1").0.contains("serial contains"), "{all:?}"); assert!(xml.contains("inconclusive"), "the skip says why: {xml}"); } }