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