1use crate::gen_cargo_nextest_run_cmd::RunKindDeps;
7use flowey::node::prelude::*;
8use std::collections::BTreeMap;
9
10#[derive(Serialize, Deserialize)]
11pub struct TestResults {
12 pub all_tests_passed: bool,
13 pub junit_xml: Option<PathBuf>,
15}
16
17pub mod build_params {
19 use crate::run_cargo_build::CargoBuildProfile;
20 use crate::run_cargo_build::CargoFeatureSet;
21 use flowey::node::prelude::*;
22 use std::collections::BTreeMap;
23
24 #[derive(Serialize, Deserialize)]
26 pub enum TestPackages {
27 Workspace {
29 exclude: Vec<String>,
31 },
32 Crates {
34 crates: Vec<String>,
36 },
37 }
38
39 #[derive(Serialize, Deserialize)]
40 pub struct NextestBuildParams<C = VarNotClaimed> {
41 pub packages: ReadVar<TestPackages, C>,
43 pub features: CargoFeatureSet,
45 pub no_default_features: bool,
47 pub target: target_lexicon::Triple,
49 pub profile: CargoBuildProfile,
51 pub extra_env: ReadVar<BTreeMap<String, String>, C>,
53 }
54}
55
56#[derive(Serialize, Deserialize)]
58pub enum NextestRunKind {
59 BuildAndRun(build_params::NextestBuildParams),
61 RunFromArchive {
63 archive_file: ReadVar<PathBuf>,
64 target: Option<ReadVar<target_lexicon::Triple>>,
65 nextest_bin: Option<ReadVar<PathBuf>>,
66 },
67}
68
69#[derive(Serialize, Deserialize)]
70pub struct Run {
71 pub friendly_name: String,
73 pub run_kind: NextestRunKind,
75 pub working_dir: ReadVar<PathBuf>,
77 pub config_file: ReadVar<PathBuf>,
79 pub tool_config_files: Vec<(String, ReadVar<PathBuf>)>,
81 pub nextest_profile: String,
84 pub nextest_filter_expr: Option<String>,
86 pub run_ignored: bool,
88 pub with_rlimit_unlimited_core_size: bool,
90 pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
92 pub pre_run_deps: Vec<ReadVar<SideEffect>>,
96 pub results: WriteVar<TestResults>,
98}
99
100flowey_request! {
101 pub enum Request {
102 DefaultNextestFailFast(bool),
105 DefaultTerminateJobOnFail(bool),
108 Run(Run),
109 }
110}
111
112new_flow_node!(struct Node);
113
114impl FlowNode for Node {
115 type Request = Request;
116
117 fn imports(ctx: &mut ImportCtx<'_>) {
118 ctx.import::<crate::cfg_cargo_common_flags::Node>();
119 ctx.import::<crate::download_cargo_nextest::Node>();
120 ctx.import::<crate::install_cargo_nextest::Node>();
121 ctx.import::<crate::install_rust::Node>();
122 ctx.import::<crate::gen_cargo_nextest_run_cmd::Node>();
123 }
124
125 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
126 let mut run = Vec::new();
127 let mut fail_fast = None;
128 let mut terminate_job_on_fail = None;
129
130 for req in requests {
131 match req {
132 Request::DefaultNextestFailFast(v) => {
133 same_across_all_reqs("OverrideFailFast", &mut fail_fast, v)?
134 }
135 Request::DefaultTerminateJobOnFail(v) => {
136 same_across_all_reqs("TerminateJobOnFail", &mut terminate_job_on_fail, v)?
137 }
138 Request::Run(v) => run.push(v),
139 }
140 }
141
142 let terminate_job_on_fail = terminate_job_on_fail.unwrap_or(false);
143
144 for Run {
145 friendly_name,
146 run_kind,
147 working_dir,
148 config_file,
149 tool_config_files,
150 nextest_profile,
151 extra_env,
152 with_rlimit_unlimited_core_size,
153 nextest_filter_expr,
154 run_ignored,
155 pre_run_deps,
156 results,
157 } in run
158 {
159 let run_kind_deps = match run_kind {
160 NextestRunKind::BuildAndRun(params) => {
161 let cargo_flags = ctx.reqv(crate::cfg_cargo_common_flags::Request::GetFlags);
162
163 let nextest_installed = ctx.reqv(crate::install_cargo_nextest::Request);
164
165 let rust_toolchain = ctx.reqv(crate::install_rust::Request::GetRustupToolchain);
166
167 ctx.req(crate::install_rust::Request::InstallTargetTriple(
168 params.target.clone(),
169 ));
170
171 RunKindDeps::BuildAndRun {
172 params,
173 nextest_installed,
174 rust_toolchain,
175 cargo_flags,
176 }
177 }
178 NextestRunKind::RunFromArchive {
179 archive_file,
180 target,
181 nextest_bin,
182 } => {
183 let target =
184 target.unwrap_or(ReadVar::from_static(target_lexicon::Triple::host()));
185
186 let nextest_bin = nextest_bin.unwrap_or_else(|| {
187 ctx.reqv(|v| crate::download_cargo_nextest::Request::Get(target.clone(), v))
188 });
189
190 RunKindDeps::RunFromArchive {
191 archive_file,
192 nextest_bin,
193 target,
194 }
195 }
196 };
197
198 let cmd = ctx.reqv(|v| crate::gen_cargo_nextest_run_cmd::Request {
199 run_kind_deps,
200 working_dir: working_dir.clone(),
201 config_file: config_file.clone(),
202 tool_config_files,
203 nextest_profile: nextest_profile.clone(),
204 nextest_filter_expr,
205 run_ignored,
206 fail_fast,
207 extra_env,
208 extra_commands: None,
209 portable: false,
210 command: v,
211 });
212
213 let (all_tests_passed_read, all_tests_passed_write) = ctx.new_var();
214 let (junit_xml_read, junit_xml_write) = ctx.new_var();
215
216 ctx.emit_rust_step(format!("run '{friendly_name}' nextest tests"), |ctx| {
217 pre_run_deps.claim(ctx);
218
219 let working_dir = working_dir.claim(ctx);
220 let config_file = config_file.claim(ctx);
221 let all_tests_passed_var = all_tests_passed_write.claim(ctx);
222 let junit_xml_write = junit_xml_write.claim(ctx);
223 let cmd = cmd.claim(ctx);
224
225 move |rt| {
226 let working_dir = rt.read(working_dir);
227 let config_file = rt.read(config_file);
228 let cmd = rt.read(cmd);
229
230 let junit_path = {
233 let nextest_toml = fs_err::read_to_string(&config_file)?
234 .parse::<toml_edit::DocumentMut>()
235 .context("failed to parse nextest.toml")?;
236
237 let path = Some(&nextest_toml)
238 .and_then(|i| i.get("profile"))
239 .and_then(|i| i.get(&nextest_profile))
240 .and_then(|i| i.get("junit"))
241 .and_then(|i| i.get("path"));
242
243 if let Some(path) = path {
244 let path: PathBuf =
245 path.as_str().context("malformed nextest.toml")?.into();
246 Some(path)
247 } else {
248 None
249 }
250 };
251
252 #[cfg(unix)]
271 let old_core_rlimits = if with_rlimit_unlimited_core_size
272 && matches!(rt.platform(), FlowPlatform::Linux(_))
273 {
274 let limits = rlimit::getrlimit(rlimit::Resource::CORE)?;
275 rlimit::setrlimit(
276 rlimit::Resource::CORE,
277 rlimit::INFINITY,
278 rlimit::INFINITY,
279 )?;
280 Some(limits)
281 } else {
282 None
283 };
284
285 #[cfg(not(unix))]
286 let _ = with_rlimit_unlimited_core_size;
287
288 log::info!("{cmd}");
289
290 assert_eq!(cmd.commands.len(), 1);
299 let mut command = std::process::Command::new(&cmd.commands[0].0);
300 command
301 .args(&cmd.commands[0].1)
302 .envs(&cmd.env)
303 .current_dir(&working_dir);
304
305 let mut child = command.spawn().with_context(|| {
306 format!("failed to spawn '{}'", &cmd.commands[0].0.to_string_lossy())
307 })?;
308
309 let status = child.wait()?;
310
311 #[cfg(unix)]
312 if let Some((soft, hard)) = old_core_rlimits {
313 rlimit::setrlimit(rlimit::Resource::CORE, soft, hard)?;
314 }
315
316 let all_tests_passed = match (status.success(), status.code()) {
317 (true, _) => true,
318 (false, Some(100)) => false,
320 (false, _) => anyhow::bail!("failed to run nextest"),
322 };
323
324 rt.write(all_tests_passed_var, &all_tests_passed);
325
326 if !all_tests_passed {
327 log::warn!("encountered at least one test failure!");
328
329 if terminate_job_on_fail {
330 anyhow::bail!("terminating job (TerminateJobOnFail = true)")
331 } else {
332 if matches!(rt.backend(), FlowBackend::Ado) {
335 eprintln!("##vso[task.complete result=SucceededWithIssues;]")
336 } else {
337 log::warn!("encountered at least one test failure");
338 }
339 }
340 }
341
342 let junit_xml = if let Some(junit_path) = junit_path {
343 let emitted_xml = working_dir
344 .join("target")
345 .join("nextest")
346 .join(&nextest_profile)
347 .join(junit_path);
348 let final_xml = std::env::current_dir()?.join("junit.xml");
349 fs_err::rename(emitted_xml, &final_xml)?;
351 Some(final_xml.absolute()?)
352 } else {
353 None
354 };
355
356 rt.write(junit_xml_write, &junit_xml);
357
358 Ok(())
359 }
360 });
361
362 ctx.emit_minor_rust_step("write results", |ctx| {
363 let all_tests_passed = all_tests_passed_read.claim(ctx);
364 let junit_xml = junit_xml_read.claim(ctx);
365 let results = results.claim(ctx);
366
367 move |rt| {
368 let all_tests_passed = rt.read(all_tests_passed);
369 let junit_xml = rt.read(junit_xml);
370
371 rt.write(
372 results,
373 &TestResults {
374 all_tests_passed,
375 junit_xml,
376 },
377 );
378 }
379 });
380 }
381
382 Ok(())
383 }
384}
385
386impl build_params::NextestBuildParams {
388 pub fn claim(self, ctx: &mut StepCtx<'_>) -> build_params::NextestBuildParams<VarClaimed> {
389 let build_params::NextestBuildParams {
390 packages,
391 features,
392 no_default_features,
393 target,
394 profile,
395 extra_env,
396 } = self;
397
398 build_params::NextestBuildParams {
399 packages: packages.claim(ctx),
400 features,
401 no_default_features,
402 target,
403 profile,
404 extra_env: extra_env.claim(ctx),
405 }
406 }
407}