1use crate::run_cargo_build::CargoBuildProfile;
7use flowey::node::prelude::*;
8use std::collections::BTreeMap;
9use std::ffi::OsString;
10
11#[derive(Serialize, Deserialize)]
12pub struct TestResults {
13 pub all_tests_passed: bool,
14 pub junit_xml: Option<PathBuf>,
16}
17
18pub mod build_params {
20 use crate::run_cargo_build::CargoBuildProfile;
21 use flowey::node::prelude::*;
22 use std::collections::BTreeMap;
23
24 #[derive(Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Debug)]
25 pub enum PanicAbortTests {
26 UsingNightly,
30 UsingRustcBootstrap,
32 }
33
34 #[derive(Serialize, Deserialize)]
35 pub enum FeatureSet {
36 All,
37 Specific(Vec<String>),
38 }
39
40 #[derive(Serialize, Deserialize)]
42 pub enum TestPackages {
43 Workspace {
45 exclude: Vec<String>,
47 },
48 Crates {
50 crates: Vec<String>,
52 },
53 }
54
55 #[derive(Serialize, Deserialize)]
56 pub struct NextestBuildParams<C = VarNotClaimed> {
57 pub packages: ReadVar<TestPackages, C>,
59 pub features: FeatureSet,
61 pub no_default_features: bool,
63 pub unstable_panic_abort_tests: Option<PanicAbortTests>,
65 pub target: target_lexicon::Triple,
67 pub profile: CargoBuildProfile,
69 pub extra_env: ReadVar<BTreeMap<String, String>, C>,
71 }
72}
73
74#[derive(Serialize, Deserialize)]
76pub enum NextestRunKind {
77 BuildAndRun(build_params::NextestBuildParams),
79 RunFromArchive(ReadVar<PathBuf>),
81}
82
83#[derive(Serialize, Deserialize)]
84pub struct Run {
85 pub friendly_name: String,
87 pub run_kind: NextestRunKind,
89 pub working_dir: ReadVar<PathBuf>,
91 pub config_file: ReadVar<PathBuf>,
93 pub tool_config_files: Vec<(String, ReadVar<PathBuf>)>,
95 pub nextest_profile: String,
98 pub nextest_filter_expr: Option<String>,
100 pub run_ignored: bool,
102 pub with_rlimit_unlimited_core_size: bool,
104 pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
106 pub pre_run_deps: Vec<ReadVar<SideEffect>>,
110 pub results: WriteVar<TestResults>,
112}
113
114flowey_request! {
115 pub enum Request {
116 DefaultNextestFailFast(bool),
119 DefaultTerminateJobOnFail(bool),
122 Run(Run),
123 }
124}
125
126enum RunKindDeps<C = VarNotClaimed> {
127 BuildAndRun {
128 params: build_params::NextestBuildParams<C>,
129 nextest_installed: ReadVar<SideEffect, C>,
130 rust_toolchain: ReadVar<Option<String>, C>,
131 cargo_flags: ReadVar<crate::cfg_cargo_common_flags::Flags, C>,
132 },
133 RunFromArchive {
134 archive_file: ReadVar<PathBuf, C>,
135 nextest_bin: ReadVar<PathBuf, C>,
136 },
137}
138
139new_flow_node!(struct Node);
140
141impl FlowNode for Node {
142 type Request = Request;
143
144 fn imports(ctx: &mut ImportCtx<'_>) {
145 ctx.import::<crate::cfg_cargo_common_flags::Node>();
146 ctx.import::<crate::download_cargo_nextest::Node>();
147 ctx.import::<crate::install_rust::Node>();
148 }
149
150 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
151 let mut run = Vec::new();
152 let mut fail_fast = None;
153 let mut terminate_job_on_fail = None;
154
155 for req in requests {
156 match req {
157 Request::DefaultNextestFailFast(v) => {
158 same_across_all_reqs("OverrideFailFast", &mut fail_fast, v)?
159 }
160 Request::DefaultTerminateJobOnFail(v) => {
161 same_across_all_reqs("TerminateJobOnFail", &mut terminate_job_on_fail, v)?
162 }
163 Request::Run(v) => run.push(v),
164 }
165 }
166
167 let terminate_job_on_fail = terminate_job_on_fail.unwrap_or(false);
168
169 for Run {
170 friendly_name,
171 run_kind,
172 working_dir,
173 config_file,
174 tool_config_files,
175 nextest_profile,
176 extra_env,
177 with_rlimit_unlimited_core_size,
178 nextest_filter_expr,
179 run_ignored,
180 pre_run_deps,
181 results,
182 } in run
183 {
184 let run_kind_deps = match run_kind {
185 NextestRunKind::BuildAndRun(params) => {
186 let cargo_flags = ctx.reqv(crate::cfg_cargo_common_flags::Request::GetFlags);
187
188 let nextest_installed =
189 ctx.reqv(crate::download_cargo_nextest::Request::InstallWithCargo);
190
191 let rust_toolchain = ctx.reqv(crate::install_rust::Request::GetRustupToolchain);
192
193 ctx.req(crate::install_rust::Request::InstallTargetTriple(
194 params.target.clone(),
195 ));
196
197 RunKindDeps::BuildAndRun {
198 params,
199 nextest_installed,
200 rust_toolchain,
201 cargo_flags,
202 }
203 }
204 NextestRunKind::RunFromArchive(archive_file) => {
205 let nextest_bin =
206 ctx.reqv(crate::download_cargo_nextest::Request::InstallStandalone);
207
208 RunKindDeps::RunFromArchive {
209 archive_file,
210 nextest_bin,
211 }
212 }
213 };
214
215 let (all_tests_passed_read, all_tests_passed_write) = ctx.new_var();
216 let (junit_xml_read, junit_xml_write) = ctx.new_var();
217
218 ctx.emit_rust_step(format!("run '{friendly_name}' nextest tests"), |ctx| {
219 pre_run_deps.claim(ctx);
220
221 let run_kind_deps = run_kind_deps.claim(ctx);
222 let working_dir = working_dir.claim(ctx);
223 let config_file = config_file.claim(ctx);
224 let tool_config_files = tool_config_files
225 .into_iter()
226 .map(|(a, b)| (a, b.claim(ctx)))
227 .collect::<Vec<_>>();
228 let extra_env = extra_env.claim(ctx);
229 let all_tests_passed_var = all_tests_passed_write.claim(ctx);
230 let junit_xml_write = junit_xml_write.claim(ctx);
231 move |rt| {
232 let working_dir = rt.read(working_dir);
233 let config_file = rt.read(config_file);
234 let mut with_env = rt.read(extra_env).unwrap_or_default();
235
236 let junit_path = {
239 let nextest_toml = fs_err::read_to_string(&config_file)?
240 .parse::<toml_edit::DocumentMut>()
241 .context("failed to parse nextest.toml")?;
242
243 let path = Some(&nextest_toml)
244 .and_then(|i| i.get("profile"))
245 .and_then(|i| i.get(&nextest_profile))
246 .and_then(|i| i.get("junit"))
247 .and_then(|i| i.get("path"));
248
249 if let Some(path) = path {
250 let path: PathBuf =
251 path.as_str().context("malformed nextest.toml")?.into();
252 Some(path)
253 } else {
254 None
255 }
256 };
257
258 enum NextestInvocation {
259 Standalone { nextest_bin: PathBuf },
261 WithCargo { rust_toolchain: Option<String> },
263 }
264
265 let (nextest_invocation, build_args, build_env) = match run_kind_deps {
271 RunKindDeps::BuildAndRun {
272 params:
273 build_params::NextestBuildParams {
274 packages,
275 features,
276 no_default_features,
277 unstable_panic_abort_tests,
278 target,
279 profile,
280 extra_env,
281 },
282 nextest_installed: _, rust_toolchain,
284 cargo_flags,
285 } => {
286 let (mut build_args, build_env) = cargo_nextest_build_args_and_env(
287 rt.read(cargo_flags),
288 profile,
289 target,
290 rt.read(packages),
291 features,
292 unstable_panic_abort_tests,
293 no_default_features,
294 rt.read(extra_env),
295 );
296
297 let nextest_invocation = NextestInvocation::WithCargo {
298 rust_toolchain: rt.read(rust_toolchain),
299 };
300
301 let cargo_metadata_path = std::env::current_dir()?
305 .absolute()?
306 .join("cargo_metadata.json");
307
308 let sh = xshell::Shell::new()?;
309 sh.change_dir(&working_dir);
310 let output =
311 xshell::cmd!(sh, "cargo metadata --format-version 1").output()?;
312 let cargo_metadata = String::from_utf8(output.stdout)?;
313 fs_err::write(&cargo_metadata_path, cargo_metadata)?;
314
315 build_args.push("--cargo-metadata".into());
316 build_args.push(cargo_metadata_path.display().to_string());
317
318 (nextest_invocation, build_args, build_env)
319 }
320 RunKindDeps::RunFromArchive {
321 archive_file,
322 nextest_bin,
323 } => {
324 let build_args = vec![
325 "--archive-file".into(),
326 rt.read(archive_file).display().to_string(),
327 ];
328
329 let nextest_invocation = NextestInvocation::Standalone {
330 nextest_bin: rt.read(nextest_bin),
331 };
332
333 (nextest_invocation, build_args, BTreeMap::default())
334 }
335 };
336
337 let mut args: Vec<OsString> = Vec::new();
338
339 let argv0: OsString = match nextest_invocation {
340 NextestInvocation::Standalone { nextest_bin } => nextest_bin.into(),
341 NextestInvocation::WithCargo { rust_toolchain } => {
342 if let Some(rust_toolchain) = rust_toolchain {
343 args.extend(["run".into(), rust_toolchain.into(), "cargo".into()]);
344 "rustup".into()
345 } else {
346 "cargo".into()
347 }
348 }
349 };
350
351 args.extend([
352 "nextest".into(),
353 "run".into(),
354 "--profile".into(),
355 (&nextest_profile).into(),
356 "--config-file".into(),
357 config_file.into(),
358 "--workspace-remap".into(),
359 (&working_dir).into(),
360 ]);
361
362 for (tool, config_file) in tool_config_files {
363 args.extend([
364 "--tool-config-file".into(),
365 format!("{}:{}", tool, rt.read(config_file).display()).into(),
366 ]);
367 }
368
369 args.extend(build_args.into_iter().map(Into::into));
370
371 if let Some(nextest_filter_expr) = nextest_filter_expr {
372 args.push("--filter-expr".into());
373 args.push(nextest_filter_expr.into());
374 }
375
376 if run_ignored {
377 args.push("--run-ignored".into());
378 args.push("all".into());
379 }
380
381 if let Some(fail_fast) = fail_fast {
382 if fail_fast {
383 args.push("--fail-fast".into());
384 } else {
385 args.push("--no-fail-fast".into());
386 }
387 }
388
389 if !with_env.contains_key("RUST_BACKTRACE") {
391 with_env.insert("RUST_BACKTRACE".into(), "1".into());
392 }
393
394 if !matches!(rt.backend(), FlowBackend::Local) {
397 with_env.insert("CARGO_INCREMENTAL".into(), "0".into());
398 }
399
400 if crate::_util::running_in_wsl(rt) {
402 let old_wslenv = std::env::var("WSLENV");
403 let new_wslenv = with_env.keys().cloned().collect::<Vec<_>>().join(":");
404 with_env.insert(
405 "WSLENV".into(),
406 format!(
407 "{}{}",
408 old_wslenv.map(|s| s + ":").unwrap_or_default(),
409 new_wslenv
410 ),
411 );
412 }
413
414 with_env.extend(build_env);
418
419 #[cfg(unix)]
438 let old_core_rlimits = if with_rlimit_unlimited_core_size
439 && matches!(rt.platform(), FlowPlatform::Linux(_))
440 {
441 let limits = rlimit::getrlimit(rlimit::Resource::CORE)?;
442 rlimit::setrlimit(
443 rlimit::Resource::CORE,
444 rlimit::INFINITY,
445 rlimit::INFINITY,
446 )?;
447 Some(limits)
448 } else {
449 None
450 };
451
452 #[cfg(not(unix))]
453 let _ = with_rlimit_unlimited_core_size;
454
455 let arg_string = || {
456 args.iter()
457 .map(|v| v.to_string_lossy().to_string())
458 .collect::<Vec<_>>()
459 .join(" ")
460 };
461
462 let env_string = with_env
463 .iter()
464 .map(|(k, v)| format!("{k}='{v}'"))
465 .collect::<Vec<_>>()
466 .join(" ");
467
468 log::info!(
477 "$ {} {} {}",
478 env_string,
479 argv0.to_string_lossy(),
480 arg_string()
481 );
482 let mut command = std::process::Command::new(&argv0);
483 command.args(&args).envs(with_env).current_dir(&working_dir);
484
485 let mut child = command.spawn().with_context(|| {
486 format!(
487 "failed to spawn '{} {}'",
488 argv0.to_string_lossy(),
489 arg_string()
490 )
491 })?;
492
493 let status = child.wait()?;
494
495 #[cfg(unix)]
496 if let Some((soft, hard)) = old_core_rlimits {
497 rlimit::setrlimit(rlimit::Resource::CORE, soft, hard)?;
498 }
499
500 let all_tests_passed = match (status.success(), status.code()) {
501 (true, _) => true,
502 (false, Some(100)) => false,
504 (false, _) => anyhow::bail!("failed to run nextest"),
506 };
507
508 rt.write(all_tests_passed_var, &all_tests_passed);
509
510 if !all_tests_passed {
511 log::warn!("encountered at least one test failure!");
512
513 if terminate_job_on_fail {
514 anyhow::bail!("terminating job (TerminateJobOnFail = true)")
515 } else {
516 if matches!(rt.backend(), FlowBackend::Ado) {
519 eprintln!("##vso[task.complete result=SucceededWithIssues;]")
520 } else {
521 log::warn!("encountered at least one test failure");
522 }
523 }
524 }
525
526 let junit_xml = if let Some(junit_path) = junit_path {
527 let emitted_xml = working_dir
528 .join("target")
529 .join("nextest")
530 .join(&nextest_profile)
531 .join(junit_path);
532 let final_xml = std::env::current_dir()?.join("junit.xml");
533 fs_err::rename(emitted_xml, &final_xml)?;
535 Some(final_xml.absolute()?)
536 } else {
537 None
538 };
539
540 rt.write(junit_xml_write, &junit_xml);
541
542 Ok(())
543 }
544 });
545
546 ctx.emit_minor_rust_step("write results", |ctx| {
547 let all_tests_passed = all_tests_passed_read.claim(ctx);
548 let junit_xml = junit_xml_read.claim(ctx);
549 let results = results.claim(ctx);
550
551 move |rt| {
552 let all_tests_passed = rt.read(all_tests_passed);
553 let junit_xml = rt.read(junit_xml);
554
555 rt.write(
556 results,
557 &TestResults {
558 all_tests_passed,
559 junit_xml,
560 },
561 );
562 }
563 });
564 }
565
566 Ok(())
567 }
568}
569
570pub(crate) fn cargo_nextest_build_args_and_env(
572 cargo_flags: crate::cfg_cargo_common_flags::Flags,
573 cargo_profile: CargoBuildProfile,
574 target: target_lexicon::Triple,
575 packages: build_params::TestPackages,
576 features: build_params::FeatureSet,
577 unstable_panic_abort_tests: Option<build_params::PanicAbortTests>,
578 no_default_features: bool,
579 mut extra_env: BTreeMap<String, String>,
580) -> (Vec<String>, BTreeMap<String, String>) {
581 let locked = cargo_flags.locked.then_some("--locked");
582 let verbose = cargo_flags.verbose.then_some("--verbose");
583 let cargo_profile = match &cargo_profile {
584 CargoBuildProfile::Debug => "dev",
585 CargoBuildProfile::Release => "release",
586 CargoBuildProfile::Custom(s) => s,
587 };
588 let target = target.to_string();
589
590 let packages: Vec<String> = {
591 let mut v = vec!["--tests".into(), "--bins".into()];
593
594 match packages {
595 build_params::TestPackages::Workspace { exclude } => {
596 v.push("--workspace".into());
597 for crate_name in exclude {
598 v.push("--exclude".into());
599 v.push(crate_name);
600 }
601 }
602 build_params::TestPackages::Crates { crates } => {
603 for crate_name in crates {
604 v.push("-p".into());
605 v.push(crate_name);
606 }
607 }
608 }
609
610 v
611 };
612
613 let features: Vec<String> = {
614 let mut v = Vec::new();
615
616 if no_default_features {
617 v.push("--no-default-features".into())
618 }
619
620 match features {
621 build_params::FeatureSet::All => v.push("--all-features".into()),
622 build_params::FeatureSet::Specific(features) => {
623 if !features.is_empty() {
624 v.push("--features".into());
625 v.push(features.join(","));
626 }
627 }
628 }
629
630 v
631 };
632
633 let (z_panic_abort_tests, use_rustc_bootstrap) = match unstable_panic_abort_tests {
634 Some(kind) => (
635 Some("-Zpanic-abort-tests"),
636 match kind {
637 build_params::PanicAbortTests::UsingNightly => false,
638 build_params::PanicAbortTests::UsingRustcBootstrap => true,
639 },
640 ),
641 None => (None, false),
642 };
643
644 let mut args = Vec::new();
645 args.extend(locked.map(Into::into));
646 args.extend(verbose.map(Into::into));
647 args.push("--cargo-profile".into());
648 args.push(cargo_profile.into());
649 args.extend(z_panic_abort_tests.map(Into::into));
650 args.push("--target".into());
651 args.push(target);
652 args.extend(packages);
653 args.extend(features);
654
655 let mut env = BTreeMap::new();
656 if use_rustc_bootstrap {
657 env.insert("RUSTC_BOOTSTRAP".into(), "1".into());
658 }
659 env.append(&mut extra_env);
660
661 (args, env)
662}
663
664impl build_params::NextestBuildParams {
666 pub fn claim(self, ctx: &mut StepCtx<'_>) -> build_params::NextestBuildParams<VarClaimed> {
667 let build_params::NextestBuildParams {
668 packages,
669 features,
670 no_default_features,
671 unstable_panic_abort_tests,
672 target,
673 profile,
674 extra_env,
675 } = self;
676
677 build_params::NextestBuildParams {
678 packages: packages.claim(ctx),
679 features,
680 no_default_features,
681 unstable_panic_abort_tests,
682 target,
683 profile,
684 extra_env: extra_env.claim(ctx),
685 }
686 }
687}
688
689impl RunKindDeps {
691 pub fn claim(self, ctx: &mut StepCtx<'_>) -> RunKindDeps<VarClaimed> {
692 match self {
693 RunKindDeps::BuildAndRun {
694 params,
695 nextest_installed,
696 rust_toolchain,
697 cargo_flags,
698 } => RunKindDeps::BuildAndRun {
699 params: params.claim(ctx),
700 nextest_installed: nextest_installed.claim(ctx),
701 rust_toolchain: rust_toolchain.claim(ctx),
702 cargo_flags: cargo_flags.claim(ctx),
703 },
704 RunKindDeps::RunFromArchive {
705 archive_file,
706 nextest_bin,
707 } => RunKindDeps::RunFromArchive {
708 archive_file: archive_file.claim(ctx),
709 nextest_bin: nextest_bin.claim(ctx),
710 },
711 }
712 }
713}