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 {
81 archive_file: ReadVar<PathBuf>,
82 target: Option<ReadVar<target_lexicon::Triple>>,
83 nextest_bin: Option<ReadVar<PathBuf>>,
84 },
85}
86
87#[derive(Serialize, Deserialize)]
88pub struct Run {
89 pub friendly_name: String,
91 pub run_kind: NextestRunKind,
93 pub working_dir: ReadVar<PathBuf>,
95 pub config_file: ReadVar<PathBuf>,
97 pub tool_config_files: Vec<(String, ReadVar<PathBuf>)>,
99 pub nextest_profile: String,
102 pub nextest_filter_expr: Option<String>,
104 pub run_ignored: bool,
106 pub with_rlimit_unlimited_core_size: bool,
108 pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
110 pub pre_run_deps: Vec<ReadVar<SideEffect>>,
114 pub results: WriteVar<TestResults>,
116}
117
118flowey_request! {
119 pub enum Request {
120 DefaultNextestFailFast(bool),
123 DefaultTerminateJobOnFail(bool),
126 Run(Run),
127 }
128}
129
130enum RunKindDeps<C = VarNotClaimed> {
131 BuildAndRun {
132 params: build_params::NextestBuildParams<C>,
133 nextest_installed: ReadVar<SideEffect, C>,
134 rust_toolchain: ReadVar<Option<String>, C>,
135 cargo_flags: ReadVar<crate::cfg_cargo_common_flags::Flags, C>,
136 },
137 RunFromArchive {
138 archive_file: ReadVar<PathBuf, C>,
139 nextest_bin: ReadVar<PathBuf, C>,
140 target: ReadVar<target_lexicon::Triple, C>,
141 },
142}
143
144new_flow_node!(struct Node);
145
146impl FlowNode for Node {
147 type Request = Request;
148
149 fn imports(ctx: &mut ImportCtx<'_>) {
150 ctx.import::<crate::cfg_cargo_common_flags::Node>();
151 ctx.import::<crate::download_cargo_nextest::Node>();
152 ctx.import::<crate::install_cargo_nextest::Node>();
153 ctx.import::<crate::install_rust::Node>();
154 }
155
156 fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
157 let mut run = Vec::new();
158 let mut fail_fast = None;
159 let mut terminate_job_on_fail = None;
160
161 for req in requests {
162 match req {
163 Request::DefaultNextestFailFast(v) => {
164 same_across_all_reqs("OverrideFailFast", &mut fail_fast, v)?
165 }
166 Request::DefaultTerminateJobOnFail(v) => {
167 same_across_all_reqs("TerminateJobOnFail", &mut terminate_job_on_fail, v)?
168 }
169 Request::Run(v) => run.push(v),
170 }
171 }
172
173 let terminate_job_on_fail = terminate_job_on_fail.unwrap_or(false);
174
175 for Run {
176 friendly_name,
177 run_kind,
178 working_dir,
179 config_file,
180 tool_config_files,
181 nextest_profile,
182 extra_env,
183 with_rlimit_unlimited_core_size,
184 nextest_filter_expr,
185 run_ignored,
186 pre_run_deps,
187 results,
188 } in run
189 {
190 let run_kind_deps = match run_kind {
191 NextestRunKind::BuildAndRun(params) => {
192 let cargo_flags = ctx.reqv(crate::cfg_cargo_common_flags::Request::GetFlags);
193
194 let nextest_installed = ctx.reqv(crate::install_cargo_nextest::Request);
195
196 let rust_toolchain = ctx.reqv(crate::install_rust::Request::GetRustupToolchain);
197
198 ctx.req(crate::install_rust::Request::InstallTargetTriple(
199 params.target.clone(),
200 ));
201
202 RunKindDeps::BuildAndRun {
203 params,
204 nextest_installed,
205 rust_toolchain,
206 cargo_flags,
207 }
208 }
209 NextestRunKind::RunFromArchive {
210 archive_file,
211 target,
212 nextest_bin,
213 } => {
214 let target =
215 target.unwrap_or(ReadVar::from_static(target_lexicon::Triple::host()));
216
217 let nextest_bin = nextest_bin.unwrap_or_else(|| {
218 ctx.reqv(|v| crate::download_cargo_nextest::Request::Get(target.clone(), v))
219 });
220
221 RunKindDeps::RunFromArchive {
222 archive_file,
223 nextest_bin,
224 target,
225 }
226 }
227 };
228
229 let (all_tests_passed_read, all_tests_passed_write) = ctx.new_var();
230 let (junit_xml_read, junit_xml_write) = ctx.new_var();
231
232 ctx.emit_rust_step(format!("run '{friendly_name}' nextest tests"), |ctx| {
233 pre_run_deps.claim(ctx);
234
235 let run_kind_deps = run_kind_deps.claim(ctx);
236 let working_dir = working_dir.claim(ctx);
237 let config_file = config_file.claim(ctx);
238 let tool_config_files = tool_config_files
239 .into_iter()
240 .map(|(a, b)| (a, b.claim(ctx)))
241 .collect::<Vec<_>>();
242 let extra_env = extra_env.claim(ctx);
243 let all_tests_passed_var = all_tests_passed_write.claim(ctx);
244 let junit_xml_write = junit_xml_write.claim(ctx);
245 move |rt| {
246 let working_dir = rt.read(working_dir);
247 let config_file = rt.read(config_file);
248 let mut with_env = rt.read(extra_env).unwrap_or_default();
249
250 let target = match &run_kind_deps {
251 RunKindDeps::BuildAndRun {
252 params: build_params::NextestBuildParams { target, .. },
253 ..
254 } => target.clone(),
255 RunKindDeps::RunFromArchive { target, .. } => rt.read(target.clone()),
256 };
257
258 let windows_via_wsl2 = crate::_util::running_in_wsl(rt)
259 && matches!(
260 target.operating_system,
261 target_lexicon::OperatingSystem::Windows
262 );
263
264 let maybe_convert_path = |path: PathBuf| -> PathBuf {
265 if windows_via_wsl2 {
266 crate::_util::wslpath::linux_to_win(path)
267 } else {
268 path
269 }
270 };
271
272 let junit_path = {
275 let nextest_toml = fs_err::read_to_string(&config_file)?
276 .parse::<toml_edit::DocumentMut>()
277 .context("failed to parse nextest.toml")?;
278
279 let path = Some(&nextest_toml)
280 .and_then(|i| i.get("profile"))
281 .and_then(|i| i.get(&nextest_profile))
282 .and_then(|i| i.get("junit"))
283 .and_then(|i| i.get("path"));
284
285 if let Some(path) = path {
286 let path: PathBuf =
287 path.as_str().context("malformed nextest.toml")?.into();
288 Some(path)
289 } else {
290 None
291 }
292 };
293
294 enum NextestInvocation {
295 Standalone { nextest_bin: PathBuf },
297 WithCargo { rust_toolchain: Option<String> },
299 }
300
301 let (nextest_invocation, build_args, build_env) = match run_kind_deps {
307 RunKindDeps::BuildAndRun {
308 params:
309 build_params::NextestBuildParams {
310 packages,
311 features,
312 no_default_features,
313 unstable_panic_abort_tests,
314 target,
315 profile,
316 extra_env,
317 },
318 nextest_installed: _, rust_toolchain,
320 cargo_flags,
321 } => {
322 let (mut build_args, build_env) = cargo_nextest_build_args_and_env(
323 rt.read(cargo_flags),
324 profile,
325 target,
326 rt.read(packages),
327 features,
328 unstable_panic_abort_tests,
329 no_default_features,
330 rt.read(extra_env),
331 );
332
333 let nextest_invocation = NextestInvocation::WithCargo {
334 rust_toolchain: rt.read(rust_toolchain),
335 };
336
337 let cargo_metadata_path = std::env::current_dir()?
341 .absolute()?
342 .join("cargo_metadata.json");
343
344 let sh = xshell::Shell::new()?;
345 sh.change_dir(&working_dir);
346 let output =
347 xshell::cmd!(sh, "cargo metadata --format-version 1").output()?;
348 let cargo_metadata = String::from_utf8(output.stdout)?;
349 fs_err::write(&cargo_metadata_path, cargo_metadata)?;
350
351 build_args.push("--cargo-metadata".into());
352 build_args.push(cargo_metadata_path.display().to_string());
353
354 (nextest_invocation, build_args, build_env)
355 }
356 RunKindDeps::RunFromArchive {
357 archive_file,
358 nextest_bin,
359 target: _,
360 } => {
361 let build_args = vec![
362 "--archive-file".into(),
363 maybe_convert_path(rt.read(archive_file))
364 .display()
365 .to_string(),
366 ];
367
368 let nextest_invocation = NextestInvocation::Standalone {
369 nextest_bin: rt.read(nextest_bin),
370 };
371
372 (nextest_invocation, build_args, BTreeMap::default())
373 }
374 };
375
376 let mut args: Vec<OsString> = Vec::new();
377
378 let argv0: OsString = match nextest_invocation {
379 NextestInvocation::Standalone { nextest_bin } => nextest_bin.into(),
380 NextestInvocation::WithCargo { rust_toolchain } => {
381 if let Some(rust_toolchain) = rust_toolchain {
382 args.extend(["run".into(), rust_toolchain.into(), "cargo".into()]);
383 "rustup".into()
384 } else {
385 "cargo".into()
386 }
387 }
388 };
389
390 args.extend([
391 "nextest".into(),
392 "run".into(),
393 "--profile".into(),
394 (&nextest_profile).into(),
395 "--config-file".into(),
396 maybe_convert_path(config_file).into(),
397 "--workspace-remap".into(),
398 maybe_convert_path(working_dir.clone()).into(),
399 ]);
400
401 for (tool, config_file) in tool_config_files {
402 args.extend([
403 "--tool-config-file".into(),
404 format!(
405 "{}:{}",
406 tool,
407 maybe_convert_path(rt.read(config_file)).display()
408 )
409 .into(),
410 ]);
411 }
412
413 args.extend(build_args.into_iter().map(Into::into));
414
415 if let Some(nextest_filter_expr) = nextest_filter_expr {
416 args.push("--filter-expr".into());
417 args.push(nextest_filter_expr.into());
418 }
419
420 if run_ignored {
421 args.push("--run-ignored".into());
422 args.push("all".into());
423 }
424
425 if let Some(fail_fast) = fail_fast {
426 if fail_fast {
427 args.push("--fail-fast".into());
428 } else {
429 args.push("--no-fail-fast".into());
430 }
431 }
432
433 if !with_env.contains_key("RUST_BACKTRACE") {
435 with_env.insert("RUST_BACKTRACE".into(), "1".into());
436 }
437
438 if !matches!(rt.backend(), FlowBackend::Local) {
441 with_env.insert("CARGO_INCREMENTAL".into(), "0".into());
442 }
443
444 if crate::_util::running_in_wsl(rt) {
446 let old_wslenv = std::env::var("WSLENV");
447 let new_wslenv = with_env.keys().cloned().collect::<Vec<_>>().join(":");
448 with_env.insert(
449 "WSLENV".into(),
450 format!(
451 "{}{}",
452 old_wslenv.map(|s| s + ":").unwrap_or_default(),
453 new_wslenv
454 ),
455 );
456 }
457
458 with_env.extend(build_env);
462
463 #[cfg(unix)]
482 let old_core_rlimits = if with_rlimit_unlimited_core_size
483 && matches!(rt.platform(), FlowPlatform::Linux(_))
484 {
485 let limits = rlimit::getrlimit(rlimit::Resource::CORE)?;
486 rlimit::setrlimit(
487 rlimit::Resource::CORE,
488 rlimit::INFINITY,
489 rlimit::INFINITY,
490 )?;
491 Some(limits)
492 } else {
493 None
494 };
495
496 #[cfg(not(unix))]
497 let _ = with_rlimit_unlimited_core_size;
498
499 let arg_string = || {
500 args.iter()
501 .map(|v| format!("'{}'", v.to_string_lossy()))
502 .collect::<Vec<_>>()
503 .join(" ")
504 };
505
506 let env_string = match target.operating_system {
507 target_lexicon::OperatingSystem::Windows => with_env
508 .iter()
509 .map(|(k, v)| format!("$env:{k}='{v}'"))
510 .collect::<Vec<_>>()
511 .join("; "),
512 _ => with_env
513 .iter()
514 .map(|(k, v)| format!("{k}='{v}'"))
515 .collect::<Vec<_>>()
516 .join(" "),
517 };
518
519 log::info!(
520 "{} {} {}",
521 env_string,
522 argv0.to_string_lossy(),
523 arg_string()
524 );
525
526 let mut command = std::process::Command::new(&argv0);
535 command.args(&args).envs(with_env).current_dir(&working_dir);
536
537 let mut child = command.spawn().with_context(|| {
538 format!(
539 "failed to spawn '{} {}'",
540 argv0.to_string_lossy(),
541 arg_string()
542 )
543 })?;
544
545 let status = child.wait()?;
546
547 #[cfg(unix)]
548 if let Some((soft, hard)) = old_core_rlimits {
549 rlimit::setrlimit(rlimit::Resource::CORE, soft, hard)?;
550 }
551
552 let all_tests_passed = match (status.success(), status.code()) {
553 (true, _) => true,
554 (false, Some(100)) => false,
556 (false, _) => anyhow::bail!("failed to run nextest"),
558 };
559
560 rt.write(all_tests_passed_var, &all_tests_passed);
561
562 if !all_tests_passed {
563 log::warn!("encountered at least one test failure!");
564
565 if terminate_job_on_fail {
566 anyhow::bail!("terminating job (TerminateJobOnFail = true)")
567 } else {
568 if matches!(rt.backend(), FlowBackend::Ado) {
571 eprintln!("##vso[task.complete result=SucceededWithIssues;]")
572 } else {
573 log::warn!("encountered at least one test failure");
574 }
575 }
576 }
577
578 let junit_xml = if let Some(junit_path) = junit_path {
579 let emitted_xml = working_dir
580 .join("target")
581 .join("nextest")
582 .join(&nextest_profile)
583 .join(junit_path);
584 let final_xml = std::env::current_dir()?.join("junit.xml");
585 fs_err::rename(emitted_xml, &final_xml)?;
587 Some(final_xml.absolute()?)
588 } else {
589 None
590 };
591
592 rt.write(junit_xml_write, &junit_xml);
593
594 Ok(())
595 }
596 });
597
598 ctx.emit_minor_rust_step("write results", |ctx| {
599 let all_tests_passed = all_tests_passed_read.claim(ctx);
600 let junit_xml = junit_xml_read.claim(ctx);
601 let results = results.claim(ctx);
602
603 move |rt| {
604 let all_tests_passed = rt.read(all_tests_passed);
605 let junit_xml = rt.read(junit_xml);
606
607 rt.write(
608 results,
609 &TestResults {
610 all_tests_passed,
611 junit_xml,
612 },
613 );
614 }
615 });
616 }
617
618 Ok(())
619 }
620}
621
622pub(crate) fn cargo_nextest_build_args_and_env(
624 cargo_flags: crate::cfg_cargo_common_flags::Flags,
625 cargo_profile: CargoBuildProfile,
626 target: target_lexicon::Triple,
627 packages: build_params::TestPackages,
628 features: build_params::FeatureSet,
629 unstable_panic_abort_tests: Option<build_params::PanicAbortTests>,
630 no_default_features: bool,
631 mut extra_env: BTreeMap<String, String>,
632) -> (Vec<String>, BTreeMap<String, String>) {
633 let locked = cargo_flags.locked.then_some("--locked");
634 let verbose = cargo_flags.verbose.then_some("--verbose");
635 let cargo_profile = match &cargo_profile {
636 CargoBuildProfile::Debug => "dev",
637 CargoBuildProfile::Release => "release",
638 CargoBuildProfile::Custom(s) => s,
639 };
640 let target = target.to_string();
641
642 let packages: Vec<String> = {
643 let mut v = vec!["--tests".into(), "--bins".into()];
645
646 match packages {
647 build_params::TestPackages::Workspace { exclude } => {
648 v.push("--workspace".into());
649 for crate_name in exclude {
650 v.push("--exclude".into());
651 v.push(crate_name);
652 }
653 }
654 build_params::TestPackages::Crates { crates } => {
655 for crate_name in crates {
656 v.push("-p".into());
657 v.push(crate_name);
658 }
659 }
660 }
661
662 v
663 };
664
665 let features: Vec<String> = {
666 let mut v = Vec::new();
667
668 if no_default_features {
669 v.push("--no-default-features".into())
670 }
671
672 match features {
673 build_params::FeatureSet::All => v.push("--all-features".into()),
674 build_params::FeatureSet::Specific(features) => {
675 if !features.is_empty() {
676 v.push("--features".into());
677 v.push(features.join(","));
678 }
679 }
680 }
681
682 v
683 };
684
685 let (z_panic_abort_tests, use_rustc_bootstrap) = match unstable_panic_abort_tests {
686 Some(kind) => (
687 Some("-Zpanic-abort-tests"),
688 match kind {
689 build_params::PanicAbortTests::UsingNightly => false,
690 build_params::PanicAbortTests::UsingRustcBootstrap => true,
691 },
692 ),
693 None => (None, false),
694 };
695
696 let mut args = Vec::new();
697 args.extend(locked.map(Into::into));
698 args.extend(verbose.map(Into::into));
699 args.push("--cargo-profile".into());
700 args.push(cargo_profile.into());
701 args.extend(z_panic_abort_tests.map(Into::into));
702 args.push("--target".into());
703 args.push(target);
704 args.extend(packages);
705 args.extend(features);
706
707 let mut env = BTreeMap::new();
708 if use_rustc_bootstrap {
709 env.insert("RUSTC_BOOTSTRAP".into(), "1".into());
710 }
711 env.append(&mut extra_env);
712
713 (args, env)
714}
715
716impl build_params::NextestBuildParams {
718 pub fn claim(self, ctx: &mut StepCtx<'_>) -> build_params::NextestBuildParams<VarClaimed> {
719 let build_params::NextestBuildParams {
720 packages,
721 features,
722 no_default_features,
723 unstable_panic_abort_tests,
724 target,
725 profile,
726 extra_env,
727 } = self;
728
729 build_params::NextestBuildParams {
730 packages: packages.claim(ctx),
731 features,
732 no_default_features,
733 unstable_panic_abort_tests,
734 target,
735 profile,
736 extra_env: extra_env.claim(ctx),
737 }
738 }
739}
740
741impl RunKindDeps {
743 pub fn claim(self, ctx: &mut StepCtx<'_>) -> RunKindDeps<VarClaimed> {
744 match self {
745 RunKindDeps::BuildAndRun {
746 params,
747 nextest_installed,
748 rust_toolchain,
749 cargo_flags,
750 } => RunKindDeps::BuildAndRun {
751 params: params.claim(ctx),
752 nextest_installed: nextest_installed.claim(ctx),
753 rust_toolchain: rust_toolchain.claim(ctx),
754 cargo_flags: cargo_flags.claim(ctx),
755 },
756 RunKindDeps::RunFromArchive {
757 archive_file,
758 nextest_bin,
759 target,
760 } => RunKindDeps::RunFromArchive {
761 archive_file: archive_file.claim(ctx),
762 nextest_bin: nextest_bin.claim(ctx),
763 target: target.claim(ctx),
764 },
765 }
766 }
767}