gam_test_support/
cli_harness.rs1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4#[macro_export]
21macro_rules! gam_binary {
22 () => {
23 $crate::cli_harness::resolve_gam_binary(option_env!("CARGO_BIN_EXE_gam"))
24 };
25}
26
27pub fn resolve_gam_binary(compiled_in: Option<&str>) -> PathBuf {
31 if let Some(path) = compiled_in {
32 return PathBuf::from(path);
33 }
34
35 if let Ok(exe) = std::env::current_exe() {
39 if let Some(profile_dir) = exe.parent().and_then(Path::parent) {
40 let candidate = profile_dir.join("gam");
41 if candidate.is_file() {
42 return candidate;
43 }
44 }
45 }
46
47 let target = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target");
50 for profile in ["release-dev", "release", "debug"] {
51 let candidate = target.join(profile).join("gam");
52 if candidate.is_file() {
53 return candidate;
54 }
55 }
56
57 target.join("release-dev").join("gam")
59}
60
61pub fn run_or_panic(mut command: Command, label: &str) {
62 let output = command
63 .output()
64 .unwrap_or_else(|err| panic!("failed to spawn `{label}`: {err}"));
67 assert!(
68 output.status.success(),
69 "`{label}` failed with status {}\n--- stdout ---\n{}\n--- stderr ---\n{}",
70 output.status,
71 String::from_utf8_lossy(&output.stdout),
72 String::from_utf8_lossy(&output.stderr),
73 );
74}
75
76pub fn run_capture_or_panic(mut command: Command, label: &str) -> String {
77 let output = command
78 .output()
79 .unwrap_or_else(|err| panic!("failed to spawn `{label}`: {err}"));
82 if !output.status.success() {
83 panic!(
86 "`{label}` failed with status {}\n--- stdout ---\n{}\n--- stderr ---\n{}",
87 output.status,
88 String::from_utf8_lossy(&output.stdout),
89 String::from_utf8_lossy(&output.stderr)
90 );
91 }
92 let mut combined = String::from_utf8_lossy(&output.stdout).into_owned();
93 combined.push_str(&String::from_utf8_lossy(&output.stderr));
94 combined
95}
96
97pub fn write_predict_csv_rows<const N: usize, I>(path: &Path, header: [&str; N], rows: I)
98where
99 I: IntoIterator<Item = [String; N]>,
100{
101 let mut writer = csv::Writer::from_path(path).expect("create predict csv");
102 writer.write_record(header).expect("write header");
103 for row in rows {
104 writer
105 .write_record(row.iter().map(String::as_str))
106 .expect("write predict row");
107 }
108 writer.flush().expect("flush predict csv");
109}
110
111pub fn read_prediction_means(path: &Path) -> Vec<f64> {
112 let mut reader = csv::Reader::from_path(path).expect("open predictions csv");
113 let headers = reader.headers().expect("predict csv headers").clone();
114 let mean_idx = headers
115 .iter()
116 .position(|h| h == "mean")
117 .or_else(|| headers.iter().position(|h| h == "linear_predictor"))
118 .unwrap_or_else(|| {
119 panic!("predict csv has neither `mean` nor `linear_predictor` column: {headers:?}")
121 });
122 reader
123 .records()
124 .map(|rec| {
125 let rec = rec.expect("predict csv row");
126 rec[mean_idx]
127 .parse::<f64>()
128 .unwrap_or_else(|_| panic!("non-numeric prediction: {:?}", &rec[mean_idx]))
130 })
131 .collect()
132}
133
134pub fn fit_then_predict_gaussian(
135 train_path: &Path,
136 formula: &str,
137 model_path: &Path,
138 predict_path: &Path,
139 out_path: &Path,
140) -> Vec<f64> {
141 let mut fit_cmd = Command::new(crate::gam_binary!());
142 fit_cmd
143 .arg("fit")
144 .arg(train_path)
145 .arg(formula)
146 .args(["--family", "gaussian"])
147 .arg("--out")
148 .arg(model_path);
149 run_or_panic(fit_cmd, &format!("gam fit {formula}"));
150 assert!(model_path.is_file(), "gam fit did not write {model_path:?}");
151
152 let mut predict_cmd = Command::new(crate::gam_binary!());
153 predict_cmd
154 .arg("predict")
155 .arg(model_path)
156 .arg(predict_path)
157 .arg("--out")
158 .arg(out_path);
159 run_or_panic(predict_cmd, "gam predict");
160
161 read_prediction_means(out_path)
162}