flowey_lib_common/
run_cargo_nextest_run.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Run cargo-nextest tests.
5
6use 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    /// Path to JUnit XML output (if enabled by the nextest profile)
15    pub junit_xml: Option<PathBuf>,
16}
17
18/// Parameters related to building nextest tests
19pub 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        /// Assume the current rust toolchain is nightly
27        // FUTURE: current flowey infrastructure doesn't actually have a path for
28        // multi-toolchain drifting
29        UsingNightly,
30        /// Build with `RUSTC_BOOTSTRAP=1` set
31        UsingRustcBootstrap,
32    }
33
34    #[derive(Serialize, Deserialize)]
35    pub enum FeatureSet {
36        All,
37        Specific(Vec<String>),
38    }
39
40    /// Types of things that can be documented
41    #[derive(Serialize, Deserialize)]
42    pub enum TestPackages {
43        /// Document an entire workspace workspace (with exclusions)
44        Workspace {
45            /// Exclude certain crates
46            exclude: Vec<String>,
47        },
48        /// Document a specific set of crates.
49        Crates {
50            /// Crates to document
51            crates: Vec<String>,
52        },
53    }
54
55    #[derive(Serialize, Deserialize)]
56    pub struct NextestBuildParams<C = VarNotClaimed> {
57        /// Packages to test for
58        pub packages: ReadVar<TestPackages, C>,
59        /// Cargo features to enable when building
60        pub features: FeatureSet,
61        /// Whether to disable default features
62        pub no_default_features: bool,
63        /// Whether to build tests with unstable `-Zpanic-abort-tests` flag
64        pub unstable_panic_abort_tests: Option<PanicAbortTests>,
65        /// Build tests for the specified target
66        pub target: target_lexicon::Triple,
67        /// Build tests with the specified cargo profile
68        pub profile: CargoBuildProfile,
69        /// Additional env vars set when building the tests
70        pub extra_env: ReadVar<BTreeMap<String, String>, C>,
71    }
72}
73
74/// Nextest run mode to use
75#[derive(Serialize, Deserialize)]
76pub enum NextestRunKind {
77    /// Build and run tests in a single step.
78    BuildAndRun(build_params::NextestBuildParams),
79    /// Run tests from pre-built nextest archive file.
80    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    /// Friendly name for this test group that will be displayed in logs.
90    pub friendly_name: String,
91    /// What kind of test run this is (inline build vs. from nextest archive).
92    pub run_kind: NextestRunKind,
93    /// Working directory the test archive was created from.
94    pub working_dir: ReadVar<PathBuf>,
95    /// Path to `.config/nextest.toml`
96    pub config_file: ReadVar<PathBuf>,
97    /// Path to any tool-specific config files
98    pub tool_config_files: Vec<(String, ReadVar<PathBuf>)>,
99    /// Nextest profile to use when running the source code (as defined in the
100    /// `.config.nextest.toml`).
101    pub nextest_profile: String,
102    /// Nextest test filter expression
103    pub nextest_filter_expr: Option<String>,
104    /// Whether to run ignored tests
105    pub run_ignored: bool,
106    /// Set rlimits to allow unlimited sized coredump file (if supported)
107    pub with_rlimit_unlimited_core_size: bool,
108    /// Additional env vars set when executing the tests.
109    pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
110    /// Wait for specified side-effects to resolve before building / running any
111    /// tests. (e.g: to allow for some ambient packages / dependencies to
112    /// get installed).
113    pub pre_run_deps: Vec<ReadVar<SideEffect>>,
114    /// Results of running the tests
115    pub results: WriteVar<TestResults>,
116}
117
118flowey_request! {
119    pub enum Request {
120        /// Set the default nextest fast fail behavior. Defaults to not
121        /// fast-failing when a single test fails.
122        DefaultNextestFailFast(bool),
123        /// Set the default behavior when a test failure is encountered.
124        /// Defaults to not terminating the job when a single test fails.
125        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                    // first things first - determine if junit is supported by
273                    // the profile, and if so, where the output if going to be.
274                    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                        // when tests are already built and provided via archive
296                        Standalone { nextest_bin: PathBuf },
297                        // when tests need to be compiled first
298                        WithCargo { rust_toolchain: Option<String> },
299                    }
300
301                    // the invocation of `nextest run` is quite different
302                    // depending on whether this is an archived run or not, as
303                    // archives don't require passing build args (after all -
304                    // those were passed when the archive was built), nor do
305                    // they require having cargo installed.
306                    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: _, // side-effect
319                            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                            // nextest also requires explicitly specifying the
338                            // path to a cargo-metadata.json file when running
339                            // using --workspace-remap (which do we below).
340                            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                    // useful default to have
434                    if !with_env.contains_key("RUST_BACKTRACE") {
435                        with_env.insert("RUST_BACKTRACE".into(), "1".into());
436                    }
437
438                    // if running in CI, no need to waste time with incremental
439                    // build artifacts
440                    if !matches!(rt.backend(), FlowBackend::Local) {
441                        with_env.insert("CARGO_INCREMENTAL".into(), "0".into());
442                    }
443
444                    // also update WSLENV in cases where we're running windows tests via WSL2
445                    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                    // the build_env vars don't need to be mirrored to WSLENV,
459                    // and so they are only injected after the WSLENV code has
460                    // run.
461                    with_env.extend(build_env);
462
463                    // allow unlimited coredump sizes
464                    //
465                    // FUTURE: would be cool if `flowey` had the ability to pass
466                    // around "callbacks" as part of a, which would subsume the
467                    // need to support things like `with_env` and
468                    // `with_rlimit_unlimited_core_size`.
469                    //
470                    // This _should_ be doable using the same sort of mechanism
471                    // that regular flowey Rust-based steps get registered +
472                    // invoked. i.e: the serializable "callback" object is just
473                    // a unique identifier for a set of
474                    // (NodeHandle,callback_idx,requests), which flowey can use
475                    // to "play-through" the specified node in order to get the
476                    // caller a handle to a concrete `Box<dyn Fn...>`.
477                    //
478                    // I suspect there'll need to be some `Any` involved to get
479                    // things to line up... but honestly, this seems doable?
480                    // Will need to find time to experiment with this...
481                    #[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                    // nextest has meaningful exit codes that we want to parse.
527                    // <https://github.com/nextest-rs/nextest/blob/main/nextest-metadata/src/exit_codes.rs#L12>
528                    //
529                    // unfortunately, xshell doesn't have a mode where it can
530                    // both emit to stdout/stderr, _and_ report the specific
531                    // exit code of the process.
532                    //
533                    // So we have to use the raw process API instead.
534                    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                        // documented nextest exit code for when a test has failed
555                        (false, Some(100)) => false,
556                        // any other exit code means something has gone disastrously wrong
557                        (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                            // special string on ADO that causes step to show orange (!)
569                            // FUTURE: flowey should prob have a built-in API for this
570                            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                        // copy locally to avoid trashing the output between test runs
586                        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
622// shared with `cargo_nextest_archive`
623pub(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        // exclude benches
644        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
716// FUTURE: this seems like something a proc-macro can help with...
717impl 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
741// FUTURE: this seems like something a proc-macro can help with...
742impl 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}