1use petri_artifacts_common::capabilities;
7use std::collections::BTreeSet;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ExecutionEnvironment {
12 Baremetal,
14 Nested,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Vendor {
21 Amd,
23 Intel,
25 Arm,
27}
28
29#[derive(Clone, Copy, Debug, PartialEq)]
31pub enum IsolationType {
32 Vbs,
34 Snp,
36 Tdx,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum VmmType {
43 OpenVmm,
45 HyperV,
47}
48
49#[derive(Debug, Clone)]
51pub struct VmHostInfo {
52 pub vbs_supported: bool,
54 pub snp_status: bool,
56 pub tdx_status: bool,
58}
59
60#[derive(Debug, Clone)]
62pub struct HostContext {
63 pub vm_host_info: Option<VmHostInfo>,
65 pub vendor: Vendor,
67 pub execution_environment: ExecutionEnvironment,
69 pub vpci_supported: bool,
71}
72
73impl HostContext {
74 pub async fn new() -> Self {
76 let is_nested = {
77 #[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 #[cfg(not(target_arch = "x86_64"))]
94 {
95 false
96 }
97 };
98
99 let vendor = {
100 #[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 #[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 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
164pub enum TestRequirement {
166 ExecutionEnvironment(ExecutionEnvironment),
168 Vendor(Vendor),
170 Isolation(IsolationType),
172 RequiresCapability(&'static str),
183 And(Box<TestRequirement>, Box<TestRequirement>),
185 Or(Box<TestRequirement>, Box<TestRequirement>),
187 Not(Box<TestRequirement>),
189 Any,
191}
192
193impl TestRequirement {
194 pub fn and(self, other: TestRequirement) -> TestRequirement {
196 TestRequirement::And(Box::new(self), Box::new(other))
197 }
198
199 pub fn or(self, other: TestRequirement) -> TestRequirement {
201 TestRequirement::Or(Box::new(self), Box::new(other))
202 }
203
204 #[expect(clippy::should_implement_trait)]
206 pub fn not(self) -> TestRequirement {
207 TestRequirement::Not(Box::new(self))
208 }
209
210 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
249pub fn known_capability(name: &str) -> Option<&'static str> {
251 capabilities::known(name)
252}
253
254pub 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#[derive(Debug, Clone)]
288pub struct TestEvaluationResult {
289 pub test_name: String,
291 pub can_run: bool,
293}
294
295impl TestEvaluationResult {
296 pub fn new(test_name: &str) -> Self {
298 Self {
299 test_name: test_name.to_string(),
300 can_run: true,
301 }
302 }
303}
304
305pub struct TestCaseRequirements {
307 requirements: TestRequirement,
308}
309
310impl TestCaseRequirements {
311 pub fn new(requirements: TestRequirement) -> Self {
313 Self { requirements }
314 }
315}
316
317pub 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}