1#![forbid(unsafe_code)]
10
11mod cases;
12pub mod numerics;
13
14use std::fmt;
15use std::vec::Vec;
16use virtio_accel_core::{
17 Accelerator, AccessMode, ArtifactFormat, BackendError, Capabilities, MemoryDomain,
18 TargetIdentity,
19};
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct ProgramFixture {
24 format: ArtifactFormat,
25 target: TargetIdentity,
26 payload: Vec<u8>,
27 resident_bytes: u64,
28}
29
30impl ProgramFixture {
31 pub fn new(
33 format: ArtifactFormat,
34 target: TargetIdentity,
35 payload: impl Into<Vec<u8>>,
36 resident_bytes: u64,
37 ) -> Result<Self, TargetDescriptionError> {
38 let payload = payload.into();
39 if payload.is_empty() {
40 return Err(TargetDescriptionError::EmptyArtifact);
41 }
42 if resident_bytes == 0 {
43 return Err(TargetDescriptionError::ZeroResidentBytes);
44 }
45 Ok(Self {
46 format,
47 target,
48 payload,
49 resident_bytes,
50 })
51 }
52
53 pub const fn format(&self) -> ArtifactFormat {
54 self.format
55 }
56
57 pub const fn target(&self) -> TargetIdentity {
58 self.target
59 }
60
61 pub fn payload(&self) -> &[u8] {
62 &self.payload
63 }
64
65 pub const fn resident_bytes(&self) -> u64 {
66 self.resident_bytes
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct BindingFixture {
73 slot: u32,
74 access: AccessMode,
75 domain: MemoryDomain,
76 alignment: u64,
77 initial: Vec<u8>,
78 expected: Vec<u8>,
79}
80
81impl BindingFixture {
82 pub fn new(
84 slot: u32,
85 access: AccessMode,
86 domain: MemoryDomain,
87 alignment: u64,
88 initial: impl Into<Vec<u8>>,
89 expected: impl Into<Vec<u8>>,
90 ) -> Result<Self, TargetDescriptionError> {
91 let initial = initial.into();
92 let expected = expected.into();
93 if initial.is_empty() {
94 return Err(TargetDescriptionError::EmptyBinding);
95 }
96 if initial.len() != expected.len() {
97 return Err(TargetDescriptionError::OutputLengthMismatch);
98 }
99 if initial == expected {
100 return Err(TargetDescriptionError::UnobservableOutput);
101 }
102 if access == AccessMode::Read {
103 return Err(TargetDescriptionError::ReadOnlyBinding);
104 }
105 if alignment == 0 || !alignment.is_power_of_two() {
106 return Err(TargetDescriptionError::InvalidAlignment);
107 }
108 Ok(Self {
109 slot,
110 access,
111 domain,
112 alignment,
113 initial,
114 expected,
115 })
116 }
117
118 pub fn read_only(
124 slot: u32,
125 domain: MemoryDomain,
126 alignment: u64,
127 initial: impl Into<Vec<u8>>,
128 ) -> Result<Self, TargetDescriptionError> {
129 let initial = initial.into();
130 if initial.is_empty() {
131 return Err(TargetDescriptionError::EmptyBinding);
132 }
133 if alignment == 0 || !alignment.is_power_of_two() {
134 return Err(TargetDescriptionError::InvalidAlignment);
135 }
136 let expected = initial.clone();
137 Ok(Self {
138 slot,
139 access: AccessMode::Read,
140 domain,
141 alignment,
142 initial,
143 expected,
144 })
145 }
146
147 pub const fn is_writable(&self) -> bool {
149 !matches!(self.access, AccessMode::Read)
150 }
151
152 pub const fn slot(&self) -> u32 {
153 self.slot
154 }
155
156 pub const fn access(&self) -> AccessMode {
157 self.access
158 }
159
160 pub const fn domain(&self) -> MemoryDomain {
161 self.domain
162 }
163
164 pub const fn alignment(&self) -> u64 {
165 self.alignment
166 }
167
168 pub fn initial(&self) -> &[u8] {
169 &self.initial
170 }
171
172 pub fn expected(&self) -> &[u8] {
173 &self.expected
174 }
175
176 pub fn bytes(&self) -> u64 {
177 self.initial.len() as u64
178 }
179}
180
181#[derive(Clone, Debug, PartialEq, Eq)]
183pub struct TargetDescription {
184 program: ProgramFixture,
185 bindings: Vec<BindingFixture>,
186 primary: usize,
187}
188
189impl TargetDescription {
190 pub fn new(program: ProgramFixture, binding: BindingFixture) -> Self {
198 assert!(
199 binding.is_writable(),
200 "a single-binding target must be writable; use with_bindings for read-only inputs"
201 );
202 Self {
203 program,
204 bindings: vec![binding],
205 primary: 0,
206 }
207 }
208
209 pub fn with_bindings(
216 program: ProgramFixture,
217 bindings: Vec<BindingFixture>,
218 ) -> Result<Self, TargetDescriptionError> {
219 if bindings.is_empty() {
220 return Err(TargetDescriptionError::NoBindings);
221 }
222 for (index, binding) in bindings.iter().enumerate() {
223 if bindings[..index]
224 .iter()
225 .any(|prior| prior.slot == binding.slot)
226 {
227 return Err(TargetDescriptionError::DuplicateBindingSlot);
228 }
229 }
230 let Some(primary) = bindings.iter().position(BindingFixture::is_writable) else {
231 return Err(TargetDescriptionError::NoObservableBinding);
232 };
233 Ok(Self {
234 program,
235 bindings,
236 primary,
237 })
238 }
239
240 pub const fn program(&self) -> &ProgramFixture {
241 &self.program
242 }
243
244 pub fn binding(&self) -> &BindingFixture {
246 &self.bindings[self.primary]
247 }
248
249 pub fn bindings(&self) -> &[BindingFixture] {
251 &self.bindings
252 }
253
254 pub const fn primary_index(&self) -> usize {
256 self.primary
257 }
258}
259
260#[derive(Clone, Copy, Debug, PartialEq, Eq)]
262pub enum TargetDescriptionError {
263 EmptyArtifact,
264 ZeroResidentBytes,
265 EmptyBinding,
266 OutputLengthMismatch,
267 UnobservableOutput,
268 ReadOnlyBinding,
269 InvalidAlignment,
270 NoBindings,
271 DuplicateBindingSlot,
272 NoObservableBinding,
273}
274
275impl fmt::Display for TargetDescriptionError {
276 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
277 write!(formatter, "{self:?}")
278 }
279}
280
281impl std::error::Error for TargetDescriptionError {}
282
283#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
285pub struct ResourceCounts {
286 pub contexts: u64,
287 pub buffers: u64,
288 pub programs: u64,
289 pub queues: u64,
290 pub events: u64,
291}
292
293impl ResourceCounts {
294 pub const fn is_empty(self) -> bool {
295 self.contexts == 0
296 && self.buffers == 0
297 && self.programs == 0
298 && self.queues == 0
299 && self.events == 0
300 }
301}
302
303#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
311pub struct SubmissionPathDiagnostics {
312 pub direct_bindings: u64,
313 pub shared_imported_bindings: u64,
314 pub staged_direct_bindings: u64,
315 pub staged_direct_bytes: u64,
316 pub explicit_transfer_bytes: u64,
317}
318
319impl SubmissionPathDiagnostics {
320 pub const fn has_hidden_direct_staging(self) -> bool {
321 self.staged_direct_bindings != 0 || self.staged_direct_bytes != 0
322 }
323
324 pub const fn saturating_delta(self, before: Self) -> Self {
325 Self {
326 direct_bindings: self.direct_bindings.saturating_sub(before.direct_bindings),
327 shared_imported_bindings: self
328 .shared_imported_bindings
329 .saturating_sub(before.shared_imported_bindings),
330 staged_direct_bindings: self
331 .staged_direct_bindings
332 .saturating_sub(before.staged_direct_bindings),
333 staged_direct_bytes: self
334 .staged_direct_bytes
335 .saturating_sub(before.staged_direct_bytes),
336 explicit_transfer_bytes: self
337 .explicit_transfer_bytes
338 .saturating_sub(before.explicit_transfer_bytes),
339 }
340 }
341}
342
343pub trait ConformanceHooks<A: Accelerator> {
345 fn complete_event(&self, backend: &A, event: &A::Event) -> Result<(), BackendError>;
347
348 fn resource_counts(&self, _backend: &A) -> Option<ResourceCounts> {
350 None
351 }
352
353 fn submission_path_diagnostics(&self, _backend: &A) -> Option<SubmissionPathDiagnostics> {
355 None
356 }
357}
358
359#[derive(Clone, Copy, Debug, PartialEq, Eq)]
361pub enum CaseRequirement {
362 Mandatory,
363 Capability(Capabilities),
364 AccountingHook,
365 DiagnosticsHook,
366}
367
368#[derive(Clone, Copy, Debug, PartialEq, Eq)]
370pub enum SkipReason {
371 CapabilityNotAdvertised(Capabilities),
372 AccountingUnavailable,
373 DiagnosticsUnavailable,
374}
375
376#[derive(Clone, Debug, PartialEq, Eq)]
378pub enum CaseStatus {
379 Passed,
380 Skipped(SkipReason),
381 Failed(String),
382}
383
384#[derive(Clone, Debug, PartialEq, Eq)]
386pub struct CaseResult {
387 pub id: &'static str,
388 pub name: &'static str,
389 pub requirement: CaseRequirement,
390 pub status: CaseStatus,
391}
392
393#[derive(Clone, Debug, PartialEq, Eq)]
395pub struct ConformanceReport {
396 cases: Vec<CaseResult>,
397}
398
399impl ConformanceReport {
400 pub fn cases(&self) -> &[CaseResult] {
401 &self.cases
402 }
403
404 pub fn case(&self, id: &str) -> Option<&CaseResult> {
405 self.cases.iter().find(|case| case.id == id)
406 }
407
408 pub fn failures(&self) -> impl Iterator<Item = &CaseResult> {
409 self.cases
410 .iter()
411 .filter(|case| matches!(case.status, CaseStatus::Failed(_)))
412 }
413
414 pub fn passed(&self) -> bool {
415 self.failures().next().is_none()
416 }
417
418 #[track_caller]
420 pub fn assert_conformant(&self) {
421 assert!(self.passed(), "{self}");
422 }
423}
424
425impl fmt::Display for ConformanceReport {
426 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
427 let mut failures = self.failures().peekable();
428 if failures.peek().is_none() {
429 return formatter.write_str("backend conformance passed");
430 }
431 formatter.write_str("backend conformance failed:")?;
432 for case in failures {
433 let CaseStatus::Failed(message) = &case.status else {
434 continue;
435 };
436 write!(formatter, "\n- {}: {}", case.id, message)?;
437 }
438 Ok(())
439 }
440}
441
442pub fn run<A, F, H>(factory: F, target: &TargetDescription, hooks: &H) -> ConformanceReport
448where
449 A: Accelerator,
450 F: Fn() -> A,
451 H: ConformanceHooks<A>,
452{
453 ConformanceReport {
454 cases: cases::run_all(&factory, target, hooks),
455 }
456}