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