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