//! `flowrsc run`: a document run with no editor, at T0 and from its exported program, the two //! side by side. The T1 runs build real exports with cargo (`--locked`), each program alone, so //! they run on the build host with the rest of the suite. use flowrs_cli::{Cli, Command, EXIT_INVALID, EXIT_OK, EXIT_USAGE, run}; use std::path::{Path, PathBuf}; use tempfile::TempDir; fn root() -> PathBuf { Path::new(env!("CARGO_MANIFEST_DIR")) .parent() .and_then(|p| p.parent()) .unwrap() .to_path_buf() } struct Ran { code: i32, out: String, err: String, } impl Ran { fn json(&self) -> serde_json::Value { serde_json::from_str(&self.out) .unwrap_or_else(|e| panic!("{e}: {}\n{}", self.out, self.err)) } } fn flowrsc_run(graph: &Path, tier: &str, target: Option<&Path>, files: &[String]) -> Ran { let cli = Cli { command: Command::Run { graph: graph.to_path_buf(), tier: tier.into(), json: true, steps: None, files: files.to_vec(), project: None, target_dir: target.map(Path::to_path_buf), offline: false, tolerance: 0.0, timeout: 120, }, quiet: false, }; let (mut out, mut err) = (Vec::new(), Vec::new()); let code = run(&cli, &mut out, &mut err); Ran { code, out: String::from_utf8(out).unwrap(), err: String::from_utf8(err).unwrap(), } } /// The reading that carries `port`, at `tier`. fn reading(j: &serde_json::Value, port: &str, tier: &str) -> serde_json::Value { j["readings"] .as_array() .unwrap() .iter() .find(|r| r["ports"].as_array().unwrap().iter().any(|p| p == port)) .unwrap_or_else(|| panic!("no reading carries {port}: {j}"))[tier] .clone() } /// The specification's example runs with no compiler: two and three make five, at T0. #[test] fn a_document_runs_at_t0_with_no_compiler() { let doc = root().join("spec/conformance/flowrs/add-two-numbers.flowrs"); let r = flowrsc_run(&doc, "t0", None, &[]); assert_eq!(r.code, EXIT_OK, "{}", r.err); let j = r.json(); assert_eq!(j["tiers"], serde_json::json!(["t0"])); assert_eq!(reading(&j, "n2.sum", "t0").as_f64(), Some(5.0)); assert!(j["agree"].is_null(), "one tier agrees with nothing"); // The graph text of the same graph reads the same. let text = flowrsc_run( &root().join("spec/conformance/fdsl/add.fdsl"), "t0", None, &[], ); assert_eq!(reading(&text.json(), "n2.sum", "t0").as_f64(), Some(5.0)); } /// The exported program computes what the interpreter does, reading for reading, for documents /// across the corpus: arithmetic, comparisons, a clamp, a rail check, lists, text, a sine, a /// filtered spectrum (flowRS's DSP crates carried in vendor/) and a held loop over ten passes. #[test] fn t0_and_the_exported_program_agree_on_the_corpus_documents() { let target = TempDir::new().unwrap(); let mut compared = 0; let mut read = 0; for rel in [ "e2e/fixtures/apps/float_sum.flowrs", "e2e/fixtures/apps/scale_and_offset.flowrs", "e2e/fixtures/apps/compare_threshold.flowrs", "e2e/fixtures/apps/clamp_reading.flowrs", "e2e/fixtures/apps/greater_than.flowrs", "e2e/fixtures/apps/sum_to_5.flowrs", "e2e/fixtures/apps/rail_check.flowrs", "e2e/fixtures/apps/vec_push_len.flowrs", "e2e/fixtures/apps/logic_and_strings.flowrs", "e2e/fixtures/apps/sine_plot.flowrs", "e2e/fixtures/apps/dsp_filter_spectrum.flowrs", "e2e/fixtures/appexport/counter.fdsl", ] { let r = flowrsc_run(&root().join(rel), "both", Some(target.path()), &[]); assert_eq!(r.code, EXIT_OK, "{rel}: {}\n{}", r.out, r.err); let j = r.json(); assert_eq!(j["agree"], true, "{rel}: {j}"); assert_eq!(j["tiers"], serde_json::json!(["t0", "t1"])); for rd in j["readings"].as_array().unwrap() { compared += 1; if !rd["t1"].is_null() { read += 1; } } } assert!(compared >= 12, "{compared} readings compared"); assert!( read * 10 >= compared * 9, "{read} of {compared} readings were printed" ); } /// A held loop runs the passes its export runs: the counter has counted to the export's steps. #[test] fn a_held_loop_runs_the_exports_passes() { let doc = root().join("e2e/fixtures/appexport/counter.fdsl"); let r = flowrsc_run(&doc, "t0", None, &[]); assert_eq!(r.code, EXIT_OK, "{}", r.err); let j = r.json(); let steps = j["steps"].as_f64().unwrap(); assert!(steps > 1.0, "a step loop runs more than one pass"); assert_eq!(reading(&j, "n1.sum", "t0").as_f64(), Some(steps)); } /// A document whose app reads nothing has no readings, and says so rather than looking empty. #[test] fn a_document_whose_app_reads_nothing_says_so() { let r = flowrsc_run( &root().join("e2e/fixtures/apps/held_counter.flowrs"), "t0", None, &[], ); assert_eq!(r.code, EXIT_OK, "{}", r.err); assert_eq!(r.json()["readings"], serde_json::json!([])); assert!(r.err.contains("reads nothing"), "{}", r.err); } #[test] fn what_cannot_run_is_refused_with_why() { let dir = TempDir::new().unwrap(); let missing = dir.path().join("nope.flowrs"); assert_eq!(flowrsc_run(&missing, "t0", None, &[]).code, EXIT_USAGE); let doc = root().join("spec/conformance/flowrs/add-two-numbers.flowrs"); let r = flowrsc_run(&doc, "t2", None, &[]); assert_eq!(r.code, EXIT_USAGE); assert!(r.err.contains("t0, t1 or both"), "{}", r.err); let r = flowrsc_run(&doc, "t0", None, &["no-equals-sign".into()]); assert_eq!(r.code, EXIT_USAGE); assert!(r.err.contains("NAME=PATH"), "{}", r.err); // A graph whose wire comes from nowhere is invalid: judged, not run. let r = flowrsc_run( &root().join("spec/conformance/flowrs/dangling-wire.flowrs"), "t0", None, &[], ); assert_eq!(r.code, EXIT_INVALID, "{}", r.err); assert!(r.out.is_empty()); }