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
6/// Execution environments where tests can run.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ExecutionEnvironment {
9    /// Bare metal execution (not nested virtualization).
10    Baremetal,
11    /// Nested virtualization environment.
12    Nested,
13}
14
15/// CPU vendors.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum Vendor {
18    /// AMD processors.
19    Amd,
20    /// Intel processors.
21    Intel,
22    /// ARM processors.
23    Arm,
24}
25
26/// Types of isolation supported.
27#[derive(Clone, Copy, Debug, PartialEq)]
28pub enum IsolationType {
29    /// Virtualization-based Security (VBS)
30    Vbs,
31    /// Secure Nested Paging (SNP)
32    Snp,
33    /// Trusted Domain Extensions (TDX)
34    Tdx,
35}
36
37/// VMM implementation types.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum VmmType {
40    /// OpenVMM.
41    OpenVmm,
42    /// Microsoft Hyper-V.
43    HyperV,
44}
45
46/// Information about the VM host, retrieved via PowerShell on Windows.
47#[derive(Debug, Clone)]
48pub struct VmHostInfo {
49    /// VBS support status
50    pub vbs_supported: bool,
51    /// SNP support status
52    pub snp_status: bool,
53    /// TDX support status
54    pub tdx_status: bool,
55}
56
57/// Platform-specific host context extending the base HostContext
58#[derive(Debug, Clone)]
59pub struct HostContext {
60    /// VmHost information retrieved via PowerShell
61    pub vm_host_info: Option<VmHostInfo>,
62    /// CPU vendor
63    pub vendor: Vendor,
64    /// Execution environment
65    pub execution_environment: ExecutionEnvironment,
66    /// Whether the host hypervisor supports software VPCI device emulation
67    pub vpci_supported: bool,
68}
69
70impl HostContext {
71    /// Create a new host context by querying host information
72    pub async fn new() -> Self {
73        let is_nested = {
74            // xtask-fmt allow-target-arch cpu-intrinsic
75            #[cfg(target_arch = "x86_64")]
76            {
77                let result = safe_intrinsics::cpuid(
78                    hvdef::HV_CPUID_FUNCTION_MS_HV_ENLIGHTENMENT_INFORMATION,
79                    0,
80                );
81                hvdef::HvEnlightenmentInformation::from(
82                    result.eax as u128
83                        | (result.ebx as u128) << 32
84                        | (result.ecx as u128) << 64
85                        | (result.edx as u128) << 96,
86                )
87                .nested()
88            }
89            // xtask-fmt allow-target-arch cpu-intrinsic
90            #[cfg(not(target_arch = "x86_64"))]
91            {
92                false
93            }
94        };
95
96        let vendor = {
97            // xtask-fmt allow-target-arch cpu-intrinsic
98            #[cfg(target_arch = "x86_64")]
99            {
100                let result = safe_intrinsics::cpuid(
101                    x86defs::cpuid::CpuidFunction::VendorAndMaxFunction.0,
102                    0,
103                );
104                if x86defs::cpuid::Vendor::from_ebx_ecx_edx(result.ebx, result.ecx, result.edx)
105                    .is_amd_compatible()
106                {
107                    Vendor::Amd
108                } else {
109                    assert!(
110                        x86defs::cpuid::Vendor::from_ebx_ecx_edx(
111                            result.ebx, result.ecx, result.edx
112                        )
113                        .is_intel_compatible()
114                    );
115                    Vendor::Intel
116                }
117            }
118            // xtask-fmt allow-target-arch cpu-intrinsic
119            #[cfg(not(target_arch = "x86_64"))]
120            {
121                Vendor::Arm
122            }
123        };
124
125        let vm_host_info = {
126            #[cfg(windows)]
127            {
128                crate::vm::hyperv::powershell::run_get_vm_host()
129                    .await
130                    .ok()
131                    .map(|info| VmHostInfo {
132                        vbs_supported: info.guest_isolation_types.contains(
133                            &crate::vm::hyperv::powershell::HyperVGuestStateIsolationType::Vbs,
134                        ),
135                        snp_status: info.snp_status,
136                        tdx_status: info.tdx_status,
137                    })
138            }
139            #[cfg(not(windows))]
140            {
141                None
142            }
143        };
144
145        // VPCI support: only Windows (virt_whp and Hyper-V) supports it for now.
146        let vpci_supported = cfg!(windows);
147
148        Self {
149            vm_host_info,
150            vendor,
151            execution_environment: if is_nested {
152                ExecutionEnvironment::Nested
153            } else {
154                ExecutionEnvironment::Baremetal
155            },
156            vpci_supported,
157        }
158    }
159}
160
161/// A single requirement for a test to run.
162pub enum TestRequirement {
163    /// Execution environment requirement.
164    ExecutionEnvironment(ExecutionEnvironment),
165    /// Vendor requirement.
166    Vendor(Vendor),
167    /// Isolation requirement.
168    Isolation(IsolationType),
169    /// Requires a hypervisor backend that supports VPCI (virtual PCI)
170    /// device emulation. On Linux this means /dev/mshv (not KVM).
171    VpciSupport,
172    /// Logical AND of two requirements.
173    And(Box<TestRequirement>, Box<TestRequirement>),
174    /// Logical OR of two requirements.
175    Or(Box<TestRequirement>, Box<TestRequirement>),
176    /// Logical NOT of a requirement.
177    Not(Box<TestRequirement>),
178    /// Requirement satisfied by any host context.
179    Any,
180}
181
182impl TestRequirement {
183    /// Combine this requirement with another requirement using logical AND.
184    pub fn and(self, other: TestRequirement) -> TestRequirement {
185        TestRequirement::And(Box::new(self), Box::new(other))
186    }
187
188    /// Combine this requirement with another requirement using logical OR.
189    pub fn or(self, other: TestRequirement) -> TestRequirement {
190        TestRequirement::Or(Box::new(self), Box::new(other))
191    }
192
193    /// Negate this requirement.
194    #[expect(clippy::should_implement_trait)]
195    pub fn not(self) -> TestRequirement {
196        TestRequirement::Not(Box::new(self))
197    }
198
199    /// Evaluate if this requirement is satisfied with the given host context
200    pub fn is_satisfied(&self, context: &HostContext) -> bool {
201        match self {
202            TestRequirement::ExecutionEnvironment(env) => context.execution_environment == *env,
203            TestRequirement::Vendor(vendor) => context.vendor == *vendor,
204            TestRequirement::Isolation(isolation_type) => {
205                if let Some(vm_host_info) = &context.vm_host_info {
206                    match isolation_type {
207                        IsolationType::Vbs => vm_host_info.vbs_supported,
208                        IsolationType::Snp => vm_host_info.snp_status,
209                        IsolationType::Tdx => vm_host_info.tdx_status,
210                    }
211                } else {
212                    false
213                }
214            }
215            TestRequirement::VpciSupport => context.vpci_supported,
216            TestRequirement::And(req1, req2) => {
217                req1.is_satisfied(context) && req2.is_satisfied(context)
218            }
219            TestRequirement::Or(req1, req2) => {
220                req1.is_satisfied(context) || req2.is_satisfied(context)
221            }
222            TestRequirement::Not(req) => !req.is_satisfied(context),
223            TestRequirement::Any => true,
224        }
225    }
226}
227
228/// Result of evaluating all requirements for a test
229#[derive(Debug, Clone)]
230pub struct TestEvaluationResult {
231    /// Name of the test being evaluated
232    pub test_name: String,
233    /// Overall result: can the test be run?
234    pub can_run: bool,
235}
236
237impl TestEvaluationResult {
238    /// Create a new result indicating the test can run (no requirements)
239    pub fn new(test_name: &str) -> Self {
240        Self {
241            test_name: test_name.to_string(),
242            can_run: true,
243        }
244    }
245}
246
247/// Container for test requirements that can be evaluated
248pub struct TestCaseRequirements {
249    requirements: TestRequirement,
250}
251
252impl TestCaseRequirements {
253    /// Create a new TestCaseRequirements from a TestRequirement
254    pub fn new(requirements: TestRequirement) -> Self {
255        Self { requirements }
256    }
257}
258
259/// Evaluates if a test case can be run in the current execution environment with context.
260pub fn can_run_test_with_context(
261    config: Option<&TestCaseRequirements>,
262    context: &HostContext,
263) -> bool {
264    if let Some(config) = config {
265        config.requirements.is_satisfied(context)
266    } else {
267        true
268    }
269}