1use std::ffi::OsString;
16use std::path::{Path, PathBuf};
17use std::process::{Command, ExitCode};
18
19use bynk_driver::test_filter::TestFilter;
20use bynk_driver::test_runner::TestArgs;
21
22use crate::compiler::{Compiler, SkewAsker, skew_gate};
23
24pub fn run(compiler: &Compiler, args: TestArgs, allow_skew: bool) -> ExitCode {
34 let Some(bynkc) = compiler.path.as_deref() else {
35 eprintln!(
36 "bynk test: no `bynkc` compiler found (looked at $BYNK_BYNKC, PATH, and next to `bynk`)."
37 );
38 eprintln!(" Run `bynk doctor --only test` for the exact remedy.");
39 return ExitCode::FAILURE;
40 };
41 let asker = SkewAsker {
44 command: Some("test"),
45 has_flag: true,
46 };
47 if !skew_gate(compiler, asker, allow_skew) {
48 return ExitCode::FAILURE;
49 }
50
51 if let Some(workspace) = bynk_driver::workspace_at(&args.input) {
54 let workspace = match workspace {
55 Ok(w) => w,
56 Err(e) => {
57 eprintln!(
58 "bynk test: {}: {e}",
59 bynk_driver::display_path(Path::new(""), &args.input.join("bynk.toml"))
60 );
61 return ExitCode::FAILURE;
62 }
63 };
64 if let Some(refusal) = workspace_refusal(&args) {
65 eprintln!("bynk test: {refusal}");
66 return ExitCode::FAILURE;
67 }
68 let members: Vec<PathBuf> = workspace
69 .members
70 .iter()
71 .map(|member| bynk_driver::member_dir(&args.input, member))
72 .collect();
73 let filter = TestFilter::new(&args.suite, &args.case);
77 if !filter.is_empty()
78 && let Some(available) = discover_all(bynkc, &members)
79 {
80 let unmatched = filter.unmatched_in(&available);
81 if !unmatched.is_empty() {
82 unmatched.report("bynk");
83 return ExitCode::FAILURE;
84 }
85 }
86 let mut failed = false;
87 for member in members {
88 println!("{}", bynk_driver::member_header(&member));
89 let _ = std::io::Write::flush(&mut std::io::stdout());
91 let member_args = TestArgs {
92 input: member,
93 partial_selection: !filter.is_empty(),
94 ..args.clone()
95 };
96 failed |= run_one(bynkc, member_args) != ExitCode::SUCCESS;
97 }
98 return if failed {
99 ExitCode::FAILURE
100 } else {
101 ExitCode::SUCCESS
102 };
103 }
104 run_one(bynkc, args)
105}
106
107fn workspace_refusal(args: &TestArgs) -> Option<&'static str> {
111 if args.format == bynk_driver::test_runner::TestFormat::Json {
112 Some("`--format json` reports one project — run it inside a member of this workspace")
113 } else if args.format == bynk_driver::test_runner::TestFormat::Ndjson {
114 Some("`--format ndjson` streams one project — run it inside a member of this workspace")
115 } else if args.output.is_some() {
116 Some("`--output` names one project's output — run it inside a member of this workspace")
117 } else if args.against.is_some() {
118 Some("`--against` runs one project's suites — run it inside a member of this workspace")
119 } else {
120 None
121 }
122}
123
124fn discover_all(bynkc: &Path, members: &[PathBuf]) -> Option<Vec<(String, String)>> {
129 let mut available = Vec::new();
130 for member in members {
131 let out = Command::new(bynkc)
132 .arg("test")
133 .arg(member)
134 .args(["--no-run", "--format", "json"])
135 .output()
136 .ok()?;
137 let doc: serde_json::Value = serde_json::from_slice(&out.stdout).ok()?;
138 if doc.get("error").is_some() {
139 return None;
140 }
141 for suite in doc.get("suites")?.as_array()? {
142 let name = suite.get("name")?.as_str()?;
143 for case in suite.get("cases")?.as_array()? {
144 available.push((name.to_string(), case.get("name")?.as_str()?.to_string()));
145 }
146 }
147 }
148 Some(available)
149}
150
151fn run_one(bynkc: &Path, args: TestArgs) -> ExitCode {
153 let mut argv: Vec<OsString> = vec!["test".into(), args.input.into_os_string()];
154 if let Some(output) = args.output {
155 argv.push("--output".into());
156 argv.push(output.into_os_string());
157 }
158 if args.no_run {
159 argv.push("--no-run".into());
160 }
161 argv.push("--format".into());
162 argv.push(args.format.as_bynkc_arg().into());
163 if args.inspect {
164 argv.push("--inspect".into());
165 }
166 if let Some(seed) = args.seed {
167 argv.push("--seed".into());
168 argv.push(seed.into());
169 }
170 if args.partial_selection {
171 argv.push("--partial-selection".into());
172 }
173 for suite in args.suite {
174 argv.push("--suite".into());
175 argv.push(suite.into());
176 }
177 for case in args.case {
178 argv.push("--case".into());
179 argv.push(case.into());
180 }
181 if args.coverage {
182 argv.push("--coverage".into());
183 }
184 let starts_dev = args.against.as_deref() == Some(bynk_driver::remote::DEV);
187 if let Some(target) = args.against {
188 argv.push("--against".into());
189 argv.push(target.into());
190 if let Some(settle) = args.settle {
193 argv.push("--settle".into());
194 argv.push(settle.into());
195 }
196 }
197 if args.skip_unsupported {
198 argv.push("--skip-unsupported".into());
199 }
200 if starts_dev {
201 let me = match std::env::current_exe() {
202 Ok(me) => me,
203 Err(e) => {
204 eprintln!("bynk test: could not find this `bynk` binary to start `bynk dev`: {e}");
205 return ExitCode::FAILURE;
206 }
207 };
208 return crate::shell::delegate_with_env(
209 bynkc,
210 argv,
211 &[(bynk_driver::remote::DEV_COMMAND_ENV, me.as_os_str())],
212 );
213 }
214
215 crate::shell::delegate(bynkc, argv)
216}