Skip to main content

petri/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! A Rust-based testing framework for VMMs.
5//!
6//! At this time - `petri` supports testing OpenVMM, OpenHCL,
7//! and Hyper-V based VMs.
8
9// TODO: Remove this dependency by adding a frontend worker that handles
10// hypervisor auto-detection in the spawned openvmm process instead of
11// requiring probes to be registered in the petri process.
12extern crate openvmm_hypervisors as _;
13
14pub mod disk_image;
15pub mod initrd;
16mod linux_direct_serial_agent;
17pub mod mshv_boot;
18pub mod mshv_image;
19// TODO: Add docs and maybe a trait interface for this, or maybe this can
20// remain crate-local somehow without violating interface privacy.
21#[expect(missing_docs)]
22pub mod openhcl_diag;
23pub mod requirements;
24mod test;
25mod tracing;
26mod vm;
27mod worker;
28
29pub use petri_artifacts_core::ArtifactHandle;
30pub use petri_artifacts_core::ArtifactResolver;
31pub use petri_artifacts_core::ArtifactSource;
32pub use petri_artifacts_core::AsArtifactHandle;
33pub use petri_artifacts_core::ErasedArtifactHandle;
34pub use petri_artifacts_core::RemoteAccess;
35pub use petri_artifacts_core::ResolveTestArtifact;
36pub use petri_artifacts_core::ResolvedArtifact;
37pub use petri_artifacts_core::ResolvedArtifactSource;
38pub use petri_artifacts_core::ResolvedOptionalArtifact;
39pub use petri_artifacts_core::TestArtifactRequirements;
40pub use petri_artifacts_core::TestArtifacts;
41pub use pipette_client as pipette;
42pub use test::PetriTestParams;
43pub use test::RunTest;
44pub use test::SimpleTest;
45pub use test::TestCase;
46pub use test::test_macro_support;
47pub use test::test_main;
48pub use tracing::*;
49pub use vm::*;
50
51use jiff::Timestamp;
52use pal_async::process::PolledChild;
53use std::ops::Deref;
54use std::ops::DerefMut;
55use std::process::Command;
56use std::process::Stdio;
57use thiserror::Error;
58
59/// 1 kibibyte's worth of bytes.
60pub const SIZE_1_KB: u64 = 1024;
61/// 1 mebibyte's worth of bytes.
62pub const SIZE_1_MB: u64 = 1024 * SIZE_1_KB;
63/// 1 gibibyte's worth of bytes.
64pub const SIZE_1_GB: u64 = 1024 * SIZE_1_MB;
65
66/// The kind of shutdown to perform.
67#[expect(missing_docs)] // Self-describing names.
68pub enum ShutdownKind {
69    Shutdown,
70    Reboot,
71    Hibernate,
72}
73
74/// Error running command
75#[derive(Error, Debug)]
76pub enum CommandError {
77    /// failed to launch command
78    #[error("failed to launch command")]
79    Launch(#[from] std::io::Error),
80    /// command exited with non-zero status
81    #[error("command exited with non-zero status ({0}): {1}")]
82    Command(std::process::ExitStatus, String),
83}
84
85/// Run a command on the host and return the output
86pub async fn run_host_cmd(mut cmd: Command) -> Result<String, CommandError> {
87    cmd.stderr(Stdio::piped()).stdin(Stdio::null());
88
89    let cmd_debug = format!("{cmd:?}");
90    ::tracing::debug!(cmd = cmd_debug, "executing command");
91
92    let start = Timestamp::now();
93    let output = blocking::unblock(move || cmd.output()).await?;
94    let time_elapsed = Timestamp::now() - start;
95
96    let stdout_str = String::from_utf8_lossy(&output.stdout).to_string();
97    let stderr_str = String::from_utf8_lossy(&output.stderr).to_string();
98    ::tracing::debug!(
99        cmd = cmd_debug,
100        stdout_str,
101        stderr_str,
102        "command exited in {:.3}s with status {}",
103        time_elapsed.total(jiff::Unit::Second).unwrap(),
104        output.status
105    );
106
107    if !output.status.success() {
108        return Err(CommandError::Command(output.status, stderr_str));
109    }
110
111    Ok(stdout_str.trim().to_owned())
112}
113
114/// Owns a process launched by a test, killing it on drop.
115///
116/// [`std::process::Child`] deliberately does *not* kill the process when it is
117/// dropped. Without this guard, any test that fails or panics before reaching
118/// its `TeardownVM`/`Quit` calls leaves an orphaned process behind.
119pub struct TestChild(PolledChild<std::process::Child>);
120
121impl TestChild {
122    /// Create a new test child that will be killed on drop
123    pub fn new(child: PolledChild<std::process::Child>) -> Self {
124        Self(child)
125    }
126}
127
128impl Deref for TestChild {
129    type Target = PolledChild<std::process::Child>;
130
131    fn deref(&self) -> &Self::Target {
132        &self.0
133    }
134}
135
136impl DerefMut for TestChild {
137    fn deref_mut(&mut self) -> &mut Self::Target {
138        &mut self.0
139    }
140}
141
142impl Drop for TestChild {
143    fn drop(&mut self) {
144        let child = self.0.get_mut();
145        // `kill` reports success for an already-reaped child, so ask `try_wait`
146        // whether the process is actually gone rather than relying on that.
147        if let Ok(Some(_)) = child.try_wait() {
148            return;
149        }
150        ::tracing::warn!(
151            "killing test child process which was still running at the end of the test"
152        );
153        if let Err(err) = child.kill() {
154            ::tracing::warn!(
155                error = &err as &dyn std::error::Error,
156                "failed to kill test child process"
157            );
158            return;
159        }
160        // Reap the process so it doesn't linger as a zombie. It was just
161        // killed, so this returns promptly.
162        let _ = child.wait();
163    }
164}