flowey_lib_common/
gen_cargo_nextest_run_cmd.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Generate a cargo-nextest run command.
5
6use crate::run_cargo_build::CargoBuildProfile;
7use crate::run_cargo_nextest_run::build_params;
8use flowey::node::prelude::*;
9use std::collections::BTreeMap;
10use std::ffi::OsString;
11
12flowey_request! {
13    pub struct Request {
14        /// What kind of test run this is (inline build vs. from nextest archive).
15        pub run_kind_deps: RunKindDeps,
16        /// Working directory the test archive was created from.
17        pub working_dir: ReadVar<PathBuf>,
18        /// Path to `.config/nextest.toml`
19        pub config_file: ReadVar<PathBuf>,
20        /// Path to any tool-specific config files
21        pub tool_config_files: Vec<(String, ReadVar<PathBuf>)>,
22        /// Nextest profile to use when running the source code (as defined in the
23        /// `.config.nextest.toml`).
24        pub nextest_profile: String,
25        /// Nextest test filter expression
26        pub nextest_filter_expr: Option<String>,
27        /// Whether to run ignored tests
28        pub run_ignored: bool,
29        /// Override fail fast setting
30        pub fail_fast: Option<bool>,
31        /// Additional env vars set when executing the tests.
32        pub extra_env: Option<ReadVar<BTreeMap<String, String>>>,
33        /// Generate a portable command with paths relative to `test_content_dir`
34        pub portable: bool,
35        /// Command for running the tests
36        pub command: WriteVar<Command>,
37    }
38}
39
40#[derive(Serialize, Deserialize)]
41pub enum RunKindDeps<C = VarNotClaimed> {
42    BuildAndRun {
43        params: build_params::NextestBuildParams<C>,
44        nextest_installed: ReadVar<SideEffect, C>,
45        rust_toolchain: ReadVar<Option<String>, C>,
46        cargo_flags: ReadVar<crate::cfg_cargo_common_flags::Flags, C>,
47    },
48    RunFromArchive {
49        archive_file: ReadVar<PathBuf, C>,
50        nextest_bin: ReadVar<PathBuf, C>,
51        target: ReadVar<target_lexicon::Triple, C>,
52    },
53}
54
55#[derive(Serialize, Deserialize)]
56pub enum CommandShell {
57    Powershell,
58    Bash,
59}
60
61#[derive(Serialize, Deserialize)]
62pub struct Command {
63    pub env: BTreeMap<String, String>,
64    pub argv0: OsString,
65    pub args: Vec<OsString>,
66    pub shell: CommandShell,
67}
68
69new_flow_node!(struct Node);
70
71impl FlowNode for Node {
72    type Request = Request;
73
74    fn imports(ctx: &mut ImportCtx<'_>) {
75        ctx.import::<crate::cfg_cargo_common_flags::Node>();
76        ctx.import::<crate::download_cargo_nextest::Node>();
77        ctx.import::<crate::install_cargo_nextest::Node>();
78        ctx.import::<crate::install_rust::Node>();
79    }
80
81    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
82        for Request {
83            run_kind_deps,
84            working_dir,
85            config_file,
86            tool_config_files,
87            nextest_profile,
88            extra_env,
89            nextest_filter_expr,
90            run_ignored,
91            fail_fast,
92            portable,
93            command,
94        } in requests
95        {
96            ctx.emit_rust_step("generate nextest command", |ctx| {
97                let run_kind_deps = run_kind_deps.claim(ctx);
98                let working_dir = working_dir.claim(ctx);
99                let config_file = config_file.claim(ctx);
100                let tool_config_files = tool_config_files
101                    .into_iter()
102                    .map(|(a, b)| (a, b.claim(ctx)))
103                    .collect::<Vec<_>>();
104                let extra_env = extra_env.claim(ctx);
105                let command = command.claim(ctx);
106
107                move |rt| {
108                    let working_dir = rt.read(working_dir);
109                    let config_file = rt.read(config_file);
110                    let mut with_env = rt.read(extra_env).unwrap_or_default();
111
112                    let target = match &run_kind_deps {
113                        RunKindDeps::BuildAndRun {
114                            params: build_params::NextestBuildParams { target, .. },
115                            ..
116                        } => target.clone(),
117                        RunKindDeps::RunFromArchive { target, .. } => rt.read(target.clone()),
118                    };
119
120                    let windows_target = matches!(
121                        target.operating_system,
122                        target_lexicon::OperatingSystem::Windows
123                    );
124                    let windows_via_wsl2 = windows_target && crate::_util::running_in_wsl(rt);
125
126                    let working_dir_ref = working_dir.as_path();
127                    let working_dir_win = windows_via_wsl2.then(|| {
128                        crate::_util::wslpath::linux_to_win(working_dir_ref)
129                            .display()
130                            .to_string()
131                    });
132                    let maybe_convert_path = |path: PathBuf| -> anyhow::Result<PathBuf> {
133                        let path = if windows_via_wsl2 {
134                            crate::_util::wslpath::linux_to_win(path)
135                        } else {
136                            path.absolute()
137                                .with_context(|| format!("invalid path {}", path.display()))?
138                        };
139                        let path = if portable {
140                            if windows_target {
141                                let working_dir_trimmed =
142                                    working_dir_win.as_ref().unwrap().trim_end_matches('\\');
143                                let path_win = path.display().to_string();
144                                let path_trimmed = path_win.trim_end_matches('\\');
145                                PathBuf::from(format!(
146                                    "$PSScriptRoot{}",
147                                    path_trimmed
148                                        .strip_prefix(working_dir_trimmed)
149                                        .with_context(|| format!(
150                                            "{} not in {}",
151                                            path_win, working_dir_trimmed
152                                        ),)?
153                                ))
154                            } else {
155                                path.strip_prefix(working_dir_ref)
156                                    .with_context(|| {
157                                        format!(
158                                            "{} not in {}",
159                                            path.display(),
160                                            working_dir_ref.display()
161                                        )
162                                    })?
163                                    .to_path_buf()
164                            }
165                        } else {
166                            path
167                        };
168                        Ok(path)
169                    };
170
171                    enum NextestInvocation {
172                        // when tests are already built and provided via archive
173                        Standalone { nextest_bin: PathBuf },
174                        // when tests need to be compiled first
175                        WithCargo { rust_toolchain: Option<String> },
176                    }
177
178                    // the invocation of `nextest run` is quite different
179                    // depending on whether this is an archived run or not, as
180                    // archives don't require passing build args (after all -
181                    // those were passed when the archive was built), nor do
182                    // they require having cargo installed.
183                    let (nextest_invocation, build_args, build_env) = match run_kind_deps {
184                        RunKindDeps::BuildAndRun {
185                            params:
186                                build_params::NextestBuildParams {
187                                    packages,
188                                    features,
189                                    no_default_features,
190                                    unstable_panic_abort_tests,
191                                    target,
192                                    profile,
193                                    extra_env,
194                                },
195                            nextest_installed: _, // side-effect
196                            rust_toolchain,
197                            cargo_flags,
198                        } => {
199                            let (mut build_args, build_env) = cargo_nextest_build_args_and_env(
200                                rt.read(cargo_flags),
201                                profile,
202                                target,
203                                rt.read(packages),
204                                features,
205                                unstable_panic_abort_tests,
206                                no_default_features,
207                                rt.read(extra_env),
208                            );
209
210                            let nextest_invocation = NextestInvocation::WithCargo {
211                                rust_toolchain: rt.read(rust_toolchain),
212                            };
213
214                            // nextest also requires explicitly specifying the
215                            // path to a cargo-metadata.json file when running
216                            // using --workspace-remap (which do we below).
217                            let cargo_metadata_path = std::env::current_dir()?
218                                .absolute()?
219                                .join("cargo_metadata.json");
220
221                            let sh = xshell::Shell::new()?;
222                            sh.change_dir(&working_dir);
223                            let output =
224                                xshell::cmd!(sh, "cargo metadata --format-version 1").output()?;
225                            let cargo_metadata = String::from_utf8(output.stdout)?;
226                            fs_err::write(&cargo_metadata_path, cargo_metadata)?;
227
228                            build_args.push("--cargo-metadata".into());
229                            build_args.push(cargo_metadata_path.display().to_string());
230
231                            (nextest_invocation, build_args, build_env)
232                        }
233                        RunKindDeps::RunFromArchive {
234                            archive_file,
235                            nextest_bin,
236                            target: _,
237                        } => {
238                            let build_args = vec![
239                                "--archive-file".into(),
240                                maybe_convert_path(rt.read(archive_file))?
241                                    .display()
242                                    .to_string(),
243                            ];
244
245                            let nextest_invocation = NextestInvocation::Standalone {
246                                nextest_bin: rt.read(nextest_bin),
247                            };
248
249                            (nextest_invocation, build_args, BTreeMap::default())
250                        }
251                    };
252
253                    let mut args: Vec<OsString> = Vec::new();
254
255                    let argv0: OsString = match nextest_invocation {
256                        NextestInvocation::Standalone { nextest_bin } => if portable {
257                            maybe_convert_path(nextest_bin)?
258                        } else {
259                            nextest_bin
260                        }
261                        .into(),
262                        NextestInvocation::WithCargo { rust_toolchain } => {
263                            if let Some(rust_toolchain) = rust_toolchain {
264                                args.extend(["run".into(), rust_toolchain.into(), "cargo".into()]);
265                                "rustup".into()
266                            } else {
267                                "cargo".into()
268                            }
269                        }
270                    };
271
272                    args.extend([
273                        "nextest".into(),
274                        "run".into(),
275                        "--profile".into(),
276                        (&nextest_profile).into(),
277                        "--config-file".into(),
278                        maybe_convert_path(config_file)?.into(),
279                        "--workspace-remap".into(),
280                        maybe_convert_path(working_dir.clone())?.into(),
281                    ]);
282
283                    for (tool, config_file) in tool_config_files {
284                        args.extend([
285                            "--tool-config-file".into(),
286                            format!(
287                                "{}:{}",
288                                tool,
289                                maybe_convert_path(rt.read(config_file))?.display()
290                            )
291                            .into(),
292                        ]);
293                    }
294
295                    args.extend(build_args.into_iter().map(Into::into));
296
297                    if let Some(nextest_filter_expr) = nextest_filter_expr {
298                        args.push("--filter-expr".into());
299                        args.push(nextest_filter_expr.into());
300                    }
301
302                    if run_ignored {
303                        args.push("--run-ignored".into());
304                        args.push("all".into());
305                    }
306
307                    if let Some(fail_fast) = fail_fast {
308                        if fail_fast {
309                            args.push("--fail-fast".into());
310                        } else {
311                            args.push("--no-fail-fast".into());
312                        }
313                    }
314
315                    // useful default to have
316                    if !with_env.contains_key("RUST_BACKTRACE") {
317                        with_env.insert("RUST_BACKTRACE".into(), "1".into());
318                    }
319
320                    // if running in CI, no need to waste time with incremental
321                    // build artifacts
322                    if !matches!(rt.backend(), FlowBackend::Local) {
323                        with_env.insert("CARGO_INCREMENTAL".into(), "0".into());
324                    }
325
326                    // also update WSLENV in cases where we're running windows tests via WSL2
327                    if !portable && crate::_util::running_in_wsl(rt) {
328                        let old_wslenv = std::env::var("WSLENV");
329                        let new_wslenv = with_env.keys().cloned().collect::<Vec<_>>().join(":");
330                        with_env.insert(
331                            "WSLENV".into(),
332                            format!(
333                                "{}{}",
334                                old_wslenv.map(|s| s + ":").unwrap_or_default(),
335                                new_wslenv
336                            ),
337                        );
338                    }
339
340                    // the build_env vars don't need to be mirrored to WSLENV,
341                    // and so they are only injected after the WSLENV code has
342                    // run.
343                    with_env.extend(build_env);
344
345                    rt.write(
346                        command,
347                        &Command {
348                            env: with_env,
349                            argv0,
350                            args,
351                            shell: if (portable || !windows_via_wsl2)
352                                && matches!(
353                                    target.operating_system,
354                                    target_lexicon::OperatingSystem::Windows
355                                ) {
356                                CommandShell::Powershell
357                            } else {
358                                CommandShell::Bash
359                            },
360                        },
361                    );
362
363                    Ok(())
364                }
365            });
366        }
367
368        Ok(())
369    }
370}
371
372// shared with `cargo_nextest_archive`
373pub(crate) fn cargo_nextest_build_args_and_env(
374    cargo_flags: crate::cfg_cargo_common_flags::Flags,
375    cargo_profile: CargoBuildProfile,
376    target: target_lexicon::Triple,
377    packages: build_params::TestPackages,
378    features: build_params::FeatureSet,
379    unstable_panic_abort_tests: Option<build_params::PanicAbortTests>,
380    no_default_features: bool,
381    mut extra_env: BTreeMap<String, String>,
382) -> (Vec<String>, BTreeMap<String, String>) {
383    let locked = cargo_flags.locked.then_some("--locked");
384    let verbose = cargo_flags.verbose.then_some("--verbose");
385    let cargo_profile = match &cargo_profile {
386        CargoBuildProfile::Debug => "dev",
387        CargoBuildProfile::Release => "release",
388        CargoBuildProfile::Custom(s) => s,
389    };
390    let target = target.to_string();
391
392    let packages: Vec<String> = {
393        // exclude benches
394        let mut v = vec!["--tests".into(), "--bins".into()];
395
396        match packages {
397            build_params::TestPackages::Workspace { exclude } => {
398                v.push("--workspace".into());
399                for crate_name in exclude {
400                    v.push("--exclude".into());
401                    v.push(crate_name);
402                }
403            }
404            build_params::TestPackages::Crates { crates } => {
405                for crate_name in crates {
406                    v.push("-p".into());
407                    v.push(crate_name);
408                }
409            }
410        }
411
412        v
413    };
414
415    let features: Vec<String> = {
416        let mut v = Vec::new();
417
418        if no_default_features {
419            v.push("--no-default-features".into())
420        }
421
422        match features {
423            build_params::FeatureSet::All => v.push("--all-features".into()),
424            build_params::FeatureSet::Specific(features) => {
425                if !features.is_empty() {
426                    v.push("--features".into());
427                    v.push(features.join(","));
428                }
429            }
430        }
431
432        v
433    };
434
435    let (z_panic_abort_tests, use_rustc_bootstrap) = match unstable_panic_abort_tests {
436        Some(kind) => (
437            Some("-Zpanic-abort-tests"),
438            match kind {
439                build_params::PanicAbortTests::UsingNightly => false,
440                build_params::PanicAbortTests::UsingRustcBootstrap => true,
441            },
442        ),
443        None => (None, false),
444    };
445
446    let mut args = Vec::new();
447    args.extend(locked.map(Into::into));
448    args.extend(verbose.map(Into::into));
449    args.push("--cargo-profile".into());
450    args.push(cargo_profile.into());
451    args.extend(z_panic_abort_tests.map(Into::into));
452    args.push("--target".into());
453    args.push(target);
454    args.extend(packages);
455    args.extend(features);
456
457    let mut env = BTreeMap::new();
458    if use_rustc_bootstrap {
459        env.insert("RUSTC_BOOTSTRAP".into(), "1".into());
460    }
461    env.append(&mut extra_env);
462
463    (args, env)
464}
465
466// FUTURE: this seems like something a proc-macro can help with...
467impl RunKindDeps {
468    pub fn claim(self, ctx: &mut StepCtx<'_>) -> RunKindDeps<VarClaimed> {
469        match self {
470            RunKindDeps::BuildAndRun {
471                params,
472                nextest_installed,
473                rust_toolchain,
474                cargo_flags,
475            } => RunKindDeps::BuildAndRun {
476                params: params.claim(ctx),
477                nextest_installed: nextest_installed.claim(ctx),
478                rust_toolchain: rust_toolchain.claim(ctx),
479                cargo_flags: cargo_flags.claim(ctx),
480            },
481            RunKindDeps::RunFromArchive {
482                archive_file,
483                nextest_bin,
484                target,
485            } => RunKindDeps::RunFromArchive {
486                archive_file: archive_file.claim(ctx),
487                nextest_bin: nextest_bin.claim(ctx),
488                target: target.claim(ctx),
489            },
490        }
491    }
492}
493
494impl std::fmt::Display for Command {
495    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496        let quote_char = match self.shell {
497            CommandShell::Powershell => "\"",
498            CommandShell::Bash => "'",
499        };
500        let arg_string = {
501            self.args
502                .iter()
503                .map(|v| format!("{quote_char}{}{quote_char}", v.to_string_lossy()))
504                .collect::<Vec<_>>()
505                .join(" ")
506        };
507
508        let env_string = match self.shell {
509            CommandShell::Powershell => self
510                .env
511                .iter()
512                .map(|(k, v)| format!("$env:{k}=\"{v}\";"))
513                .collect::<Vec<_>>()
514                .join(" "),
515            CommandShell::Bash => self
516                .env
517                .iter()
518                .map(|(k, v)| format!("{k}=\"{v}\""))
519                .collect::<Vec<_>>()
520                .join(" "),
521        };
522
523        let argv0_string = self.argv0.to_string_lossy();
524        let argv0_string = match self.shell {
525            CommandShell::Powershell => format!("&\"{argv0_string}\""),
526            CommandShell::Bash => format!("\"{argv0_string}\""),
527        };
528
529        write!(f, "{} {} {}", env_string, argv0_string, arg_string)
530    }
531}