diff --git a/.config/nextest.toml b/.config/nextest.toml index 834b2fdeff..ef238deecd 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -23,6 +23,14 @@ # https://nexte.st/docs/configuration/per-test-overrides/#override-precedence # https://nexte.st/docs/configuration/?h=hierar#hierarchical-configuration +[[profile.ci.overrides]] +# use fuzzy-matching for the package() to allow out-of-tree tests to use the +# same profile +filter = 'package(~vmm_tests)' +# VMM tests contain their own watchdog timer, but keep an extra long timer +# here as a backup. +slow-timeout = { period = "10m", terminate-after = 2 } + [[profile.default.overrides]] filter = 'package(~vmm_tests) and test(very_heavy)' # Extra heavy tests have even more VPs. Internal runners fail when 32vp tests @@ -46,6 +54,13 @@ filter = 'package(~vmm_tests)' # For local dev runs, you may need to manually restrict the number of # threads running via the -j cli arg. threads-required = 2 +# Use the same slow timeout as ci, but never terminate +slow-timeout = { period = "10m" } + +[[profile.ci.overrides]] +# allow loom based tests more time, as they take a while +filter = 'test(loom)' +slow-timeout = { period = "30s", terminate-after = 2 } # Profile for AI agent runs. Minimizes output noise: only prints slow, failing, # and flaky tests. Agents should use: cargo nextest run -p --profile agent @@ -72,17 +87,4 @@ fail-fast = false [profile.ci.junit] path = "junit.xml" -store-success-output = "true" - -[[profile.ci.overrides]] -# allow loom based tests more time, as they take a while -filter = 'test(loom)' -slow-timeout = { period = "30s", terminate-after = 2 } - -[[profile.ci.overrides]] -# use fuzzy-matching for the package() to allow out-of-tree tests to use the -# same profile -filter = 'package(~vmm_tests)' -# VMM tests contain their own watchdog timer, but keep an extra long timer -# here as a backup. -slow-timeout = { period = "10m", terminate-after = 2 } +store-success-output = "true" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index cc95e2e8e3..3c7d6de1b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2131,6 +2131,9 @@ dependencies = [ "flowey_lib_common", "flowey_lib_hvlite", "log", + "petri_artifacts_common", + "petri_artifacts_core", + "petri_artifacts_vmm_test", "serde", "serde_json", "target-lexicon", @@ -6055,6 +6058,7 @@ version = "0.0.0" dependencies = [ "anyhow", "paste", + "serde", ] [[package]] diff --git a/flowey/flowey_hvlite/Cargo.toml b/flowey/flowey_hvlite/Cargo.toml index dd1a95b26c..c39b86589c 100644 --- a/flowey/flowey_hvlite/Cargo.toml +++ b/flowey/flowey_hvlite/Cargo.toml @@ -12,6 +12,9 @@ flowey_lib_common.workspace = true flowey_lib_hvlite.workspace = true flowey.workspace = true +petri_artifacts_common.workspace = true +petri_artifacts_core.workspace = true +petri_artifacts_vmm_test.workspace = true vmm_test_images = { workspace = true, features = ["serde", "clap"] } anyhow.workspace = true diff --git a/flowey/flowey_hvlite/src/pipelines/vmm_tests_run.rs b/flowey/flowey_hvlite/src/pipelines/vmm_tests_run.rs index 50a58a06fd..de4b016d05 100644 --- a/flowey/flowey_hvlite/src/pipelines/vmm_tests_run.rs +++ b/flowey/flowey_hvlite/src/pipelines/vmm_tests_run.rs @@ -12,16 +12,20 @@ use anyhow::Context as _; use flowey::node::prelude::ReadVar; use flowey::pipeline::prelude::*; +use flowey_lib_hvlite::_jobs::local_build_and_run_nextest_vmm_tests::BuildSelections; use flowey_lib_hvlite::_jobs::local_build_and_run_nextest_vmm_tests::VmmTestSelections; -use flowey_lib_hvlite::artifact_to_build_mapping::ResolvedArtifactSelections; -use flowey_lib_hvlite::common::CommonArch; use flowey_lib_hvlite::common::CommonTriple; use flowey_lib_hvlite::install_vmm_tests_deps::VmmTestsDepSelections; +use petri_artifacts_core::ArtifactId; +use petri_artifacts_core::ArtifactListOutput; +use std::collections::BTreeMap; +use std::collections::BTreeSet; use std::io::Write as _; use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::process::Stdio; +use vmm_test_images::KnownTestArtifacts; /// Build and run VMM tests with automatic artifact discovery #[derive(clap::Args)] @@ -32,9 +36,13 @@ pub struct VmmTestsRunCli { #[clap(long)] target: Option, - /// Directory for the output artifacts + /// Directory for the output artifacts. + /// + /// If not specified, defaults to `target/vmm_tests`. + /// WSL-to-Windows runs still require explicitly overriding this to a + /// Windows-accessible output directory. #[clap(long)] - dir: PathBuf, + dir: Option, /// Test filter (nextest filter expression) /// @@ -88,6 +96,48 @@ pub struct VmmTestsRunCli { /// downloaded release. Path to a locally-built MSVM.fd file. #[clap(long)] custom_uefi_firmware: Option, + + /// use the nextest CI profile rather than the default one + #[clap(long)] + ci_profile: bool, + + /// Don't reuse prepped vhds, even if they already exist. + /// Use when making changes to prep_steps + #[clap(long)] + no_reuse_prepped_vhds: bool, +} + +struct CargoNextestListRequest<'a> { + repo_root: &'a Path, + target: &'a str, + filter: &'a str, + release: bool, + include_ignored: bool, +} + +struct RustSuite { + binary_path: PathBuf, + testcases: Vec, +} + +/// Result of resolving artifact requirements to build/download selections +struct ResolvedArtifactSelections { + /// What to build + build: BuildSelections, + /// What to download + downloads: BTreeSet, + /// Whether any tests need release IGVM files from GitHub + needs_release_igvm: bool, +} + +impl Default for ResolvedArtifactSelections { + fn default() -> Self { + Self { + build: BuildSelections::none(), + downloads: BTreeSet::new(), + needs_release_igvm: false, + } + } } impl IntoPipeline for VmmTestsRunCli { @@ -111,80 +161,108 @@ impl IntoPipeline for VmmTestsRunCli { custom_kernel_modules, custom_kernel, custom_uefi_firmware, + ci_profile, + no_reuse_prepped_vhds, } = self; - // 1. Resolve target let target = resolve_target(target, backend_hint)?; let target_os = target.as_triple().operating_system; - let target_architecture = target.as_triple().architecture; + let target_architecture = target.common_arch()?; let target_str = target.as_triple().to_string(); - // 2. Validate output directory for WSL - validate_output_dir(&dir, target_os)?; - std::fs::create_dir_all(&dir).context("failed to create output directory")?; - - // 3. Run artifact discovery inline at pipeline construction time - log::info!("Step 1: Discovering required artifacts..."); let repo_root = crate::repo_root(); - let (artifacts_json, test_names, test_binary) = - discover_artifacts(&repo_root, &target_str, &filter, release, build_only) - .context("during artifact discovery")?; - - // 4. Resolve to build selections - let mut resolved = ResolvedArtifactSelections::from_artifact_list_json( - &artifacts_json, - target_architecture, - target_os, - ) - .context("failed to parse discovered artifacts")?; - - if !resolved.unknown.is_empty() { - anyhow::bail!( - "Unknown artifacts found (mapping needs to be updated):\n {}", - resolved.unknown.join("\n ") - ); + + // Validate output directory for WSL + validate_output_dir(dir.as_deref(), target_os)?; + let test_content_dir = dir.unwrap_or_else(|| repo_root.join("target").join("vmm_tests")); + std::fs::create_dir_all(&test_content_dir).context("failed to create output directory")?; + + // Run artifact discovery inline at pipeline construction time since + // flowey doesn't support conditional requests yet + log::info!( + "Discovering artifacts for filter: {} (target: {})", + filter, + target + ); + + // Determine which tests match the filter + let suites = run_cargo_nextest_list(CargoNextestListRequest { + repo_root: &repo_root, + target: &target_str, + filter: &filter, + release, + // When using build-only mode, we need to enumerate tests that could be + // run on any system so that we build all necessary dependencies. By default + // petri marks incompatible tests as ignored. + include_ignored: build_only, + })?; + + if suites.is_empty() { + anyhow::bail!("No tests found for the given filter"); } - // 5. Determine lazy fetch mode. + // Query for the required artifacts + let mut artifacts = Vec::new(); + for suite in suites.values() { + artifacts.append(&mut query_test_binary_artifacts(suite)?); + } + + // Resolve to build selections + let mut resolved = ResolvedArtifactSelections::default(); + for artifact in artifacts { + resolved.resolve_artifact(&artifact)?; + } + + // Determine lazy fetch mode. // // By default, VHD/ISO downloads are skipped and disk images are // streamed on demand via HTTP (with local SQLite caching). This // avoids multi-GB upfront downloads for dev-inner-loop scenarios. // - // Lazy fetch is disabled when: - // - The user passes --no-lazy-fetch - // - Any Hyper-V test is selected (Hyper-V requires local files) + // Lazy fetch is disabled for all downloads when the user passes + // --no-lazy-fetch and for any downloads that are used by a selected + // Hyper-V test. // // When both Hyper-V and non-Hyper-V tests are selected, only the // artifacts required by Hyper-V tests are downloaded upfront; the // rest are lazy-fetched. if no_lazy_fetch { - log::info!("Lazy fetch disabled by --no-lazy-fetch"); + log::info!("Lazy fetch disabled"); } else { - let hyperv_names: Vec<_> = test_names - .iter() - .filter(|name| name.contains("hyperv_")) - .cloned() - .collect(); + let mut hyperv_tests: usize = 0; + let mut hyperv_artifacts = Vec::new(); + for (_, suite) in suites.iter() { + let hyperv_testcases: Vec<_> = suite + .testcases + .iter() + .filter(|name| name.contains("hyperv_")) + .cloned() + .collect(); + + if !hyperv_testcases.is_empty() { + hyperv_tests += hyperv_testcases.len(); + hyperv_artifacts.append(&mut query_test_binary_artifacts(&RustSuite { + binary_path: suite.binary_path.clone(), + testcases: hyperv_testcases, + })?); + } + } - if hyperv_names.is_empty() { + resolved.downloads.retain(|a| !a.supports_blob_disk()); + + if hyperv_tests == 0 { log::info!("Lazy fetch enabled: disk images will be streamed on demand via HTTP"); - resolved.downloads.clear(); } else { log::info!( "Downloading disk images required by {} Hyper-V tests", - hyperv_names.len() + hyperv_tests ); - let hyperv_json = - query_test_binary_artifacts(&test_binary, &hyperv_names, &target_str) - .context("during Hyper-V artifact discovery")?; - let hyperv_resolved = ResolvedArtifactSelections::from_artifact_list_json( - &hyperv_json, - target_architecture, - target_os, - ) - .context("failed to parse Hyper-V artifacts")?; - resolved.downloads = hyperv_resolved.downloads; + } + + // Re-add only the downloads needed for hyper-v. Other selections should + // remain the same since resolve_artifact can only add selections + for artifact in hyperv_artifacts { + resolved.resolve_artifact(&artifact)?; } } @@ -194,44 +272,103 @@ impl IntoPipeline for VmmTestsRunCli { resolved.downloads.iter().collect::>() ); - let selections = selections_from_resolved(filter, resolved, target_os); + let openvmm_repo = flowey_lib_common::git_checkout::RepoSource::ExistingClone( + ReadVar::from_static(repo_root), + ); - // 6. Construct and return the pipeline - log::info!("Step 2: Building and running tests..."); - build_vmm_tests_pipeline( - backend_hint, - target, - selections, - dir, - VmmTestsPipelineOptions { - verbose, - install_missing_deps, - unstable_whp, - release, - build_only, - copy_extras, - skip_vhd_prompt, - custom_kernel_modules, - custom_kernel, - custom_uefi_firmware, - }, - ) + let mut pipeline = Pipeline::new(); + + let mut job = pipeline.new_job( + FlowPlatform::host(backend_hint), + FlowArch::host(backend_hint), + "build all dependencies and run vmm tests", + ); + + job = job.dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init); + + // Override kernel with local paths if both kernel and modules are specified + if let (Some(kernel_path), Some(modules_path)) = + (custom_kernel.clone(), custom_kernel_modules.clone()) + { + job = + job.dep_on( + move |_| flowey_lib_hvlite::_jobs::cfg_versions::Request::LocalKernel { + arch: target_architecture, + kernel: ReadVar::from_static(kernel_path), + modules: ReadVar::from_static(modules_path), + }, + ); + } + + // Override UEFI firmware with a local MSVM.fd path + if let Some(fw_path) = custom_uefi_firmware { + job = job.dep_on(move |_| { + flowey_lib_hvlite::_jobs::cfg_versions::Request::LocalUefi( + target_architecture, + ReadVar::from_static(fw_path), + ) + }); + } + + job = job + .dep_on( + |_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params { + hvlite_repo_source: openvmm_repo.clone(), + }, + ) + .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params { + local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams { + interactive: true, + auto_install: install_missing_deps, + ignore_rust_version: true, + }), + verbose: ReadVar::from_static(verbose), + locked: false, + deny_warnings: false, + no_incremental: false, + }) + .dep_on(|ctx| { + flowey_lib_hvlite::_jobs::local_build_and_run_nextest_vmm_tests::Params { + target, + test_content_dir, + selections: selections_from_resolved(filter, resolved, target_os), + unstable_whp, + release, + build_only, + copy_extras, + custom_kernel_modules, + custom_kernel, + skip_vhd_prompt, + nextest_profile: if ci_profile { + flowey_lib_hvlite::run_cargo_nextest_run::NextestProfile::Ci + } else { + flowey_lib_hvlite::run_cargo_nextest_run::NextestProfile::Default + }, + reuse_prepped_vhds: !no_reuse_prepped_vhds, + done: ctx.new_done_handle(), + } + }); + + job.finish(); + + Ok(pipeline) } } -/// Run artifact discovery by invoking `cargo nextest list` and the test -/// binary's `--list-required-artifacts` flag. -/// -/// Returns the raw JSON string describing required/optional artifacts, the -/// list of matching test names (used for backend detection), and the path -/// to the test binary (for follow-up queries). -fn discover_artifacts( - repo_root: &Path, - target: &str, - filter: &str, - release: bool, - ignore_host_requirements: bool, -) -> anyhow::Result<(String, Vec, PathBuf)> { +/// Get test binaries and associated matching tests for a given nextest filter. +// TODO: this function should really be a flowey node without automatic +// dependency installation, but that would require conditional requests. +fn run_cargo_nextest_list<'a>( + req: CargoNextestListRequest<'a>, +) -> anyhow::Result> { + let CargoNextestListRequest { + repo_root, + target, + filter, + release, + include_ignored, + } = req; + // Check that cargo-nextest is available let nextest_check = Command::new("cargo") .args(["nextest", "--version"]) @@ -240,15 +377,11 @@ fn discover_artifacts( .status(); match nextest_check { Ok(status) if status.success() => {} - _ => anyhow::bail!("cargo-nextest not found. Run 'cargo xflowey restore-packages' first."), + _ => anyhow::bail!( + "cargo-nextest not found. Run 'cargo install --locked cargo-nextest' first." + ), } - log::info!( - "Discovering artifacts for filter: {} (target: {})", - filter, - target - ); - // Step 1: Use nextest to resolve the filter expression to test names and // get the binary path let mut cmd = Command::new("cargo"); @@ -268,59 +401,78 @@ fn discover_artifacts( if release { cmd.arg("--release"); } - // When using build-only mode, we need to enumerate tests that could be - // run on any system so that we build all necessary dependencies. By default - // petri marks incompatible tests as ignored. - if ignore_host_requirements { + if include_ignored { cmd.args(["--run-ignored", "all"]); } let nextest_output = cmd.output().context("failed to run cargo nextest list")?; anyhow::ensure!(nextest_output.status.success(), "cargo nextest list failed",); let nextest_stdout = String::from_utf8(nextest_output.stdout) .map_err(|e| anyhow::anyhow!("nextest output is not valid UTF-8: {}", e))?; - let (test_binary, test_names) = parse_nextest_output(&nextest_stdout)?; - - if test_names.is_empty() { - log::warn!("No tests match the filter: {}", filter); - let empty_output = serde_json::json!({ - "target": target, - "required": [], - "optional": [] - }); - return Ok(( - serde_json::to_string_pretty(&empty_output)?, - Vec::new(), - test_binary, - )); - } - log::info!("Found {} matching tests", test_names.len()); - for name in &test_names { - log::debug!(" - {}", name); + parse_nextest_output(&nextest_stdout) +} + +/// Parse `cargo nextest list --message-format json` output to extract test +/// names and binary path. +fn parse_nextest_output(stdout: &str) -> anyhow::Result> { + let json: serde_json::Value = serde_json::from_str(stdout) + .map_err(|e| anyhow::anyhow!("failed to parse nextest JSON output: {}", e))?; + + let mut suites = BTreeMap::new(); + + for (name, suite) in json + .get("rust-suites") + .and_then(|s| s.as_object()) + .context("no rust-suites object")? + { + let binary_path = PathBuf::from( + suite + .get("binary-path") + .and_then(|v| v.as_str()) + .context("no binary-path str")?, + ); + + let testcases: Vec<_> = suite + .get("testcases") + .and_then(|t| t.as_object()) + .context("no testcases object")? + .iter() + .filter(|(_, test_info)| { + test_info + .get("filter-match") + .and_then(|fm| fm.get("status")) + .and_then(|s| s.as_str()) + .is_some_and(|s| s == "matches") + }) + .map(|(test_name, _)| test_name.to_owned()) + .collect(); + + if !testcases.is_empty() { + suites.insert( + name.to_owned(), + RustSuite { + binary_path, + testcases, + }, + ); + } } - // Step 2: Query petri for artifacts of each matching test - let json = query_test_binary_artifacts(&test_binary, &test_names, target)?; - Ok((json, test_names, test_binary)) + Ok(suites) } -/// Query the test binary for required artifacts given a list of test names. -/// -/// This invokes the binary with `--list-required-artifacts --tests-from-stdin` -/// and returns the resulting JSON as a string after processing it to inject -/// the `target` field. -fn query_test_binary_artifacts( - test_binary: &Path, - test_names: &[String], - target: &str, -) -> anyhow::Result { - log::info!("Using test binary: {}", test_binary.display()); - log::info!("Querying artifacts for {} tests", test_names.len()); - let stdin_data = test_names +/// Runs the test binary with `--list-required-artifacts --tests-from-stdin` +/// and returns all the required and optional artifacts for all test defined +/// in the RustSuite. +fn query_test_binary_artifacts(suite: &RustSuite) -> anyhow::Result> { + log::info!("Using test binary: {}", suite.binary_path.display()); + log::info!("Querying artifacts for {} tests", suite.testcases.len()); + let stdin_data = suite + .testcases .iter() .map(|n| format!("{n}\n")) .collect::(); - let mut child = Command::new(test_binary) + let mut child = Command::new(&suite.binary_path) .args(["--list-required-artifacts", "--tests-from-stdin"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -346,87 +498,18 @@ fn query_test_binary_artifacts( let artifact_stdout = String::from_utf8(artifact_output.stdout) .map_err(|e| anyhow::anyhow!("test output is not valid UTF-8: {}", e))?; - parse_artifacts_output(&artifact_stdout, target) -} - -/// Parse `cargo nextest list --message-format json` output to extract test -/// names and binary path. -fn parse_nextest_output(stdout: &str) -> anyhow::Result<(PathBuf, Vec)> { - let json: serde_json::Value = serde_json::from_str(stdout) - .map_err(|e| anyhow::anyhow!("failed to parse nextest JSON output: {}", e))?; - - let mut test_names = Vec::new(); - let mut binary_path = None; - - // Navigate to rust-suites -> vmm_tests::tests -> testcases - if let Some(vmm_tests) = json - .get("rust-suites") - .and_then(|s| s.get("vmm_tests::tests")) - { - if let Some(path) = vmm_tests.get("binary-path").and_then(|v| v.as_str()) { - binary_path = Some(PathBuf::from(path)); - } - - if let Some(testcases_obj) = vmm_tests.get("testcases").and_then(|t| t.as_object()) { - for (test_name, test_info) in testcases_obj { - let matches = test_info - .get("filter-match") - .and_then(|fm| fm.get("status")) - .and_then(|s| s.as_str()) - == Some("matches"); - - if matches { - test_names.push(test_name.clone()); - } - } - } - } - - let binary_path = binary_path - .ok_or_else(|| anyhow::anyhow!("Could not find test binary path in nextest output"))?; - - Ok((binary_path, test_names)) -} - -/// Parse test binary `--list-required-artifacts` JSON output and add target -/// info. -fn parse_artifacts_output(stdout: &str, target: &str) -> anyhow::Result { - let json: serde_json::Value = serde_json::from_str(stdout) + let ArtifactListOutput { + mut required, + mut optional, + } = serde_json::from_str(&artifact_stdout) .map_err(|e| anyhow::anyhow!("failed to parse test output JSON: {}", e))?; - let required = json - .get("required") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .map(String::from) - .collect::>() - }) - .unwrap_or_default(); - - let optional = json - .get("optional") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str()) - .map(String::from) - .collect::>() - }) - .unwrap_or_default(); - - let output = serde_json::json!({ - "target": target, - "required": required, - "optional": optional, - }); - - Ok(serde_json::to_string_pretty(&output)?) + let mut artifacts = Vec::new(); + artifacts.append(&mut required); + artifacts.append(&mut optional); + Ok(artifacts) } -// Target resolution and pipeline construction helpers - #[derive(clap::ValueEnum, Copy, Clone)] enum VmmTestTargetCli { /// Windows Aarch64 @@ -470,19 +553,26 @@ fn resolve_target( /// requires VHDs to reside on a Windows filesystem. On native Windows or Linux /// this check is a no-op. fn validate_output_dir( - dir: &Path, + dir: Option<&Path>, target_os: target_lexicon::OperatingSystem, ) -> anyhow::Result<()> { - if flowey_cli::running_in_wsl() - && matches!(target_os, target_lexicon::OperatingSystem::Windows) - && !flowey_cli::is_wsl_windows_path(dir) + if flowey_cli::running_in_wsl() && matches!(target_os, target_lexicon::OperatingSystem::Windows) { - anyhow::bail!( - "When targeting Windows from WSL, --dir must be a path on Windows \ - (i.e., on a DrvFs mount like /mnt/c/vmm_tests). \ - Got: {}", - dir.display() - ); + if let Some(dir) = dir { + if !flowey_cli::is_wsl_windows_path(dir) { + anyhow::bail!( + "When targeting Windows from WSL, --dir must be a path on Windows \ + (i.e., on a DrvFs mount like /mnt/c/vmm_tests). \ + Got: {}", + dir.display() + ); + } + } else { + anyhow::bail!( + "An output directory on the Windows filesystem \ + must be specified when targeting Windows from WSL." + ) + } } Ok(()) } @@ -510,105 +600,189 @@ fn selections_from_resolved( } } -struct VmmTestsPipelineOptions { - verbose: bool, - install_missing_deps: bool, - unstable_whp: bool, - release: bool, - build_only: bool, - copy_extras: bool, - skip_vhd_prompt: bool, - custom_kernel_modules: Option, - custom_kernel: Option, - custom_uefi_firmware: Option, -} +impl ResolvedArtifactSelections { + /// Resolve a single artifact ID and update selections. + fn resolve_artifact(&mut self, id: &str) -> anyhow::Result<()> { + match id { + // OpenVMM binary + petri_artifacts_vmm_test::artifacts::OPENVMM_WIN_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::OPENVMM_LINUX_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::OPENVMM_WIN_AARCH64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::OPENVMM_LINUX_AARCH64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::OPENVMM_MACOS_AARCH64::GLOBAL_UNIQUE_ID => { + self.build.openvmm = true; + } -/// Construct the pipeline job for building and running VMM tests. -fn build_vmm_tests_pipeline( - backend_hint: PipelineBackendHint, - target: CommonTriple, - selections: VmmTestSelections, - dir: PathBuf, - opts: VmmTestsPipelineOptions, -) -> anyhow::Result { - let target_architecture = target.as_triple().architecture; - let recipe_arch = match target_architecture { - target_lexicon::Architecture::X86_64 => CommonArch::X86_64, - target_lexicon::Architecture::Aarch64(_) => CommonArch::Aarch64, - _ => anyhow::bail!("Unsupported architecture: {:?}", target_architecture), - }; + // OpenVMM vhost binary (Linux only) + petri_artifacts_vmm_test::artifacts::OPENVMM_VHOST_LINUX_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::OPENVMM_VHOST_LINUX_AARCH64 ::GLOBAL_UNIQUE_ID => { + self.build.openvmm_vhost = true; + } - let openvmm_repo = flowey_lib_common::git_checkout::RepoSource::ExistingClone( - ReadVar::from_static(crate::repo_root()), - ); + // OpenHCL IGVM files + petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_CVM_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_LINUX_DIRECT_TEST_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_AARCH64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_AARCH64::GLOBAL_UNIQUE_ID => + { + self.build.openhcl = true; + } + + // Release IGVM files (downloaded, not built) + petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_RELEASE_STANDARD_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_RELEASE_LINUX_DIRECT_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_RELEASE_STANDARD_AARCH64::GLOBAL_UNIQUE_ID => + { + // These are downloaded from GitHub releases, not built + self.needs_release_igvm = true; + } - let mut pipeline = Pipeline::new(); + // Guest test UEFI + petri_artifacts_vmm_test::artifacts::test_vhd::GUEST_TEST_UEFI_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::test_vhd::GUEST_TEST_UEFI_AARCH64 ::GLOBAL_UNIQUE_ID => { + self.build.guest_test_uefi = true; + } - let mut job = pipeline.new_job( - FlowPlatform::host(backend_hint), - FlowArch::host(backend_hint), - "build vmm test dependencies", - ); + // TMKs + petri_artifacts_vmm_test::artifacts::tmks::SIMPLE_TMK_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::tmks::SIMPLE_TMK_AARCH64 ::GLOBAL_UNIQUE_ID => { + self.build.tmks = true; + } - job = job.dep_on(|_| flowey_lib_hvlite::_jobs::cfg_versions::Request::Init); - - if let (Some(kernel_path), Some(modules_path)) = ( - opts.custom_kernel.clone(), - opts.custom_kernel_modules.clone(), - ) { - job = job.dep_on( - move |_| flowey_lib_hvlite::_jobs::cfg_versions::Request::LocalKernel { - arch: recipe_arch, - kernel: ReadVar::from_static(kernel_path), - modules: ReadVar::from_static(modules_path), - }, - ); - } + // TMK VMM + petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_WIN_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_WIN_AARCH64::GLOBAL_UNIQUE_ID => { + self.build.tmk_vmm_windows = true; + } + petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_AARCH64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_X64_MUSL::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_AARCH64_MUSL::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_MACOS_AARCH64::GLOBAL_UNIQUE_ID => { + self.build.tmk_vmm_linux = true; + } - // Override UEFI firmware with a local MSVM.fd path - if let Some(fw_path) = opts.custom_uefi_firmware { - job = job.dep_on(move |_| { - flowey_lib_hvlite::_jobs::cfg_versions::Request::LocalUefi( - recipe_arch, - ReadVar::from_static(fw_path), - ) - }); - } + // VmgsTool + petri_artifacts_vmm_test::artifacts::VMGSTOOL_WIN_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::VMGSTOOL_WIN_AARCH64::GLOBAL_UNIQUE_ID => { + self.build.vmgstool = true; + } + petri_artifacts_vmm_test::artifacts::VMGSTOOL_LINUX_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::VMGSTOOL_LINUX_AARCH64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::VMGSTOOL_MACOS_AARCH64::GLOBAL_UNIQUE_ID => { + self.build.vmgstool = true; + } - job = job - .dep_on( - |_| flowey_lib_hvlite::_jobs::cfg_hvlite_reposource::Params { - hvlite_repo_source: openvmm_repo.clone(), - }, - ) - .dep_on(|_| flowey_lib_hvlite::_jobs::cfg_common::Params { - local_only: Some(flowey_lib_hvlite::_jobs::cfg_common::LocalOnlyParams { - interactive: true, - auto_install: opts.install_missing_deps, - ignore_rust_version: true, - }), - verbose: ReadVar::from_static(opts.verbose), - locked: false, - deny_warnings: false, - no_incremental: false, - }) - .side_effect(|done| { - flowey_lib_hvlite::_jobs::local_build_and_run_nextest_vmm_tests::Params { - target, - test_content_dir: dir, - selections, - unstable_whp: opts.unstable_whp, - release: opts.release, - build_only: opts.build_only, - copy_extras: opts.copy_extras, - custom_kernel_modules: opts.custom_kernel_modules, - custom_kernel: opts.custom_kernel, - skip_vhd_prompt: opts.skip_vhd_prompt, - done, - } - }); - - job.finish(); - - Ok(pipeline) + // TPM guest tests + petri_artifacts_vmm_test::artifacts::guest_tools::TPM_GUEST_TESTS_WINDOWS_X64::GLOBAL_UNIQUE_ID => { + self.build.tpm_guest_tests_windows = true; + } + petri_artifacts_vmm_test::artifacts::guest_tools::TPM_GUEST_TESTS_LINUX_X64::GLOBAL_UNIQUE_ID => { + self.build.tpm_guest_tests_linux = true; + } + + // Host tools + petri_artifacts_vmm_test::artifacts::host_tools::TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64::GLOBAL_UNIQUE_ID => + { + self.build.test_igvm_agent_rpc_server = true; + } + + // Loadable firmware artifacts (these come from deps, not built) + petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_KERNEL_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_INITRD_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_INITRD_AARCH64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::loadable::PCAT_FIRMWARE_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::loadable::SVGA_FIRMWARE_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::loadable::UEFI_FIRMWARE_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::loadable::UEFI_FIRMWARE_AARCH64::GLOBAL_UNIQUE_ID => { + // These are resolved from OpenVMM deps, always available + } + + // Test VHDs + petri_artifacts_vmm_test::artifacts::test_vhd::GEN1_WINDOWS_DATA_CENTER_CORE2022_X64::GLOBAL_UNIQUE_ID => + { + self.downloads + .insert(KnownTestArtifacts::Gen1WindowsDataCenterCore2022X64Vhd); + } + petri_artifacts_vmm_test::artifacts::test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2022_X64::GLOBAL_UNIQUE_ID => + { + self.downloads + .insert(KnownTestArtifacts::Gen2WindowsDataCenterCore2022X64Vhd); + } + petri_artifacts_vmm_test::artifacts::test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64::GLOBAL_UNIQUE_ID => + { + self.downloads + .insert(KnownTestArtifacts::Gen2WindowsDataCenterCore2025X64Vhd); + } + petri_artifacts_vmm_test::artifacts::test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64_PREPPED::GLOBAL_UNIQUE_ID => + { + self.build.prep_steps = true; + } + petri_artifacts_vmm_test::artifacts::test_vhd::FREE_BSD_13_2_X64::GLOBAL_UNIQUE_ID => { + self.downloads.insert(KnownTestArtifacts::FreeBsd13_2X64Vhd); + } + petri_artifacts_vmm_test::artifacts::test_vhd::ALPINE_3_23_X64::GLOBAL_UNIQUE_ID => { + self.downloads.insert(KnownTestArtifacts::Alpine323X64Vhd); + } + petri_artifacts_vmm_test::artifacts::test_vhd::ALPINE_3_23_AARCH64::GLOBAL_UNIQUE_ID => { + self.downloads + .insert(KnownTestArtifacts::Alpine323Aarch64Vhd); + } + petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2404_SERVER_X64::GLOBAL_UNIQUE_ID => { + self.downloads + .insert(KnownTestArtifacts::Ubuntu2404ServerX64Vhd); + } + petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2504_SERVER_X64::GLOBAL_UNIQUE_ID => { + self.downloads + .insert(KnownTestArtifacts::Ubuntu2504ServerX64Vhd); + } + petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2404_SERVER_AARCH64::GLOBAL_UNIQUE_ID => { + self.downloads + .insert(KnownTestArtifacts::Ubuntu2404ServerAarch64Vhd); + } + petri_artifacts_vmm_test::artifacts::test_vhd::WINDOWS_11_ENTERPRISE_AARCH64::GLOBAL_UNIQUE_ID => { + self.downloads + .insert(KnownTestArtifacts::Windows11EnterpriseAarch64Vhdx); + } + + // Test ISOs (downloaded) + petri_artifacts_vmm_test::artifacts::test_iso::FREE_BSD_13_2_X64::GLOBAL_UNIQUE_ID => { + self.downloads.insert(KnownTestArtifacts::FreeBsd13_2X64Iso); + } + + // Test VMGS files + petri_artifacts_vmm_test::artifacts::test_vmgs::VMGS_WITH_BOOT_ENTRY::GLOBAL_UNIQUE_ID => { + self.downloads.insert(KnownTestArtifacts::VmgsWithBootEntry); + } + petri_artifacts_vmm_test::artifacts::test_vmgs::VMGS_WITH_16K_TPM::GLOBAL_UNIQUE_ID => { + self.downloads.insert(KnownTestArtifacts::VmgsWith16kTpm); + } + + // OpenHCL usermode binaries (built as part of IGVM) + petri_artifacts_vmm_test::artifacts::openhcl_igvm::um_bin::LATEST_LINUX_DIRECT_TEST_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_vmm_test::artifacts::openhcl_igvm::um_dbg::LATEST_LINUX_DIRECT_TEST_X64::GLOBAL_UNIQUE_ID => + { + self.build.openhcl = true; + } + + // Common artifacts (always available, no build needed) + petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY::GLOBAL_UNIQUE_ID => {} + + // Pipette binaries (from petri_artifacts_common) + petri_artifacts_common::artifacts::PIPETTE_LINUX_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_common::artifacts::PIPETTE_LINUX_AARCH64::GLOBAL_UNIQUE_ID => { + self.build.pipette_linux = true; + } + petri_artifacts_common::artifacts::PIPETTE_WINDOWS_X64::GLOBAL_UNIQUE_ID + | petri_artifacts_common::artifacts::PIPETTE_WINDOWS_AARCH64::GLOBAL_UNIQUE_ID => { + self.build.pipette_windows = true; + } + + _ => anyhow::bail!("unknown artifact: {id}"), + }; + Ok(()) + } } diff --git a/flowey/flowey_lib_hvlite/Cargo.toml b/flowey/flowey_lib_hvlite/Cargo.toml index 81d2a25ef2..51adc1d792 100644 --- a/flowey/flowey_lib_hvlite/Cargo.toml +++ b/flowey/flowey_lib_hvlite/Cargo.toml @@ -27,3 +27,7 @@ powershell_builder.workspace = true [lints] workspace = true + +[package.metadata.xtask.unused-deps] +# Pulled in via prelude +ignored = ["serde"] diff --git a/flowey/flowey_lib_hvlite/src/_jobs/consume_and_test_nextest_vmm_tests_archive.rs b/flowey/flowey_lib_hvlite/src/_jobs/consume_and_test_nextest_vmm_tests_archive.rs index 6cf9e63697..134495353a 100644 --- a/flowey/flowey_lib_hvlite/src/_jobs/consume_and_test_nextest_vmm_tests_archive.rs +++ b/flowey/flowey_lib_hvlite/src/_jobs/consume_and_test_nextest_vmm_tests_archive.rs @@ -192,6 +192,7 @@ impl SimpleFlowNode for Node { release_igvm_files, use_relative_paths: false, disable_remote_artifacts: true, + reuse_prepped_vhds: false, }); // Start the test_igvm_agent_rpc_server before running tests (Windows only). diff --git a/flowey/flowey_lib_hvlite/src/_jobs/local_build_and_run_nextest_vmm_tests.rs b/flowey/flowey_lib_hvlite/src/_jobs/local_build_and_run_nextest_vmm_tests.rs index e00f7e93c3..49dff96e97 100644 --- a/flowey/flowey_lib_hvlite/src/_jobs/local_build_and_run_nextest_vmm_tests.rs +++ b/flowey/flowey_lib_hvlite/src/_jobs/local_build_and_run_nextest_vmm_tests.rs @@ -53,30 +53,7 @@ pub struct BuildSelections { pub test_igvm_agent_rpc_server: bool, } -// Build everything we can by default -impl Default for BuildSelections { - fn default() -> Self { - Self { - prep_steps: true, - openhcl: true, - openvmm: true, - openvmm_vhost: true, - pipette_windows: true, - pipette_linux: true, - guest_test_uefi: true, - tmks: true, - tmk_vmm_windows: true, - tmk_vmm_linux: true, - vmgstool: true, - tpm_guest_tests_windows: true, - tpm_guest_tests_linux: true, - test_igvm_agent_rpc_server: true, - } - } -} - impl BuildSelections { - /// No selections (build nothing) pub fn none() -> Self { Self { prep_steps: false, @@ -123,6 +100,10 @@ flowey_request! { /// Skip the interactive VHD download prompt pub skip_vhd_prompt: bool, + pub nextest_profile: crate::run_cargo_nextest_run::NextestProfile, + + pub reuse_prepped_vhds: bool, + pub done: WriteVar, } } @@ -171,6 +152,8 @@ impl SimpleFlowNode for Node { custom_kernel_modules, custom_kernel, skip_vhd_prompt, + nextest_profile, + reuse_prepped_vhds, done, } = request; @@ -726,6 +709,7 @@ impl SimpleFlowNode for Node { release_igvm_files, use_relative_paths: build_only, disable_remote_artifacts: false, + reuse_prepped_vhds, }); let mut side_effects = Vec::new(); @@ -782,8 +766,6 @@ impl SimpleFlowNode for Node { } })); - let nextest_profile = crate::run_cargo_nextest_run::NextestProfile::Default; - let nextest_run_cmd = ctx.reqv(|v| flowey_lib_common::gen_cargo_nextest_run_cmd::Request { run_kind_deps: RunKindDeps::RunFromArchive { archive_file: ReadVar::from_static(nextest_archive_file.clone()), diff --git a/flowey/flowey_lib_hvlite/src/artifact_to_build_mapping.rs b/flowey/flowey_lib_hvlite/src/artifact_to_build_mapping.rs deleted file mode 100644 index 1ea2e7c715..0000000000 --- a/flowey/flowey_lib_hvlite/src/artifact_to_build_mapping.rs +++ /dev/null @@ -1,403 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Mapping from petri artifact IDs to flowey build/download selections. -//! -//! This module defines a lookup table that maps the string representation of -//! petri `ArtifactHandle` IDs (as output by `--list-required-artifacts`) to -//! their corresponding build selections and download artifacts. -//! -//! FUTURE: This is currently a manual lookup table that requires fixups each -//! time a new artifact is added, but this requires a rearchitecture of petri's -//! artifact system and type erasure. Live with this for now since this is only -//! used in the local vmm-test-run workflow, and is straightforward to fix. - -use crate::_jobs::local_build_and_run_nextest_vmm_tests::BuildSelections; -use std::collections::BTreeSet; -use vmm_test_images::KnownTestArtifacts; - -/// Result of resolving artifact requirements to build/download selections. -#[derive(Debug)] -pub struct ResolvedArtifactSelections { - /// What to build - pub build: BuildSelections, - /// What to download - pub downloads: BTreeSet, - /// Any unknown artifacts that couldn't be mapped - pub unknown: Vec, - /// Target triple from the artifacts file (if present) - pub target_from_file: Option, - /// Whether any tests need release IGVM files from GitHub - pub needs_release_igvm: bool, -} - -impl Default for ResolvedArtifactSelections { - fn default() -> Self { - Self { - build: BuildSelections::none(), - downloads: BTreeSet::new(), - unknown: Vec::new(), - target_from_file: None, - needs_release_igvm: false, - } - } -} - -impl ResolvedArtifactSelections { - /// Parse the JSON output from `--list-required-artifacts` and resolve to - /// build/download selections. - /// - /// The `target_arch` and `target_os` parameters specify the target to - /// validate against. If the JSON contains a `target` field, it will be - /// checked to ensure it matches. - pub fn from_artifact_list_json( - json: &str, - target_arch: target_lexicon::Architecture, - target_os: target_lexicon::OperatingSystem, - ) -> anyhow::Result { - let parsed: ArtifactListOutput = serde_json::from_str(json)?; - - // Validate target if present in the JSON - if let Some(ref file_target) = parsed.target { - let expected_target = format!( - "{}-{}", - match target_arch { - target_lexicon::Architecture::X86_64 => "x86_64", - target_lexicon::Architecture::Aarch64(_) => "aarch64", - _ => "unknown", - }, - match target_os { - target_lexicon::OperatingSystem::Windows => "pc-windows-msvc", - target_lexicon::OperatingSystem::Linux => "unknown-linux-gnu", - _ => "unknown", - } - ); - - // Check if the target in the file is compatible with what we're building for - if !file_target.contains(expected_target.split('-').next().unwrap_or("")) - || (target_os == target_lexicon::OperatingSystem::Windows - && !file_target.contains("windows")) - || (target_os == target_lexicon::OperatingSystem::Linux - && !file_target.contains("linux")) - { - anyhow::bail!( - "Target mismatch: artifacts file was generated for '{}', but building for '{}'", - file_target, - expected_target - ); - } - } - - let mut result = Self { - target_from_file: parsed.target, - ..Default::default() - }; - - // Process both required and optional artifacts - for artifact in parsed.required.iter().chain(parsed.optional.iter()) { - if !result.resolve_artifact(artifact, target_arch, target_os) { - result.unknown.push(artifact.clone()); - } - } - - Ok(result) - } - - /// Resolve a single artifact ID and update selections. Returns true if the - /// artifact was recognized. - fn resolve_artifact( - &mut self, - artifact_id: &str, - target_arch: target_lexicon::Architecture, - target_os: target_lexicon::OperatingSystem, - ) -> bool { - // Artifact IDs are in the format: - // "petri_artifacts_vmm_test::artifacts::ARTIFACT_NAME" - // or nested like: - // "petri_artifacts_vmm_test::artifacts::test_vhd::ARTIFACT_NAME" - - let is_windows = matches!(target_os, target_lexicon::OperatingSystem::Windows); - let is_x64 = matches!(target_arch, target_lexicon::Architecture::X86_64); - - match artifact_id { - // OpenVMM binary - "petri_artifacts_vmm_test::artifacts::OPENVMM_WIN_X64" - | "petri_artifacts_vmm_test::artifacts::OPENVMM_LINUX_X64" - | "petri_artifacts_vmm_test::artifacts::OPENVMM_WIN_AARCH64" - | "petri_artifacts_vmm_test::artifacts::OPENVMM_LINUX_AARCH64" - | "petri_artifacts_vmm_test::artifacts::OPENVMM_MACOS_AARCH64" => { - self.build.openvmm = true; - true - } - - // OpenVMM vhost binary (Linux only) - "petri_artifacts_vmm_test::artifacts::OPENVMM_VHOST_LINUX_X64" - | "petri_artifacts_vmm_test::artifacts::OPENVMM_VHOST_LINUX_AARCH64" => { - self.build.openvmm_vhost = true; - true - } - - // OpenHCL IGVM files - "petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_X64" - | "petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_X64" - | "petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_CVM_X64" - | "petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_LINUX_DIRECT_TEST_X64" - | "petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_AARCH64" - | "petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_STANDARD_DEV_KERNEL_AARCH64" => - { - self.build.openhcl = true; - true - } - - // Release IGVM files (downloaded, not built) - "petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_RELEASE_STANDARD_X64" - | "petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_RELEASE_LINUX_DIRECT_X64" - | "petri_artifacts_vmm_test::artifacts::openhcl_igvm::LATEST_RELEASE_STANDARD_AARCH64" => - { - // These are downloaded from GitHub releases, not built - self.needs_release_igvm = true; - true - } - - // Guest test UEFI - "petri_artifacts_vmm_test::artifacts::test_vhd::GUEST_TEST_UEFI_X64" - | "petri_artifacts_vmm_test::artifacts::test_vhd::GUEST_TEST_UEFI_AARCH64" => { - self.build.guest_test_uefi = true; - true - } - - // TMKs - "petri_artifacts_vmm_test::artifacts::tmks::SIMPLE_TMK_X64" - | "petri_artifacts_vmm_test::artifacts::tmks::SIMPLE_TMK_AARCH64" => { - self.build.tmks = true; - true - } - - // TMK VMM - "petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_WIN_X64" - | "petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_WIN_AARCH64" => { - self.build.tmk_vmm_windows = true; - true - } - "petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_X64" - | "petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_AARCH64" - | "petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_X64_MUSL" - | "petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_LINUX_AARCH64_MUSL" - | "petri_artifacts_vmm_test::artifacts::tmks::TMK_VMM_MACOS_AARCH64" => { - self.build.tmk_vmm_linux = true; - true - } - - // VMGSTool - "petri_artifacts_vmm_test::artifacts::VMGSTOOL_WIN_X64" - | "petri_artifacts_vmm_test::artifacts::VMGSTOOL_WIN_AARCH64" => { - self.build.vmgstool = true; - true - } - "petri_artifacts_vmm_test::artifacts::VMGSTOOL_LINUX_X64" - | "petri_artifacts_vmm_test::artifacts::VMGSTOOL_LINUX_AARCH64" - | "petri_artifacts_vmm_test::artifacts::VMGSTOOL_MACOS_AARCH64" => { - self.build.vmgstool = true; - true - } - - // TPM guest tests - "petri_artifacts_vmm_test::artifacts::guest_tools::TPM_GUEST_TESTS_WINDOWS_X64" => { - self.build.tpm_guest_tests_windows = true; - true - } - "petri_artifacts_vmm_test::artifacts::guest_tools::TPM_GUEST_TESTS_LINUX_X64" => { - self.build.tpm_guest_tests_linux = true; - true - } - - // Host tools - "petri_artifacts_vmm_test::artifacts::host_tools::TEST_IGVM_AGENT_RPC_SERVER_WINDOWS_X64" => - { - self.build.test_igvm_agent_rpc_server = true; - true - } - - // Loadable firmware artifacts (these come from deps, not built) - "petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_KERNEL_X64" - | "petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_INITRD_X64" - | "petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_KERNEL_AARCH64" - | "petri_artifacts_vmm_test::artifacts::loadable::LINUX_DIRECT_TEST_INITRD_AARCH64" - | "petri_artifacts_vmm_test::artifacts::loadable::PCAT_FIRMWARE_X64" - | "petri_artifacts_vmm_test::artifacts::loadable::SVGA_FIRMWARE_X64" - | "petri_artifacts_vmm_test::artifacts::loadable::UEFI_FIRMWARE_X64" - | "petri_artifacts_vmm_test::artifacts::loadable::UEFI_FIRMWARE_AARCH64" => { - // These are resolved from OpenVMM deps, always available - true - } - - // Test VHDs (downloaded) - "petri_artifacts_vmm_test::artifacts::test_vhd::GEN1_WINDOWS_DATA_CENTER_CORE2022_X64" => - { - self.downloads - .insert(KnownTestArtifacts::Gen1WindowsDataCenterCore2022X64Vhd); - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2022_X64" => - { - self.downloads - .insert(KnownTestArtifacts::Gen2WindowsDataCenterCore2022X64Vhd); - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64" => - { - self.downloads - .insert(KnownTestArtifacts::Gen2WindowsDataCenterCore2025X64Vhd); - // Requires prep_steps for CVM tests - self.build.prep_steps = is_windows && is_x64; - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64_PREPPED" => - { - // This is created by prep_steps, not downloaded - self.build.prep_steps = is_windows && is_x64; - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::FREE_BSD_13_2_X64" => { - self.downloads.insert(KnownTestArtifacts::FreeBsd13_2X64Vhd); - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::ALPINE_3_23_X64" => { - self.downloads.insert(KnownTestArtifacts::Alpine323X64Vhd); - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::ALPINE_3_23_AARCH64" => { - self.downloads - .insert(KnownTestArtifacts::Alpine323Aarch64Vhd); - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2404_SERVER_X64" => { - self.downloads - .insert(KnownTestArtifacts::Ubuntu2404ServerX64Vhd); - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2504_SERVER_X64" => { - self.downloads - .insert(KnownTestArtifacts::Ubuntu2504ServerX64Vhd); - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2404_SERVER_AARCH64" => { - self.downloads - .insert(KnownTestArtifacts::Ubuntu2404ServerAarch64Vhd); - true - } - "petri_artifacts_vmm_test::artifacts::test_vhd::WINDOWS_11_ENTERPRISE_AARCH64" => { - self.downloads - .insert(KnownTestArtifacts::Windows11EnterpriseAarch64Vhdx); - true - } - - // Test ISOs (downloaded) - "petri_artifacts_vmm_test::artifacts::test_iso::FREE_BSD_13_2_X64" => { - self.downloads.insert(KnownTestArtifacts::FreeBsd13_2X64Iso); - true - } - - // Test VMGS files (downloaded) - "petri_artifacts_vmm_test::artifacts::test_vmgs::VMGS_WITH_BOOT_ENTRY" => { - self.downloads.insert(KnownTestArtifacts::VmgsWithBootEntry); - true - } - - // OpenHCL usermode binaries (built as part of IGVM) - "petri_artifacts_vmm_test::artifacts::openhcl_igvm::um_bin::LATEST_LINUX_DIRECT_TEST_X64" - | "petri_artifacts_vmm_test::artifacts::openhcl_igvm::um_dbg::LATEST_LINUX_DIRECT_TEST_X64" => - { - self.build.openhcl = true; - true - } - - // Common artifacts (always available, no build needed) - "petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY" => true, - - // Pipette binaries (from petri_artifacts_common) - "petri_artifacts_common::artifacts::PIPETTE_LINUX_X64" - | "petri_artifacts_common::artifacts::PIPETTE_LINUX_AARCH64" => { - self.build.pipette_linux = true; - true - } - "petri_artifacts_common::artifacts::PIPETTE_WINDOWS_X64" - | "petri_artifacts_common::artifacts::PIPETTE_WINDOWS_AARCH64" => { - self.build.pipette_windows = true; - true - } - - "petri_artifacts_vmm_test::artifacts::test_vmgs::VMGS_WITH_16K_TPM" => { - self.downloads.insert(KnownTestArtifacts::VmgsWith16kTpm); - true - } - - _ => { - log::warn!("unknown artifact ID with no build mapping: {artifact_id}"); - false - } - } - } -} - -/// JSON structure matching the output of `--list-required-artifacts` -#[derive(serde::Deserialize)] -struct ArtifactListOutput { - /// Target triple the artifacts were discovered for (if present) - #[serde(default)] - target: Option, - required: Vec, - optional: Vec, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_resolve_openvmm() { - let json = r#"{"required":["petri_artifacts_vmm_test::artifacts::OPENVMM_WIN_X64"],"optional":[]}"#; - let result = ResolvedArtifactSelections::from_artifact_list_json( - json, - target_lexicon::Architecture::X86_64, - target_lexicon::OperatingSystem::Windows, - ) - .unwrap(); - - assert!(result.build.openvmm); - assert!(!result.build.openhcl); - assert!(result.downloads.is_empty()); - assert!(result.unknown.is_empty()); - } - - #[test] - fn test_resolve_with_downloads() { - let json = r#"{"required":["petri_artifacts_vmm_test::artifacts::test_vhd::UBUNTU_2404_SERVER_X64","petri_artifacts_common::artifacts::PIPETTE_LINUX_X64"],"optional":[]}"#; - let result = ResolvedArtifactSelections::from_artifact_list_json( - json, - target_lexicon::Architecture::X86_64, - target_lexicon::OperatingSystem::Linux, - ) - .unwrap(); - - assert!(result.build.pipette_linux); - assert!( - result - .downloads - .contains(&KnownTestArtifacts::Ubuntu2404ServerX64Vhd) - ); - } - - #[test] - fn test_unknown_artifact() { - let json = r#"{"required":["some::unknown::artifact"],"optional":[]}"#; - let result = ResolvedArtifactSelections::from_artifact_list_json( - json, - target_lexicon::Architecture::X86_64, - target_lexicon::OperatingSystem::Linux, - ) - .unwrap(); - - assert_eq!(result.unknown, vec!["some::unknown::artifact"]); - } -} diff --git a/flowey/flowey_lib_hvlite/src/download_openvmm_vmm_tests_artifacts.rs b/flowey/flowey_lib_hvlite/src/download_openvmm_vmm_tests_artifacts.rs index 2572cdea71..e1afb27bdf 100644 --- a/flowey/flowey_lib_hvlite/src/download_openvmm_vmm_tests_artifacts.rs +++ b/flowey/flowey_lib_hvlite/src/download_openvmm_vmm_tests_artifacts.rs @@ -8,10 +8,9 @@ use flowey::node::prelude::*; use std::collections::BTreeSet; use std::io::IsTerminal; +use vmm_test_images::CONTAINER; use vmm_test_images::KnownTestArtifacts; - -const STORAGE_ACCOUNT: &str = "hvlitetestvhds"; -const CONTAINER: &str = "vhds"; +use vmm_test_images::STORAGE_ACCOUNT; #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum CustomDiskPolicy { diff --git a/flowey/flowey_lib_hvlite/src/init_vmm_tests_env.rs b/flowey/flowey_lib_hvlite/src/init_vmm_tests_env.rs index 88073c6e16..aa4b8741f4 100644 --- a/flowey/flowey_lib_hvlite/src/init_vmm_tests_env.rs +++ b/flowey/flowey_lib_hvlite/src/init_vmm_tests_env.rs @@ -70,6 +70,8 @@ flowey_request! { /// Disable lazy remote artifact fetching (set PETRI_REMOTE_ARTIFACTS=0). /// Should be true in CI where all images are pre-downloaded. pub disable_remote_artifacts: bool, + /// Whether to reuse VHDs created with prep_steps + pub reuse_prepped_vhds: bool, } } @@ -107,6 +109,7 @@ impl SimpleFlowNode for Node { release_igvm_files, use_relative_paths, disable_remote_artifacts, + reuse_prepped_vhds, } = request; let arch = CommonArch::from_architecture(vmm_tests_target.architecture)?; @@ -254,6 +257,10 @@ impl SimpleFlowNode for Node { env.insert("PETRI_REMOTE_ARTIFACTS".into(), "0".into()); } + if reuse_prepped_vhds { + env.insert("PETRI_REUSE_PREPPED_VHDS".into(), "1".into()); + } + if let Some(openvmm) = openvmm { // TODO OSS: update filenames to use openvmm naming (requires petri updates) match rt.read(openvmm) { diff --git a/flowey/flowey_lib_hvlite/src/lib.rs b/flowey/flowey_lib_hvlite/src/lib.rs index 7ff2355423..3976df85d4 100644 --- a/flowey/flowey_lib_hvlite/src/lib.rs +++ b/flowey/flowey_lib_hvlite/src/lib.rs @@ -10,7 +10,6 @@ pub mod _jobs; pub mod artifact_openhcl_igvm_from_recipe; pub mod artifact_openhcl_igvm_from_recipe_extras; pub mod artifact_openvmm_hcl_sizecheck; -pub mod artifact_to_build_mapping; pub mod build_and_test_vmgs_lib; pub mod build_guest_test_uefi; pub mod build_guide; diff --git a/petri/petri_artifacts_core/Cargo.toml b/petri/petri_artifacts_core/Cargo.toml index dc7b21db14..0d66542135 100644 --- a/petri/petri_artifacts_core/Cargo.toml +++ b/petri/petri_artifacts_core/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] anyhow.workspace = true paste.workspace = true +serde = { workspace = true, features = ["std", "derive"] } [lints] workspace = true diff --git a/petri/petri_artifacts_core/src/lib.rs b/petri/petri_artifacts_core/src/lib.rs index 2ea355d2db..53223d9b24 100644 --- a/petri/petri_artifacts_core/src/lib.rs +++ b/petri/petri_artifacts_core/src/lib.rs @@ -51,6 +51,10 @@ pub trait ArtifactId: 'static { #[doc(hidden)] const GLOBAL_UNIQUE_ID: &'static str; + /// Whether this artifact can be backed by blob disk + #[doc(hidden)] + const SUPPORTS_BLOB_DISK: bool; + /// ...in case you decide to flaunt the trait-level docs regarding manually /// implementing this trait. #[doc(hidden)] @@ -187,16 +191,21 @@ pub struct ArtifactResolver<'a> { } impl<'a> ArtifactResolver<'a> { - /// Returns the default remote access policy, checking the + /// Returns the remote access policy, checking the /// `PETRI_REMOTE_ARTIFACTS` environment variable. /// /// Set `PETRI_REMOTE_ARTIFACTS=0` to force all artifacts to be resolved - /// locally, even if `RemoteAccess::Allow` is specified per-call. - fn default_remote_policy() -> RemoteAccess { - match std::env::var("PETRI_REMOTE_ARTIFACTS").as_deref() { - Ok("0") | Ok("false") => RemoteAccess::LocalOnly, - _ => RemoteAccess::Allow, - } + /// locally, even if `RemoteAccess::Allow` is specified per-call or per-test. + pub fn set_remote_policy(&mut self, test_policy: RemoteAccess) { + self.remote_policy = if matches!(test_policy, RemoteAccess::LocalOnly) + || matches!( + std::env::var("PETRI_REMOTE_ARTIFACTS").as_deref(), + Ok("0") | Ok("false") + ) { + RemoteAccess::LocalOnly + } else { + RemoteAccess::Allow + }; } /// Returns a resolver to collect requirements; the artifact objects returned by @@ -204,7 +213,7 @@ impl<'a> ArtifactResolver<'a> { pub fn collector(requirements: &'a mut TestArtifactRequirements) -> Self { ArtifactResolver { inner: ArtifactResolverInner::Collecting(RefCell::new(requirements)), - remote_policy: Self::default_remote_policy(), + remote_policy: RemoteAccess::LocalOnly, } } @@ -212,14 +221,14 @@ impl<'a> ArtifactResolver<'a> { pub fn resolver(artifacts: &'a TestArtifacts) -> Self { ArtifactResolver { inner: ArtifactResolverInner::Resolving(artifacts), - remote_policy: Self::default_remote_policy(), + remote_policy: RemoteAccess::LocalOnly, } } /// Returns the effective remote access for a given per-call policy, /// respecting the resolver-wide policy. - fn effective_remote(&self, per_call: RemoteAccess) -> RemoteAccess { - if matches!(self.remote_policy, RemoteAccess::LocalOnly) { + fn effective_remote(&self, per_call: RemoteAccess) -> RemoteAccess { + if matches!(self.remote_policy, RemoteAccess::LocalOnly) || !A::SUPPORTS_BLOB_DISK { RemoteAccess::LocalOnly } else { per_call @@ -248,7 +257,9 @@ impl<'a> ArtifactResolver<'a> { ) -> ResolvedOptionalArtifact { match &self.inner { ArtifactResolverInner::Collecting(requirements) => { - requirements.borrow_mut().try_require(handle.erase()); + requirements + .borrow_mut() + .require(handle.erase(), RemoteAccess::LocalOnly, true); ResolvedOptionalArtifact(OptionalArtifactState::Collecting, PhantomData) } ArtifactResolverInner::Resolving(artifacts) => ResolvedOptionalArtifact( @@ -273,12 +284,12 @@ impl<'a> ArtifactResolver<'a> { handle: ArtifactHandle, remote: RemoteAccess, ) -> ResolvedArtifactSource { - let effective = self.effective_remote(remote); + let effective = self.effective_remote::(remote); match &self.inner { ArtifactResolverInner::Collecting(requirements) => { requirements .borrow_mut() - .require_source(handle.erase(), effective); + .require(handle.erase(), effective, false); ResolvedArtifactSource(None, PhantomData) } ArtifactResolverInner::Resolving(artifacts) => { @@ -300,6 +311,13 @@ pub struct ErasedArtifactHandle { artifact_id_str: &'static str, } +impl ErasedArtifactHandle { + /// used to serialize the artifact handle when querying petri for test requirements + pub fn global_unique_id(&self) -> String { + self.artifact_id_str.to_string() + } +} + impl std::fmt::Debug for ErasedArtifactHandle { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // the `declare_artifacts!` macro uses `module_path!` under-the-hood to @@ -357,7 +375,7 @@ impl AsArtifactHandle for ArtifactHandle { } } -/// Declare one or more type-safe artifacts. +/// Declare one or more type-safe artifacts that do not support blob disk. #[macro_export] macro_rules! declare_artifacts { ( @@ -366,6 +384,45 @@ macro_rules! declare_artifacts { $name:ident ),* $(,)? + ) => { + $crate::declare_artifacts_inner!( + $( + $(#[$doc])* + $name(false), + )* + ); + }; +} + +/// Declare one or more type-safe artifacts that support blob disk. +#[macro_export] +macro_rules! declare_blob_artifacts { + ( + $( + $(#[$doc:meta])* + $name:ident + ),* + $(,)? + ) => { + $crate::declare_artifacts_inner!( + $( + $(#[$doc])* + $name(true), + )* + ); + }; +} + +/// Declare one or more type-safe artifacts, specifying whether each supports +/// blob disk. +#[macro_export] +macro_rules! declare_artifacts_inner { + ( + $( + $(#[$doc:meta])* + $name:ident($supports_blob_disk:literal) + ),* + $(,)? ) => { $( $crate::paste::paste! { @@ -382,6 +439,7 @@ macro_rules! declare_artifacts { mod [< $name __ty >] { impl $crate::ArtifactId for super::$name { const GLOBAL_UNIQUE_ID: &'static str = module_path!(); + const SUPPORTS_BLOB_DISK: bool = $supports_blob_disk; fn i_know_what_im_doing_with_this_manual_impl_instead_of_using_the_declare_artifacts_macro() {} } } @@ -444,36 +502,15 @@ impl TestArtifactRequirements { } } - /// Add a dependency to the set of required artifacts (must be local). - pub fn require(&mut self, dependency: impl AsArtifactHandle) -> &mut Self { - self.require_source(dependency, RemoteAccess::LocalOnly) - } - - /// Add an optional dependency to the set of artifacts. - pub fn try_require(&mut self, dependency: impl AsArtifactHandle) -> &mut Self { - self.artifacts.push(( - dependency.erase(), - ArtifactRequirement { - optional: true, - remote: RemoteAccess::LocalOnly, - }, - )); - self - } - - /// Add a dependency that may resolve to a remote URL. - pub fn require_source( + /// Add a dependency that may be optional or resolve to a remote URL. + pub fn require( &mut self, dependency: impl AsArtifactHandle, remote: RemoteAccess, + optional: bool, ) -> &mut Self { - self.artifacts.push(( - dependency.erase(), - ArtifactRequirement { - optional: false, - remote, - }, - )); + self.artifacts + .push((dependency.erase(), ArtifactRequirement { optional, remote })); self } @@ -594,3 +631,12 @@ impl TestArtifacts { .unwrap_or_else(|| panic!("Artifact not initially required: {:?}", artifact.erase())) } } + +/// JSON output format for `--list-required-artifacts`. +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ArtifactListOutput { + /// List of unique required artifact IDs across all matching tests. + pub required: Vec, + /// List of unique optional artifact IDs across all matching tests. + pub optional: Vec, +} diff --git a/petri/src/test.rs b/petri/src/test.rs index 6d705df123..9641dcd556 100644 --- a/petri/src/test.rs +++ b/petri/src/test.rs @@ -30,6 +30,7 @@ use crate::requirements::can_run_test_with_context; use crate::tracing::try_init_tracing; use anyhow::Context as _; use petri_artifacts_core::ArtifactResolver; +use petri_artifacts_core::RemoteAccess; use std::panic::AssertUnwindSafe; use std::panic::catch_unwind; use test_macro_support::TESTS; @@ -39,7 +40,15 @@ use test_macro_support::TESTS; macro_rules! test { ($f:ident, $req:expr) => { $crate::multitest!(vec![ - $crate::SimpleTest::new(stringify!($f), $req, $f, None, false).into() + $crate::SimpleTest::new( + stringify!($f), + $req, + $f, + None, + false, + ::petri::RemoteAccess::LocalOnly + ) + .into() ]); }; } @@ -49,7 +58,15 @@ macro_rules! test { macro_rules! unstable_test { ($f:ident, $req:expr) => { $crate::multitest!(vec![ - $crate::SimpleTest::new(stringify!($f), $req, $f, None, true).into() + $crate::SimpleTest::new( + stringify!($f), + $req, + $f, + None, + true, + ::petri::RemoteAccess::LocalOnly + ) + .into() ]); }; } @@ -99,8 +116,11 @@ impl Test { tests.into_iter().filter_map(move |test| { let mut artifact_requirements = test.0.artifact_requirements()?; // All tests require the log directory. - artifact_requirements - .require(petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY); + artifact_requirements.require( + petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY, + RemoteAccess::LocalOnly, + false, + ); Some(Self { module, artifact_requirements, @@ -214,7 +234,7 @@ pub trait RunTest: Send { /// Returns `None` if this test makes no sense for this host environment /// (e.g., an x86_64 test on an aarch64 host) and should be left out of the /// test list. - fn resolve(&self, resolver: &ArtifactResolver<'_>) -> Option; + fn resolve(&self, resolver: ArtifactResolver<'_>) -> Option; /// Runs the test, which has been assigned `name`, with the given /// `artifacts`. fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()>; @@ -239,13 +259,13 @@ impl DynRunTest for T { fn artifact_requirements(&self) -> Option { let mut requirements = TestArtifactRequirements::new(); - self.resolve(&ArtifactResolver::collector(&mut requirements))?; + self.resolve(ArtifactResolver::collector(&mut requirements))?; Some(requirements) } fn run(&self, params: PetriTestParams<'_>, artifacts: &TestArtifacts) -> anyhow::Result<()> { let artifacts = self - .resolve(&ArtifactResolver::resolver(artifacts)) + .resolve(ArtifactResolver::resolver(artifacts)) .context("test should have been skipped")?; self.run(params, artifacts) } @@ -303,6 +323,7 @@ pub struct SimpleTest { /// Optional test requirements pub host_requirements: Option, unstable: bool, + remote_policy: RemoteAccess, } impl SimpleTest @@ -319,6 +340,7 @@ where run: F, host_requirements: Option, unstable: bool, + remote_policy: RemoteAccess, ) -> Self { SimpleTest { leaf_name, @@ -326,6 +348,7 @@ where run, host_requirements, unstable, + remote_policy, } } } @@ -342,8 +365,9 @@ where self.leaf_name } - fn resolve(&self, resolver: &ArtifactResolver<'_>) -> Option { - (self.resolve)(resolver) + fn resolve(&self, mut resolver: ArtifactResolver<'_>) -> Option { + resolver.set_remote_policy(self.remote_policy); + (self.resolve)(&resolver) } fn run(&self, params: PetriTestParams<'_>, artifacts: Self::Artifacts) -> anyhow::Result<()> { @@ -380,15 +404,6 @@ struct Options { inner: libtest_mimic::Arguments, } -/// JSON output format for `--list-required-artifacts`. -#[derive(serde::Serialize)] -struct ArtifactListOutput { - /// List of unique required artifact IDs across all matching tests. - required: Vec, - /// List of unique optional artifact IDs across all matching tests. - optional: Vec, -} - /// Entry point for test binaries. pub fn test_main( resolve: fn(&str, TestArtifactRequirements) -> anyhow::Result, @@ -430,19 +445,18 @@ pub fn test_main( if matches { for artifact in test.artifact_requirements.required_artifacts() { - required_set.insert(format!("{artifact:?}")); + required_set.insert(artifact.global_unique_id()); } for artifact in test.artifact_requirements.optional_artifacts() { - optional_set.insert(format!("{artifact:?}")); + optional_set.insert(artifact.global_unique_id()); } } } // Remove from optional any artifacts that are required - let optional_set: BTreeSet = - optional_set.difference(&required_set).cloned().collect(); + let optional_set: BTreeSet<_> = optional_set.difference(&required_set).cloned().collect(); - let output = ArtifactListOutput { + let output = petri_artifacts_core::ArtifactListOutput { required: required_set.into_iter().collect(), optional: optional_set.into_iter().collect(), }; diff --git a/vmm_tests/petri_artifact_resolver_openvmm_known_paths/src/lib.rs b/vmm_tests/petri_artifact_resolver_openvmm_known_paths/src/lib.rs index 1ff30ad5f3..a1b6147ff8 100644 --- a/vmm_tests/petri_artifact_resolver_openvmm_known_paths/src/lib.rs +++ b/vmm_tests/petri_artifact_resolver_openvmm_known_paths/src/lib.rs @@ -12,6 +12,9 @@ use petri_artifacts_core::ErasedArtifactHandle; use std::env::consts::EXE_EXTENSION; use std::path::Path; use std::path::PathBuf; +use vmm_test_images::CONTAINER; +use vmm_test_images::KnownTestArtifacts; +use vmm_test_images::STORAGE_ACCOUNT; /// Returns the Cargo build profile directory name for cross-compiled /// artifacts (e.g., pipette). @@ -91,6 +94,7 @@ impl petri_artifacts_core::ResolveTestArtifact for OpenvmmKnownPathsTestArtifact _ if id == test_vhd::GUEST_TEST_UEFI_X64 => guest_test_uefi_disk_path(MachineArch::X86_64), _ if id == test_vhd::GUEST_TEST_UEFI_AARCH64 => guest_test_uefi_disk_path(MachineArch::Aarch64), + _ if id == test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64_PREPPED => { let base_filename = test_vhd::GEN2_WINDOWS_DATA_CENTER_CORE2025_X64::FILENAME; let prepped_filename = base_filename.replace(".vhd", "-prepped.vhd"); @@ -106,13 +110,6 @@ impl petri_artifacts_core::ResolveTestArtifact for OpenvmmKnownPathsTestArtifact ) } - // Blob-hosted artifacts: resolved via blob_artifact_info. - _ => { - if let Some(artifact) = blob_artifact_info(id) { - return get_test_artifact_path(artifact.filename(), artifact.name()); - } - - match id { _ if id == tmks::TMK_VMM_NATIVE => tmk_vmm_native_executable_path(), _ if id == tmks::TMK_VMM_LINUX_X64_MUSL => tmk_vmm_paravisor_path(MachineArch::X86_64), _ if id == tmks::TMK_VMM_LINUX_AARCH64_MUSL => tmk_vmm_paravisor_path(MachineArch::Aarch64), @@ -132,9 +129,12 @@ impl petri_artifacts_core::ResolveTestArtifact for OpenvmmKnownPathsTestArtifact test_igvm_agent_rpc_server_windows_path(MachineArch::X86_64) } - _ => anyhow::bail!("no support for given artifact type"), - } + // Blob-hosted artifacts: resolved via blob_artifact_info. + _ if let Some(artifact) = KnownTestArtifacts::from_handle(id) => { + get_test_artifact_path(artifact) } + + _ => anyhow::bail!("no support for given artifact type"), } } @@ -147,12 +147,11 @@ impl petri_artifacts_core::ResolveTestArtifact for OpenvmmKnownPathsTestArtifact // Fall back to remote URL for artifacts hosted on Azure Blob Storage, // but only for formats the blob disk backend supports (fixed VHD1 and flat). - if let Some(artifact) = blob_artifact_info(id) { - let filename = artifact.filename(); - if filename.ends_with(".vhd") || filename.ends_with(".iso") { + if let Some(artifact) = KnownTestArtifacts::from_handle(id) { + if artifact.supports_blob_disk() { let url = format!( "https://{STORAGE_ACCOUNT}.blob.core.windows.net/{CONTAINER}/{}", - filename + artifact.filename() ); return Ok(ArtifactSource::Remote { url }); } @@ -163,15 +162,6 @@ impl petri_artifacts_core::ResolveTestArtifact for OpenvmmKnownPathsTestArtifact } } -const STORAGE_ACCOUNT: &str = "hvlitetestvhds"; -const CONTAINER: &str = "vhds"; - -/// Returns blob-hosted artifact info (filename, download name) for the given -/// artifact handle, if it is a known blob-hosted artifact. -fn blob_artifact_info(id: ErasedArtifactHandle) -> Option { - vmm_test_images::KnownTestArtifacts::from_handle(id) -} - /// Returns the bundle-relative file name for the given artifact. /// /// This is the `file_name` argument that [`get_path`] would use when @@ -258,15 +248,20 @@ fn windows_msvc_target(arch: MachineArch) -> &'static str { } } -fn get_test_artifact_path(filename: &str, download_name: &str) -> Result { +fn get_test_artifact_path(artifact: KnownTestArtifacts) -> Result { let images_dir = std::env::var("VMM_TEST_IMAGES"); let full_path = Path::new(images_dir.as_deref().unwrap_or("images")); get_path( full_path, - filename, + artifact.filename(), MissingCommand::Xtask { - xtask_args: &["guest-test", "download-image", "--artifacts", download_name], + xtask_args: &[ + "guest-test", + "download-image", + "--artifacts", + artifact.name(), + ], description: "test artifact", }, ) diff --git a/vmm_tests/petri_artifacts_vmm_test/src/lib.rs b/vmm_tests/petri_artifacts_vmm_test/src/lib.rs index 555d859c4e..83ea00a7b7 100644 --- a/vmm_tests/petri_artifacts_vmm_test/src/lib.rs +++ b/vmm_tests/petri_artifacts_vmm_test/src/lib.rs @@ -273,6 +273,11 @@ pub mod artifacts { } } + /// Azure storage account where test VHDs, ISOs, and VMGS files are stored + pub const STORAGE_ACCOUNT: &str = "hvlitetestvhds"; + /// Azure container where test VHDs, ISOs, and VMGS files are stored + pub const CONTAINER: &str = "vhds"; + /// Test VHD artifacts pub mod test_vhd { use crate::tags::IsHostedOnHvliteAzureBlobStore; @@ -283,6 +288,7 @@ pub mod artifacts { use petri_artifacts_common::tags::MachineArch; use petri_artifacts_common::tags::OsFlavor; use petri_artifacts_core::declare_artifacts; + use petri_artifacts_core::declare_blob_artifacts; declare_artifacts! { /// guest_test_uefi.img, built for x86_64 from the in-tree `guest_test_uefi` codebase. @@ -305,7 +311,7 @@ pub mod artifacts { // built just-in-time, using the code that is present in-tree, under // `guest_test_uefi`. - declare_artifacts! { + declare_blob_artifacts! { /// Generation 1 windows test image GEN1_WINDOWS_DATA_CENTER_CORE2022_X64 } @@ -322,7 +328,7 @@ pub mod artifacts { const DOWNLOAD_NAME: &'static str = "Gen1WindowsDataCenterCore2022X64Vhd"; } - declare_artifacts! { + declare_blob_artifacts! { /// Generation 2 windows test image GEN2_WINDOWS_DATA_CENTER_CORE2022_X64 } @@ -339,7 +345,7 @@ pub mod artifacts { const DOWNLOAD_NAME: &'static str = "Gen2WindowsDataCenterCore2022X64Vhd"; } - declare_artifacts! { + declare_blob_artifacts! { /// Generation 2 windows test image GEN2_WINDOWS_DATA_CENTER_CORE2025_X64 } @@ -363,7 +369,7 @@ pub mod artifacts { const DOWNLOAD_NAME: &'static str = "Gen2WindowsDataCenterCore2025X64Vhd"; } - declare_artifacts! { + declare_blob_artifacts! { /// FreeBSD 13.2 FREE_BSD_13_2_X64 } @@ -386,7 +392,7 @@ pub mod artifacts { const DOWNLOAD_NAME: &'static str = "FreeBsd13_2X64Vhd"; } - declare_artifacts! { + declare_blob_artifacts! { /// Ubuntu 24.04 Server X64 UBUNTU_2404_SERVER_X64 } @@ -408,7 +414,7 @@ pub mod artifacts { const DOWNLOAD_NAME: &'static str = "Ubuntu2404ServerX64Vhd"; } - declare_artifacts! { + declare_blob_artifacts! { /// Ubuntu 25.04 Server X64 UBUNTU_2504_SERVER_X64 } @@ -430,7 +436,7 @@ pub mod artifacts { const DOWNLOAD_NAME: &'static str = "Ubuntu2504ServerX64Vhd"; } - declare_artifacts! { + declare_blob_artifacts! { /// Alpine Linux 3.23.2 x64 UEFI nocloud cloud-init /// NOTE: The image on the alpine website is qcow2 and must be converted to a fixed vhd. ALPINE_3_23_X64 @@ -453,7 +459,7 @@ pub mod artifacts { const DOWNLOAD_NAME: &'static str = "Alpine323X64Vhd"; } - declare_artifacts! { + declare_blob_artifacts! { /// Alpine Linux 3.23.2 aarch64 UEFI nocloud cloud-init /// NOTE: The image on the alpine website is qcow2 and must be converted to a fixed vhd. ALPINE_3_23_AARCH64 @@ -476,7 +482,7 @@ pub mod artifacts { const DOWNLOAD_NAME: &'static str = "Alpine323Aarch64Vhd"; } - declare_artifacts! { + declare_blob_artifacts! { /// Ubuntu 24.04 Server Aarch64 UBUNTU_2404_SERVER_AARCH64 } @@ -498,6 +504,7 @@ pub mod artifacts { const DOWNLOAD_NAME: &'static str = "Ubuntu2404ServerAarch64Vhd"; } + // blob disk does not support VHDX files declare_artifacts! { /// Windows 11 Enterprise ARM64 24H2 WINDOWS_11_ENTERPRISE_AARCH64 @@ -547,9 +554,9 @@ pub mod artifacts { use petri_artifacts_common::tags::IsTestIso; use petri_artifacts_common::tags::MachineArch; use petri_artifacts_common::tags::OsFlavor; - use petri_artifacts_core::declare_artifacts; + use petri_artifacts_core::declare_blob_artifacts; - declare_artifacts! { + declare_blob_artifacts! { /// FreeBSD 13.2 FREE_BSD_13_2_X64 } @@ -579,6 +586,8 @@ pub mod artifacts { use petri_artifacts_common::tags::IsTestVmgs; use petri_artifacts_core::declare_artifacts; + // These could support blob disk in some cases, but Petri doesn't support + // remote VMGS files and they are small, so just disable it for now. declare_artifacts! { /// VMGS file containing a UEFI boot entry /// diff --git a/vmm_tests/prep_steps/src/main.rs b/vmm_tests/prep_steps/src/main.rs index fb1bd0d2d5..0b02012930 100644 --- a/vmm_tests/prep_steps/src/main.rs +++ b/vmm_tests/prep_steps/src/main.rs @@ -92,7 +92,15 @@ fn run( .replace(".vhd", "-prepped.vhd"), ); if result_disk.exists() { - tracing::warn!("Result disk already exists, recreating it."); + if std::env::var("PETRI_REUSE_PREPPED_VHDS") + .ok() + .is_some_and(|v| v.eq_ignore_ascii_case("true") || v == "1") + { + tracing::info!("Result disk already exists, skipping..."); + return Ok(()); + } else { + tracing::warn!("Result disk already exists, recreating it."); + } } else { tracing::info!("Copying source disk to result disk."); } diff --git a/vmm_tests/vmm_test_images/src/lib.rs b/vmm_tests/vmm_test_images/src/lib.rs index 0e31bb9759..682679803c 100644 --- a/vmm_tests/vmm_test_images/src/lib.rs +++ b/vmm_tests/vmm_test_images/src/lib.rs @@ -15,10 +15,14 @@ #![forbid(unsafe_code)] +use petri_artifacts_core::ArtifactId; use petri_artifacts_core::AsArtifactHandle; use petri_artifacts_core::ErasedArtifactHandle; use petri_artifacts_vmm_test::tags::IsHostedOnHvliteAzureBlobStore; +pub use petri_artifacts_vmm_test::artifacts::CONTAINER; +pub use petri_artifacts_vmm_test::artifacts::STORAGE_ACCOUNT; + /// The VHDs currently stored in Azure Blob Storage. #[cfg_attr(feature = "clap", derive(clap::ValueEnum))] #[cfg_attr(feature = "clap", clap(rename_all = "verbatim"))] @@ -47,6 +51,7 @@ struct KnownTestArtifactMeta { filename: &'static str, size: u64, download_name: &'static str, + supports_blob_disk: bool, } const KNOWN_TEST_ARTIFACT_METADATA: &[KnownTestArtifactMeta] = { @@ -79,7 +84,7 @@ const KNOWN_TEST_ARTIFACT_METADATA: &[KnownTestArtifactMeta] = { ] }; -const fn meta( +const fn meta( variant: KnownTestArtifacts, ) -> KnownTestArtifactMeta { KnownTestArtifactMeta { @@ -88,6 +93,7 @@ const fn meta( filename: T::FILENAME, size: T::SIZE, download_name: T::DOWNLOAD_NAME, + supports_blob_disk: T::SUPPORTS_BLOB_DISK, } } @@ -136,4 +142,9 @@ impl KnownTestArtifacts { .find(|m| (m.handle_fn)() == id) .map(|m| m.variant) } + + /// Whether this artifact supports blob disk + pub fn supports_blob_disk(&self) -> bool { + self.meta().supports_blob_disk + } } diff --git a/vmm_tests/vmm_test_macros/src/lib.rs b/vmm_tests/vmm_test_macros/src/lib.rs index 99f3de5eee..b33517b3a0 100644 --- a/vmm_tests/vmm_test_macros/src/lib.rs +++ b/vmm_tests/vmm_test_macros/src/lib.rs @@ -174,11 +174,11 @@ impl ToTokens for PcatGuest { tokens.extend(match self { PcatGuest::Vhd(known_vhd) => { let vhd = known_vhd.image_artifact.clone(); - quote!(::petri::PcatGuest::Vhd(petri::BootImageConfig::from_vhd(resolver.require_source(#vhd, remote_access)))) + quote!(::petri::PcatGuest::Vhd(petri::BootImageConfig::from_vhd(resolver.require_source(#vhd, ::petri::RemoteAccess::Allow)))) } PcatGuest::Iso(known_iso) => { let iso = known_iso.image_artifact.clone(); - quote!(::petri::PcatGuest::Iso(petri::BootImageConfig::from_iso(resolver.require_source(#iso, remote_access)))) + quote!(::petri::PcatGuest::Iso(petri::BootImageConfig::from_iso(resolver.require_source(#iso, ::petri::RemoteAccess::Allow)))) } }); } @@ -199,7 +199,7 @@ impl ToTokens for UefiGuest { tokens.extend(match self { UefiGuest::Vhd(known_vhd) => { let v = known_vhd.image_artifact.clone(); - quote!(::petri::UefiGuest::Vhd(petri::BootImageConfig::from_vhd(resolver.require_source(#v, remote_access)))) + quote!(::petri::UefiGuest::Vhd(petri::BootImageConfig::from_vhd(resolver.require_source(#v, ::petri::RemoteAccess::Allow)))) } UefiGuest::GuestTestUefi(arch) => { let arch_tokens = arch_to_tokens(*arch); @@ -870,7 +870,6 @@ fn make_vmm_test(args: ArgsWithOverrides, item: ItemFn) -> syn::Result syn::Result anyhow::Result<()> { if self.artifacts.is_empty() {