microsoft/openvmm
Publicmirrored fromhttps://github.com/microsoft/openvmmAvailable
flowey/flowey_lib_hvlite/src/build_nextest_unit_tests.rs
338lines · modecode
| 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 | |
| 10 | use crate::common::CommonArch; |
| 11 | use crate::common::CommonProfile; |
| 12 | use crate::common::CommonTriple; |
| 13 | use crate::run_cargo_nextest_run::NextestProfile; |
| 14 | use flowey::node::prelude::*; |
| 15 | use flowey_lib_common::run_cargo_build::CargoBuildProfile; |
| 16 | use flowey_lib_common::run_cargo_build::CargoFeatureSet; |
| 17 | use flowey_lib_common::run_cargo_nextest_run::TestResults; |
| 18 | use flowey_lib_common::run_cargo_nextest_run::build_params::NextestBuildParams; |
| 19 | use flowey_lib_common::run_cargo_nextest_run::build_params::TestPackages; |
| 20 | use std::collections::BTreeMap; |
| 21 | |
| 22 | /// Type-safe wrapper around a built nextest archive containing unit tests |
| 23 | #[derive(Serialize, Deserialize)] |
| 24 | pub 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)] |
| 31 | pub 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 | |
| 53 | flowey_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 | |
| 64 | new_flow_node!(struct Node); |
| 65 | |
| 66 | impl 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 since the TPM requires |
| 171 | // OpenSSL for crypto, which isn't supported in Windows CI today. |
| 172 | // |
| 173 | // Adding the "ci" feature is also used to skip certain tests that |
| 174 | // 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 | } |
| 339 | |