Skip to main content

flowey_lib_hvlite/
build_nextest_unit_tests.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Build all cargo-nextest based unit-tests in the OpenVMM workspace.
5//!
6//! In the context of OpenVMM, we consider a "unit-test" to be any test which
7//! doesn't require any special dependencies (e.g: additional binaries, disk
8//! images, etc...), and can be run simply by invoking the test bin itself.
9
10use crate::common::CommonArch;
11use crate::common::CommonProfile;
12use crate::common::CommonTriple;
13use crate::run_cargo_nextest_run::NextestProfile;
14use flowey::node::prelude::*;
15use flowey_lib_common::run_cargo_build::CargoFeatureSet;
16use flowey_lib_common::run_cargo_nextest_run::TestResults;
17use flowey_lib_common::run_cargo_nextest_run::build_params::NextestBuildParams;
18use flowey_lib_common::run_cargo_nextest_run::build_params::TestPackages;
19use std::collections::BTreeMap;
20
21/// Type-safe wrapper around a built nextest archive containing unit tests
22#[derive(Serialize, Deserialize)]
23pub struct NextestUnitTestArchive {
24    #[serde(rename = "unit_tests.tar.zst")]
25    pub archive_file: PathBuf,
26}
27
28/// Build mode to use when building the nextest unit tests
29#[derive(Serialize, Deserialize)]
30pub enum BuildNextestUnitTestMode {
31    /// Build, immediately run, and publish unit test results, side-stepping
32    /// any intermediate archiving steps.
33    ImmediatelyRun {
34        nextest_profile: NextestProfile,
35        /// Friendly label prefix used when publishing JUnit results. Each run
36        /// is published with this prefix combined with the run's friendly
37        /// name to ensure uniqueness within the pipeline.
38        junit_test_label: String,
39        /// If provided, also copy the published junit.xml files into this
40        /// directory (only honored on local backends).
41        artifact_dir: Option<ReadVar<PathBuf>>,
42        /// Per-run test results, in the same order produced internally.
43        results: WriteVar<Vec<TestResults>>,
44        /// Signaled once every run's junit.xml has been published.
45        publish_done: WriteVar<SideEffect>,
46    },
47    /// Build and archive the tests into nextest archive files, which can then
48    /// be run via [`crate::test_nextest_unit_tests_archive`].
49    Archive(WriteVar<Vec<NextestUnitTestArchive>>),
50    /// Collect attributed coverage from selected local unit tests.
51    Coverage {
52        packages: Vec<String>,
53        options: crate::collect_unit_test_coverage::Options,
54        done: WriteVar<SideEffect>,
55    },
56}
57
58flowey_request! {
59    pub struct Request {
60        /// Build and run unit tests for the specified target
61        pub target: target_lexicon::Triple,
62        /// Build and run unit tests with the specified cargo profile
63        pub profile: CommonProfile,
64        /// Build mode to use when building the nextest unit tests
65        pub build_mode: BuildNextestUnitTestMode,
66    }
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::build_xtask::Node>();
76        ctx.import::<crate::git_checkout_openvmm_repo::Node>();
77        ctx.import::<crate::init_openvmm_magicpath_openhcl_sysroot::Node>();
78        ctx.import::<crate::install_openvmm_rust_build_essential::Node>();
79        ctx.import::<crate::run_cargo_nextest_run::Node>();
80        ctx.import::<crate::init_cross_build::Node>();
81        ctx.import::<crate::collect_unit_test_coverage::Node>();
82        ctx.import::<flowey_lib_common::run_cargo_nextest_archive::Node>();
83        ctx.import::<flowey_lib_common::publish_test_results::Node>();
84    }
85
86    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
87        let xtask_target = CommonTriple::Common {
88            arch: ctx.arch().try_into()?,
89            platform: ctx.platform().try_into()?,
90        };
91        let xtask = ctx.reqv(|v| crate::build_xtask::Request {
92            target: xtask_target,
93            xtask: v,
94        });
95
96        let openvmm_repo_path = ctx.reqv(crate::git_checkout_openvmm_repo::req::GetRepoDir);
97
98        // building these packages in the OpenVMM repo requires installing some
99        // additional deps
100        let ambient_deps = vec![ctx.reqv(crate::install_openvmm_rust_build_essential::Request)];
101
102        for Request {
103            target,
104            profile,
105            build_mode,
106        } in requests
107        {
108            let test_packages = ctx.emit_rust_stepv("determine unit test exclusions", |ctx| {
109                let xtask = xtask.clone().claim(ctx);
110                let openvmm_repo_path = openvmm_repo_path.clone().claim(ctx);
111                move |rt| {
112                    let xtask = rt.read(xtask);
113                    let openvmm_repo_path = rt.read(openvmm_repo_path);
114
115                    let mut exclude = [
116                        // Skip VMM tests, they get run in a different step.
117                        "vmm_tests",
118                        // Skip CCA tests, they aren't setup to run in CI yet
119                        "cca_tests",
120                        // Skip guest_test_uefi, as it's a no_std UEFI crate
121                        "guest_test_uefi",
122                        // Exclude various proc_macro crates, since they don't compile successfully
123                        // under --test with panic=abort targets.
124                        // https://github.com/rust-lang/cargo/issues/4336 is tracking this.
125                        //
126                        // In any case though, it's not like these crates should have unit tests
127                        // anyway.
128                        "inspect_derive",
129                        "mesh_derive",
130                        "save_restore_derive",
131                        "test_with_tracing_macro",
132                        "pal_async_test",
133                        "vmm_test_macros",
134                    ]
135                    .map(|x| x.to_string())
136                    .to_vec();
137
138                    // Exclude fuzz crates, since there libfuzzer-sys doesn't play
139                    // nice with unit tests
140                    {
141                        let xtask_bin = match xtask {
142                            crate::build_xtask::XtaskOutput::LinuxBin { bin, dbg: _ } => bin,
143                            crate::build_xtask::XtaskOutput::WindowsBin { exe, pdb: _ } => exe,
144                        };
145
146                        rt.sh.change_dir(openvmm_repo_path);
147                        let output =
148                            flowey::shell_cmd!(rt, "{xtask_bin} fuzz list --crates").output()?;
149                        let output = String::from_utf8(output.stdout)?;
150
151                        let fuzz_crates = output.trim().split('\n').map(|s| s.to_owned());
152                        exclude.extend(fuzz_crates);
153                    }
154
155                    // vtpm_util unconditionally enables the TPM's OpenSSL backend.
156                    if matches!(
157                        target.operating_system,
158                        target_lexicon::OperatingSystem::Windows
159                            | target_lexicon::OperatingSystem::Darwin(_)
160                    ) {
161                        exclude.push("vtpm_util".into());
162                    }
163
164                    Ok(TestPackages::Workspace { exclude })
165                }
166            });
167
168            let mut pre_run_deps = ambient_deps.clone();
169
170            let packages = if let BuildNextestUnitTestMode::Coverage { packages, .. } = &build_mode
171            {
172                let packages = packages.clone();
173                ctx.emit_rust_stepv("select unit test coverage packages", |ctx| {
174                    let test_packages = test_packages.clone().claim(ctx);
175                    move |rt| {
176                        let TestPackages::Workspace { exclude } = rt.read(test_packages) else {
177                            unreachable!()
178                        };
179                        for package in &packages {
180                            anyhow::ensure!(
181                                !exclude.contains(package),
182                                "{package} is excluded from the unit test suite"
183                            );
184                        }
185                        Ok(TestPackages::Crates { crates: packages })
186                    }
187                })
188            } else {
189                test_packages.clone()
190            };
191
192            let sysroot_arch = CommonArch::from_architecture(target.architecture)?;
193
194            // See comment in `crate::cargo_build` for why this is necessary.
195            //
196            // copied here since this node doesn't actually route through `cargo build`.
197            if matches!(target.environment, target_lexicon::Environment::Musl) {
198                pre_run_deps.push(
199                    ctx.reqv(|v| crate::init_openvmm_magicpath_openhcl_sysroot::Request {
200                        arch: sysroot_arch,
201                        path: v,
202                    })
203                    .into_side_effect(),
204                );
205            }
206
207            // On Windows we can't run with all features since the TPM requires
208            // OpenSSL for crypto, which isn't supported in Windows CI today.
209            //
210            // Adding the "ci" feature is also used to skip certain tests that
211            // fail in CI.
212            let features = if matches!(
213                target.operating_system,
214                target_lexicon::OperatingSystem::Windows
215            ) {
216                CargoFeatureSet::Specific(vec!["ci".into()])
217            } else {
218                CargoFeatureSet::All
219            };
220
221            let injected_env = ctx.reqv(|v| crate::init_cross_build::Request {
222                target: target.clone(),
223                injected_env: v,
224            });
225
226            let base_build_params = NextestBuildParams {
227                packages,
228                features,
229                no_default_features: false,
230                target: target.clone(),
231                profile: profile.into(),
232                extra_env: injected_env,
233            };
234
235            // The first run is the main workspace run with the base features.
236            let mut runs: Vec<(String, NextestBuildParams)> =
237                vec![("base".into(), base_build_params.clone())];
238
239            // crypto has non-additive features, so it gets its own runs to
240            // ensure full coverage of different backends. Always test the
241            // native and pure-rust backends. On linux additionally test
242            // the openssl & symcrypt backends and --all-features fallback.
243            // We could test openssl on non-linux targets too, but setting up
244            // builds for them is a pain. We could test Symcrypt on non-musl
245            // linux targets too, but we don't currently have a prebuilt
246            // library for them.
247            let mut crypto_feature_sets = vec![
248                ("native", CargoFeatureSet::Specific(vec!["native".into()])),
249                ("rust", CargoFeatureSet::Specific(vec!["rust".into()])),
250            ];
251            if matches!(
252                target.operating_system,
253                target_lexicon::OperatingSystem::Linux
254            ) {
255                crypto_feature_sets
256                    .push(("openssl", CargoFeatureSet::Specific(vec!["openssl".into()])));
257                // Only test the symcrypt backend on musl targets with our prebuilt lib
258                if matches!(target.environment, target_lexicon::Environment::Musl) {
259                    crypto_feature_sets.push((
260                        "symcrypt",
261                        CargoFeatureSet::Specific(vec!["symcrypt".into()]),
262                    ));
263                }
264                crypto_feature_sets.push(("all", CargoFeatureSet::All));
265            }
266            for (name, features) in crypto_feature_sets {
267                if matches!(
268                    &build_mode,
269                    BuildNextestUnitTestMode::Coverage { packages, .. }
270                        if !packages.iter().any(|p| p == "crypto")
271                ) {
272                    continue;
273                }
274                runs.push((
275                    format!("crypto-{}", name),
276                    NextestBuildParams {
277                        packages: ReadVar::from_static(TestPackages::Crates {
278                            crates: vec!["crypto".into()],
279                        }),
280                        features,
281                        ..base_build_params.clone()
282                    },
283                ));
284            }
285
286            match build_mode {
287                BuildNextestUnitTestMode::Coverage { options, done, .. } => {
288                    ctx.req(crate::collect_unit_test_coverage::Request::Collect {
289                        xtask: xtask.clone(),
290                        runs,
291                        options,
292                        working_dir: openvmm_repo_path.clone(),
293                        pre_run_deps,
294                        done,
295                    });
296                }
297                BuildNextestUnitTestMode::ImmediatelyRun {
298                    nextest_profile,
299                    junit_test_label,
300                    artifact_dir,
301                    results,
302                    publish_done,
303                } => {
304                    let test_results: Vec<_> = runs
305                        .into_iter()
306                        .map(|(friendly_name, build_params)| {
307                            let test_label = format!("{junit_test_label}-{friendly_name}");
308                            let r = ctx.reqv(|v| crate::run_cargo_nextest_run::Request {
309                                friendly_name: test_label.clone(),
310                                run_kind:
311                                    flowey_lib_common::run_cargo_nextest_run::NextestRunKind::BuildAndRun(
312                                        build_params,
313                                    ),
314                                nextest_profile,
315                                nextest_filter_expr: None,
316                                nextest_working_dir: None,
317                                nextest_config_file: None,
318                                run_ignored: false,
319                                extra_env: None,
320                                pre_run_deps: pre_run_deps.clone(),
321                                results: v,
322                            });
323                            (test_label, r)
324                        })
325                        .collect();
326
327                    // Emit a publish_test_results request per run, so each
328                    // run's junit.xml gets uploaded with a distinct label.
329                    let publish_dones: Vec<_> = test_results
330                        .iter()
331                        .map(|(test_label, r)| {
332                            ctx.reqv(|v| flowey_lib_common::publish_test_results::Request {
333                                test_results: r.clone(),
334                                test_label: test_label.clone(),
335                                attachments: BTreeMap::new(),
336                                output_dir: artifact_dir.clone(),
337                                upload_logs_on_success: true,
338                                done: v,
339                            })
340                        })
341                        .collect();
342
343                    ctx.emit_minor_rust_step("merge unit test results", |ctx| {
344                        let test_results = test_results
345                            .into_iter()
346                            .map(|(_, r)| r.claim(ctx))
347                            .collect::<Vec<_>>();
348                        let results = results.claim(ctx);
349                        move |rt| {
350                            let flattened = test_results.into_iter().map(|t| rt.read(t)).collect();
351                            rt.write(results, &flattened);
352                        }
353                    });
354
355                    ctx.emit_side_effect_step(publish_dones, [publish_done]);
356                }
357                BuildNextestUnitTestMode::Archive(unit_tests_archive) => {
358                    let archive_files: Vec<_> = runs
359                        .into_iter()
360                        .map(|(friendly_name, build_params)| {
361                            ctx.reqv(|v| flowey_lib_common::run_cargo_nextest_archive::Request {
362                                friendly_label: friendly_name,
363                                working_dir: openvmm_repo_path.clone(),
364                                build_params,
365                                pre_run_deps: pre_run_deps.clone(),
366                                archive_file: v,
367                            })
368                        })
369                        .collect();
370
371                    ctx.emit_minor_rust_step("report built unit tests", |ctx| {
372                        let archive_files = archive_files.claim(ctx);
373                        let unit_tests = unit_tests_archive.claim(ctx);
374                        |rt| {
375                            let flattened = archive_files
376                                .into_iter()
377                                .map(|t| NextestUnitTestArchive {
378                                    archive_file: rt.read(t),
379                                })
380                                .collect::<Vec<_>>();
381                            rt.write(unit_tests, &flattened);
382                        }
383                    });
384                }
385            }
386        }
387
388        Ok(())
389    }
390}