//! The missing-input rule of `support/require_vectors.rs`, checked in the //! environment it is about. //! //! The vector tests' CI jobs always set `WAI_REQUIRE_SOTA_VECTORS=1`, so those runs //! never exercise what the helpers do WITHOUT the switch: that a variable naming a //! test's inputs still makes them required, and that a test skips only when no //! variable names what is missing (an unset variable, or an absent or incomplete //! default location). These tests check that rule directly. Each case starts this //! test binary again as a child process running the ignored `probe` test, with the //! switch and the input variables set exactly as the case needs (and every other //! one removed, so the parent's environment cannot leak in), then checks whether //! the child passed or failed and what it printed. //! //! Their inputs are scratch directories and small files that each case creates //! under the system temporary directory, so these tests never skip. #[path = "support/require_vectors.rs"] mod require_vectors; use std::path::PathBuf; use std::process::Command; /// Which helper the probe runs: `dir` (`env_dir` + `load_all`, as the int_hyper /// tests use them) or `paths` (`env_paths`, as the JPEG-AI tests use it). const MODE: &str = "WAI_RV_PROBE"; /// The default directory the probe passes to `env_dir`. const DEFAULT: &str = "WAI_RV_PROBE_DEFAULT"; /// The directory variable the probe reads, as `int_hyper_synth` does. const DIR_VAR: &str = "WAI_SOTA_INT_DIR"; /// The two path variables the probe reads, as the JPEG-AI tests read three. const PATH_VAR: &str = "WAI_JPEGAI_CODESTREAM"; const PATH_VAR2: &str = "WAI_JPEGAI_NORM"; /// The one input file the `dir` probe needs. const INPUT: &str = "cond.json"; /// Runs one helper the way a vector test does and prints what it returned. It is /// only meaningful in a child started by [`run`], with the environment [`run`] /// gives it; run any other way, it returns without doing anything. #[test] #[ignore = "a probe that the other tests in this file run in a child process"] fn probe() { let Ok(mode) = std::env::var(MODE) else { return; }; let loaded = match mode.as_str() { "dir" => { let default = std::env::var(DEFAULT).expect("the default directory"); let d = require_vectors::env_dir("probe", DIR_VAR, &default, "the probe"); require_vectors::load_all("probe", &d, [INPUT], "the probe").is_some() } "paths" => require_vectors::env_paths("probe", [PATH_VAR, PATH_VAR2], "the probe").is_some(), other => panic!("unknown probe mode {other}"), }; println!( "probe: {}", if loaded { "inputs loaded" } else { "returned without inputs" } ); } /// What a probe run did. struct Outcome { passed: bool, /// The child's stdout followed by its stderr. out: String, } impl Outcome { /// The probe passed because it found nothing to check: the loud skip. fn assert_skipped_loudly(&self) { assert!(self.passed, "expected a skip, got a failure:\n{}", self.out); assert!( self.out.contains("probe: returned without inputs"), "{}", self.out ); // The banner CI's backstop looks for, so a skip is never read as a pass. assert!( self.out.contains("SKIPPED") && self.out.contains("NOT VERIFIED"), "{}", self.out ); } fn assert_failed_with(&self, why: &str) { assert!( !self.passed, "expected a failure naming {why:?}, got a pass:\n{}", self.out ); assert!( self.out.contains(why), "the failure does not say {why:?}:\n{}", self.out ); } } /// Runs the probe in a child process in `mode`, with `env` set and the switch and /// every input variable otherwise removed. fn run(mode: &str, env: &[(&str, &str)]) -> Outcome { let exe = std::env::current_exe().expect("this test binary"); let mut cmd = Command::new(exe); cmd.args([ "probe", "--exact", "--ignored", "--nocapture", "--test-threads=1", ]); for v in [require_vectors::SWITCH, MODE, DEFAULT, DIR_VAR, PATH_VAR, PATH_VAR2] { cmd.env_remove(v); } cmd.env(MODE, mode); for (k, v) in env { cmd.env(k, v); } let o = cmd.output().expect("start the probe"); let out = format!( "{}{}", String::from_utf8_lossy(&o.stdout), String::from_utf8_lossy(&o.stderr) ); // A filter that matched nothing would also exit 0. assert!( out.contains("running 1 test"), "the probe did not run:\n{out}" ); Outcome { passed: o.status.success(), out, } } /// A scratch directory unique to one test case, removed when dropped. struct Scratch(PathBuf); impl Scratch { fn new(case: &str) -> Scratch { let p = std::env::temp_dir().join(format!( "wai-require-vectors-rule-{}-{case}", std::process::id() )); let _ = std::fs::remove_dir_all(&p); std::fs::create_dir_all(&p).expect("create the scratch directory"); Scratch(p) } /// A path inside the scratch directory, not created. fn at(&self, name: &str) -> String { self.0 .join(name) .to_str() .expect("a UTF-8 temp path") .to_owned() } } impl Drop for Scratch { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } // ---- without the switch ---- #[test] fn without_the_switch_a_named_directory_that_does_not_exist_fails() { let s = Scratch::new("dir-missing"); let (named, default) = (s.at("not-there"), s.at("default")); run("dir", &[(DIR_VAR, &named), (DEFAULT, &default)]).assert_failed_with("is not a directory"); } #[test] fn without_the_switch_a_named_path_that_is_a_file_fails_as_a_directory() { let s = Scratch::new("dir-is-file"); let (named, default) = (s.at("a-file"), s.at("default")); std::fs::write(&named, b"{}").unwrap(); run("dir", &[(DIR_VAR, &named), (DEFAULT, &default)]).assert_failed_with("is not a directory"); } #[test] fn without_the_switch_a_named_empty_directory_fails() { // The case this file was written for: the directory exists, so only the // missing file can fail the run. let s = Scratch::new("dir-empty"); let (named, default) = (s.at("vectors"), s.at("default")); std::fs::create_dir(&named).unwrap(); run("dir", &[(DIR_VAR, &named), (DEFAULT, &default)]) .assert_failed_with(&format!("{DIR_VAR} names this directory")); } #[test] fn without_the_switch_an_unset_or_empty_directory_variable_skips_on_an_absent_default() { let s = Scratch::new("dir-default"); let default = s.at("default"); run("dir", &[(DEFAULT, &default)]).assert_skipped_loudly(); run("dir", &[(DIR_VAR, ""), (DEFAULT, &default)]).assert_skipped_loudly(); } #[test] fn without_the_switch_an_incomplete_default_directory_skips() { let s = Scratch::new("dir-default-empty"); let default = s.at("default"); std::fs::create_dir(&default).unwrap(); run("dir", &[(DEFAULT, &default)]).assert_skipped_loudly(); } #[test] fn a_named_directory_holding_the_input_loads_it() { // The positive control: without it, a probe that always failed would make the // failure cases above pass for the wrong reason. let s = Scratch::new("dir-full"); let (named, default) = (s.at("vectors"), s.at("default")); std::fs::create_dir(&named).unwrap(); std::fs::write(PathBuf::from(&named).join(INPUT), b"{}").unwrap(); let o = run("dir", &[(DIR_VAR, &named), (DEFAULT, &default)]); assert!( o.passed && o.out.contains("probe: inputs loaded"), "{}", o.out ); assert!(!o.out.contains("NOT VERIFIED"), "{}", o.out); } #[test] fn without_the_switch_a_path_variable_naming_nothing_fails() { let s = Scratch::new("path-missing"); let other = s.at("norm.json"); std::fs::write(&other, b"{}").unwrap(); run("paths", &[(PATH_VAR, &s.at("not-there")), (PATH_VAR2, &other)]) .assert_failed_with("does not exist"); // env_paths has no default: an empty value names no existing path. run("paths", &[(PATH_VAR, ""), (PATH_VAR2, &other)]).assert_failed_with("does not exist"); } #[test] fn without_the_switch_a_path_variable_naming_nothing_fails_when_another_is_unset() { // The other variable being unset would end the test as a skip; the one that // names a missing path must fail it first. let s = Scratch::new("path-missing-partial"); run("paths", &[(PATH_VAR, &s.at("not-there"))]).assert_failed_with("does not exist"); run("paths", &[(PATH_VAR2, &s.at("not-there"))]).assert_failed_with("does not exist"); } #[test] fn without_the_switch_an_unset_path_variable_skips() { run("paths", &[]).assert_skipped_loudly(); // One set to a file that exists, the other unset: nothing named is missing. let s = Scratch::new("path-partial"); let file = s.at("codestream.bin"); std::fs::write(&file, b"x").unwrap(); run("paths", &[(PATH_VAR, &file)]).assert_skipped_loudly(); } #[test] fn path_variables_naming_files_that_exist_load_them() { // The positive control for `env_paths`, as the directory one is for `env_dir`. let s = Scratch::new("path-full"); let (a, b) = (s.at("codestream.bin"), s.at("norm.json")); std::fs::write(&a, b"x").unwrap(); std::fs::write(&b, b"{}").unwrap(); let o = run("paths", &[(PATH_VAR, &a), (PATH_VAR2, &b)]); assert!( o.passed && o.out.contains("probe: inputs loaded"), "{}", o.out ); assert!(!o.out.contains("NOT VERIFIED"), "{}", o.out); } #[test] fn switch_values_that_leave_it_off_behave_as_unset() { let s = Scratch::new("switch-off"); let default = s.at("default"); for off in ["", "0", "false", "no", "FALSE", " no "] { run( "dir", &[(require_vectors::SWITCH, off), (DEFAULT, &default)], ) .assert_skipped_loudly(); } } // ---- with the switch ---- #[test] fn with_the_switch_an_absent_default_fails() { let s = Scratch::new("switch-on"); let default = s.at("default"); for on in ["1", "true", "yes"] { run("dir", &[(require_vectors::SWITCH, on), (DEFAULT, &default)]) .assert_failed_with("a missing input is a failure, not a skip"); run("paths", &[(require_vectors::SWITCH, on)]) .assert_failed_with("a missing input is a failure, not a skip"); } }