Skip to main content

petri/
requirements.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Test requirements framework for runtime test filtering.
5
6use petri_artifacts_common::capabilities;
7use std::collections::BTreeSet;
8
9/// Execution environments where tests can run.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ExecutionEnvironment {
12    /// Bare metal execution (not nested virtualization).
13    Baremetal,
14    /// Nested virtualization environment.
15    Nested,
16}
17
18/// CPU vendors.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Vendor {
21    /// AMD processors.
22    Amd,
23    /// Intel processors.
24    Intel,
25    /// ARM processors.
26    Arm,
27}
28
29/// Types of isolation supported.
30#[derive(Clone, Copy, Debug, PartialEq)]
31pub enum IsolationType {
32    /// Virtualization-based Security (VBS)
33    Vbs,
34    /// Secure Nested Paging (SNP)
35    Snp,
36    /// Trusted Domain Extensions (TDX)
37    Tdx,
38}
39
40/// VMM implementation types.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum VmmType {
43    /// OpenVMM.
44    OpenVmm,
45    /// Microsoft Hyper-V.
46    HyperV,
47}
48
49/// Information about the VM host, retrieved via PowerShell on Windows.
50#[derive(Debug, Clone)]
51pub struct VmHostInfo {
52    /// VBS support status
53    pub vbs_supported: bool,
54    /// SNP support status
55    pub snp_status: bool,
56    /// TDX support status
57    pub tdx_status: bool,
58}
59
60/// Platform-specific host context extending the base HostContext
61#[derive(Debug, Clone)]
62pub struct HostContext {
63    /// VmHost information retrieved via PowerShell
64    pub vm_host_info: Option<VmHostInfo>,
65    /// CPU vendor
66    pub vendor: Vendor,
67    /// Execution environment
68    pub execution_environment: ExecutionEnvironment,
69    /// Whether the host hypervisor supports software VPCI device emulation
70    pub vpci_supported: bool,
71}
72
73impl HostContext {
74    /// Create a new host context by querying host information
75    pub async fn new() -> Self {
76        let is_nested = {
77            // xtask-fmt allow-target-arch cpu-intrinsic
78            #[cfg(target_arch = "x86_64")]
79            {
80                let result = safe_intrinsics::cpuid(
81                    hvdef::HV_CPUID_FUNCTION_MS_HV_ENLIGHTENMENT_INFORMATION,
82                    0,
83                );
84                hvdef::HvEnlightenmentInformation::from(
85                    result.eax as u128
86                        | (result.ebx as u128) << 32
87                        | (result.ecx as u128) << 64
88                        | (result.edx as u128) << 96,
89                )
90                .nested()
91            }
92            // xtask-fmt allow-target-arch cpu-intrinsic
93            #[cfg(not(target_arch = "x86_64"))]
94            {
95                false
96            }
97        };
98
99        let vendor = {
100            // xtask-fmt allow-target-arch cpu-intrinsic
101            #[cfg(target_arch = "x86_64")]
102            {
103                let result = safe_intrinsics::cpuid(
104                    x86defs::cpuid::CpuidFunction::VendorAndMaxFunction.0,
105                    0,
106                );
107                if x86defs::cpuid::Vendor::from_ebx_ecx_edx(result.ebx, result.ecx, result.edx)
108                    .is_amd_compatible()
109                {
110                    Vendor::Amd
111                } else {
112                    assert!(
113                        x86defs::cpuid::Vendor::from_ebx_ecx_edx(
114                            result.ebx, result.ecx, result.edx
115                        )
116                        .is_intel_compatible()
117                    );
118                    Vendor::Intel
119                }
120            }
121            // xtask-fmt allow-target-arch cpu-intrinsic
122            #[cfg(not(target_arch = "x86_64"))]
123            {
124                Vendor::Arm
125            }
126        };
127
128        let vm_host_info = {
129            #[cfg(windows)]
130            {
131                crate::vm::hyperv::powershell::run_get_vm_host()
132                    .await
133                    .ok()
134                    .map(|info| VmHostInfo {
135                        vbs_supported: info.guest_isolation_types.contains(
136                            &crate::vm::hyperv::powershell::HyperVGuestStateIsolationType::Vbs,
137                        ),
138                        snp_status: info.snp_status,
139                        tdx_status: info.tdx_status,
140                    })
141            }
142            #[cfg(not(windows))]
143            {
144                None
145            }
146        };
147
148        // VPCI support: only Windows (virt_whp and Hyper-V) supports it for now.
149        let vpci_supported = cfg!(windows);
150
151        Self {
152            vm_host_info,
153            vendor,
154            execution_environment: if is_nested {
155                ExecutionEnvironment::Nested
156            } else {
157                ExecutionEnvironment::Baremetal
158            },
159            vpci_supported,
160        }
161    }
162}
163
164/// A single requirement for a test to run.
165pub enum TestRequirement {
166    /// Execution environment requirement.
167    ExecutionEnvironment(ExecutionEnvironment),
168    /// Vendor requirement.
169    Vendor(Vendor),
170    /// Isolation requirement.
171    Isolation(IsolationType),
172    /// Requires a named capability advertised by the execution environment or
173    /// detected by petri.
174    ///
175    /// Capabilities are how a test says "I need a specific resource to be
176    /// provisioned for me" without naming who provides it or how. The
177    /// execution environment can advertise capabilities via the
178    /// comma-separated `PETRI_CAPABILITIES` environment variable, and petri can
179    /// add capabilities that it detects itself. A test requiring a capability
180    /// that is not available is skipped, so such tests automatically
181    /// self-exclude on any host that cannot satisfy them.
182    RequiresCapability(&'static str),
183    /// Logical AND of two requirements.
184    And(Box<TestRequirement>, Box<TestRequirement>),
185    /// Logical OR of two requirements.
186    Or(Box<TestRequirement>, Box<TestRequirement>),
187    /// Logical NOT of a requirement.
188    Not(Box<TestRequirement>),
189    /// Requirement satisfied by any host context.
190    Any,
191}
192
193impl TestRequirement {
194    /// Combine this requirement with another requirement using logical AND.
195    pub fn and(self, other: TestRequirement) -> TestRequirement {
196        TestRequirement::And(Box::new(self), Box::new(other))
197    }
198
199    /// Combine this requirement with another requirement using logical OR.
200    pub fn or(self, other: TestRequirement) -> TestRequirement {
201        TestRequirement::Or(Box::new(self), Box::new(other))
202    }
203
204    /// Negate this requirement.
205    #[expect(clippy::should_implement_trait)]
206    pub fn not(self) -> TestRequirement {
207        TestRequirement::Not(Box::new(self))
208    }
209
210    /// Evaluate if this requirement is satisfied with the given host context
211    pub fn is_satisfied(&self, context: &HostContext) -> bool {
212        self.is_satisfied_with_capabilities(context, &available_capabilities(context))
213    }
214
215    fn is_satisfied_with_capabilities(
216        &self,
217        context: &HostContext,
218        capabilities: &BTreeSet<&'static str>,
219    ) -> bool {
220        match self {
221            TestRequirement::ExecutionEnvironment(env) => context.execution_environment == *env,
222            TestRequirement::Vendor(vendor) => context.vendor == *vendor,
223            TestRequirement::Isolation(isolation_type) => {
224                if let Some(vm_host_info) = &context.vm_host_info {
225                    match isolation_type {
226                        IsolationType::Vbs => vm_host_info.vbs_supported,
227                        IsolationType::Snp => vm_host_info.snp_status,
228                        IsolationType::Tdx => vm_host_info.tdx_status,
229                    }
230                } else {
231                    false
232                }
233            }
234            TestRequirement::RequiresCapability(name) => capabilities.contains(name),
235            TestRequirement::And(req1, req2) => {
236                req1.is_satisfied_with_capabilities(context, capabilities)
237                    && req2.is_satisfied_with_capabilities(context, capabilities)
238            }
239            TestRequirement::Or(req1, req2) => {
240                req1.is_satisfied_with_capabilities(context, capabilities)
241                    || req2.is_satisfied_with_capabilities(context, capabilities)
242            }
243            TestRequirement::Not(req) => !req.is_satisfied_with_capabilities(context, capabilities),
244            TestRequirement::Any => true,
245        }
246    }
247}
248
249/// Returns the canonical runtime name for a known capability.
250pub fn known_capability(name: &str) -> Option<&'static str> {
251    capabilities::known(name)
252}
253
254/// Returns whether `name` is a known capability.
255pub fn is_known_capability(name: &str) -> bool {
256    known_capability(name).is_some()
257}
258
259fn available_capabilities(context: &HostContext) -> BTreeSet<&'static str> {
260    let mut capabilities = BTreeSet::new();
261
262    if context.vpci_supported {
263        capabilities.insert(capabilities::VPCI);
264    }
265
266    match std::env::var("PETRI_CAPABILITIES") {
267        Ok(env_capabilities) => {
268            for capability in env_capabilities.split(',').map(str::trim) {
269                if capability.is_empty() {
270                    continue;
271                }
272                let capability = known_capability(capability)
273                    .unwrap_or_else(|| panic!("unknown PETRI_CAPABILITIES entry: {capability}"));
274                capabilities.insert(capability);
275            }
276        }
277        Err(std::env::VarError::NotPresent) => {}
278        Err(std::env::VarError::NotUnicode(_)) => {
279            panic!("PETRI_CAPABILITIES is not valid UTF-8")
280        }
281    }
282
283    capabilities
284}
285
286/// Result of evaluating all requirements for a test
287#[derive(Debug, Clone)]
288pub struct TestEvaluationResult {
289    /// Name of the test being evaluated
290    pub test_name: String,
291    /// Overall result: can the test be run?
292    pub can_run: bool,
293}
294
295impl TestEvaluationResult {
296    /// Create a new result indicating the test can run (no requirements)
297    pub fn new(test_name: &str) -> Self {
298        Self {
299            test_name: test_name.to_string(),
300            can_run: true,
301        }
302    }
303}
304
305/// Container for test requirements that can be evaluated
306pub struct TestCaseRequirements {
307    requirements: TestRequirement,
308}
309
310impl TestCaseRequirements {
311    /// Create a new TestCaseRequirements from a TestRequirement
312    pub fn new(requirements: TestRequirement) -> Self {
313        Self { requirements }
314    }
315}
316
317/// Evaluates if a test case can be run in the current execution environment with context.
318pub fn can_run_test_with_context(
319    config: Option<&TestCaseRequirements>,
320    context: &HostContext,
321) -> bool {
322    if let Some(config) = config {
323        config.requirements.is_satisfied(context)
324    } else {
325        true
326    }
327}