Built alone from an empty CARGO_HOME, its tests passed, and flowrsc checked, ran and exported a document from the conformance corpus; the export built alone. Co-Authored-By: Claude Opus 5.5 <[email protected]>
162 lines
6 KiB
Rust
162 lines
6 KiB
Rust
//! `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}");
|
|
}
|
|
}
|