1use crate::{
4 artifact::ArtifactError, ArtifactFormat, ArtifactInspection, GgufCompanionRole,
5 InputModalities, ModelResourceProfile, Observed, PreparationAdmission,
6 PreparationAdmissionError, SessionCapabilities,
7};
8use serde::{Deserialize, Serialize};
9use std::{
10 collections::{BTreeMap, BTreeSet},
11 path::{Path, PathBuf},
12};
13
14#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum InspectionReadiness {
18 Ready,
20 Missing,
22 Unsupported,
24 Invalid,
26 RequestDependent,
28 Unverified,
30 NotApplicable,
32}
33
34#[derive(Debug, Clone, Copy, Eq, PartialEq)]
36pub struct RealizedInspectionOutcomes {
37 pub artifact_authority: bool,
39 pub binding: bool,
41 pub construction: bool,
43 pub communication: bool,
45 pub execution: bool,
47 pub session: SessionCapabilities,
49}
50
51pub fn finalize_realized_model_inspection(
57 admitted: &ModelInspectionReport,
58 outcomes: RealizedInspectionOutcomes,
59) -> ModelInspectionReport {
60 let mut report = admitted.clone();
61 let admission = admitted.preparation_admission;
62 let session_matches =
63 admission.is_some_and(|value| value.session_capabilities() == outcomes.session);
64 report.container = if outcomes.artifact_authority {
65 InspectionReadiness::Ready
66 } else {
67 InspectionReadiness::Invalid
68 };
69 report.architecture_support = if outcomes.construction {
70 InspectionReadiness::Ready
71 } else {
72 InspectionReadiness::Invalid
73 };
74 report.structural_binding = if outcomes.binding {
75 InspectionReadiness::Ready
76 } else {
77 InspectionReadiness::Invalid
78 };
79 report.model_loadability = if outcomes.artifact_authority
80 && outcomes.binding
81 && outcomes.construction
82 && outcomes.communication
83 && outcomes.execution
84 {
85 InspectionReadiness::Ready
86 } else {
87 InspectionReadiness::Invalid
88 };
89 report.requested_load =
90 if report.model_loadability == InspectionReadiness::Ready && session_matches {
91 InspectionReadiness::Ready
92 } else {
93 InspectionReadiness::Unsupported
94 };
95 report
96}
97
98pub fn assemble_portable_model_inspection<P>(
106 inspection: &ArtifactInspection<P>,
107 admission: PreparationAdmission,
108 modalities: InputModalities,
109 embedded_draft_layers: Option<usize>,
110 processor: Option<(bool, MediaFeatureAvailability)>,
111) -> ModelInspectionReport {
112 let mut report = ModelInspectionReport::unverified(inspection.path(), inspection.format());
113 report.record_artifact_inspection(inspection);
114 report.record_architecture_capabilities(modalities, embedded_draft_layers);
115 if let Some((has_processor, availability)) = processor {
116 record_processor_inspection(&mut report, has_processor, availability);
117 }
118 report.record_preparation_admission(admission);
119 report
120}
121
122pub fn reject_portable_artifact_inspection(
124 report: &mut ModelInspectionReport,
125 path: &Path,
126 error: &ArtifactError,
127) {
128 let detail = error.to_string();
129 let missing_media_projector = matches!(
130 error,
131 ArtifactError::MissingRequiredGgufCompanion {
132 role: GgufCompanionRole::MediaProjector,
133 ..
134 }
135 );
136 let (code, container, architecture, structural, type_code) = match error {
137 ArtifactError::UnsupportedGgufArchitecture(name) => {
138 report.architecture = Some(name.clone());
139 (
140 InspectionIssueCode::UnsupportedArchitecture,
141 InspectionReadiness::Ready,
142 InspectionReadiness::Unsupported,
143 InspectionReadiness::Unsupported,
144 None,
145 )
146 }
147 ArtifactError::MissingGgufArchitecture => (
148 InspectionIssueCode::InvalidConfiguration,
149 InspectionReadiness::Ready,
150 InspectionReadiness::Invalid,
151 InspectionReadiness::Invalid,
152 None,
153 ),
154 ArtifactError::MissingRequiredGgufCompanion {
155 role: GgufCompanionRole::MediaProjector,
156 ..
157 } => (
158 InspectionIssueCode::MissingMediaProjector,
159 InspectionReadiness::Ready,
160 InspectionReadiness::Ready,
161 InspectionReadiness::Unverified,
162 None,
163 ),
164 ArtifactError::InvalidArtifact(_)
165 | ArtifactError::InvalidArchitecturePlan(_)
166 | ArtifactError::DuplicateTensor(_)
167 | ArtifactError::Catalog(_) => (
168 InspectionIssueCode::InvalidConfiguration,
169 InspectionReadiness::Ready,
170 InspectionReadiness::Unverified,
171 InspectionReadiness::Invalid,
172 None,
173 ),
174 ArtifactError::Gguf(error) => {
175 let type_code = error.unsupported_tensor_type_code();
176 (
177 if type_code.is_some() {
178 InspectionIssueCode::UnsupportedTensorEncoding
179 } else {
180 InspectionIssueCode::InvalidContainer
181 },
182 InspectionReadiness::Invalid,
183 InspectionReadiness::Unverified,
184 InspectionReadiness::Unverified,
185 type_code,
186 )
187 }
188 _ => (
189 InspectionIssueCode::InvalidContainer,
190 InspectionReadiness::Invalid,
191 InspectionReadiness::Unverified,
192 InspectionReadiness::Unverified,
193 None,
194 ),
195 };
196 report.container = container;
197 report.architecture_support = architecture;
198 report.structural_binding = structural;
199 report.model_loadability = if missing_media_projector {
200 InspectionReadiness::Missing
201 } else if architecture == InspectionReadiness::Unsupported {
202 InspectionReadiness::Unsupported
203 } else {
204 InspectionReadiness::Invalid
205 };
206 report.requested_load = report.model_loadability;
207 if missing_media_projector {
208 report.multimodal = InspectionReadiness::Missing;
209 report.requirements.push(InspectionRequirement {
210 code: InspectionIssueCode::MissingMediaProjector,
211 readiness: InspectionReadiness::Missing,
212 detail: detail.clone(),
213 path: None,
214 });
215 }
216 report.issues.push(InspectionIssue {
217 code,
218 severity: InspectionSeverity::Error,
219 detail,
220 path: Some(path.to_path_buf()),
221 metadata_key: None,
222 tensor_name: None,
223 tensor_type_code: type_code,
224 });
225}
226
227#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum InspectionSeverity {
231 Error,
233 Warning,
235 Info,
237}
238
239#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
241#[serde(rename_all = "snake_case")]
242#[non_exhaustive]
243pub enum InspectionIssueCode {
244 InvalidContainer,
246 InvalidConfiguration,
248 UnsupportedArchitecture,
250 MissingCheckpointShard,
252 UnsupportedTensorEncoding,
254 MissingRequiredTensor,
256 ConflictingTensorLayout,
258 TensorShapeMismatch,
260 QuantizationCompanionMismatch,
262 InvalidLayerOrExpertCount,
264 MissingTokenizer,
266 MissingChatTemplate,
268 MissingMediaProjector,
270 MissingProcessor,
272 UnsupportedQuantizationRequest,
274 UnsupportedResidencyPolicy,
276 UnsupportedParallelTopology,
278 UnsupportedSemanticProtocol,
280 UnsupportedToolProtocol,
282 MissingEosMetadata,
284 ValidationUnavailableUntilLoad,
286 RequestSpecificValidation,
288 Io,
290}
291
292#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
294pub struct InspectionIssue {
295 pub code: InspectionIssueCode,
297 pub severity: InspectionSeverity,
299 pub detail: String,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub path: Option<PathBuf>,
304 #[serde(skip_serializing_if = "Option::is_none")]
306 pub metadata_key: Option<String>,
307 #[serde(skip_serializing_if = "Option::is_none")]
309 pub tensor_name: Option<String>,
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub tensor_type_code: Option<u32>,
313}
314
315#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
317pub struct ArtifactTensorEncoding {
318 pub name: String,
320 #[serde(skip_serializing_if = "Option::is_none")]
322 pub ggml_type_code: Option<u32>,
323}
324
325#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
327#[serde(rename_all = "snake_case")]
328pub enum ArtifactModality {
329 Text,
331 Image,
333 Video,
335 Audio,
337}
338
339#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
341pub struct InspectionRequirement {
342 pub code: InspectionIssueCode,
344 pub readiness: InspectionReadiness,
346 pub detail: String,
348 #[serde(skip_serializing_if = "Option::is_none")]
350 pub path: Option<PathBuf>,
351}
352
353#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
355pub struct ModelInspectionReport {
356 pub path: PathBuf,
358 pub artifact_format: ArtifactFormat,
360 #[serde(skip_serializing_if = "Option::is_none")]
362 pub model_family: Option<String>,
363 #[serde(skip_serializing_if = "Option::is_none")]
365 pub architecture: Option<String>,
366 #[serde(skip_serializing_if = "Option::is_none")]
368 pub gguf_versions: Option<Vec<u32>>,
369 #[serde(skip_serializing_if = "Option::is_none")]
371 pub checkpoint_shards: Option<usize>,
372 #[serde(skip_serializing_if = "Option::is_none")]
374 pub tensor_count: Option<usize>,
375 pub resources: ModelResourceProfile,
377 pub tensor_encodings: Vec<ArtifactTensorEncoding>,
379 pub expected_modalities: Vec<ArtifactModality>,
381 pub container: InspectionReadiness,
383 pub architecture_support: InspectionReadiness,
385 pub structural_binding: InspectionReadiness,
387 pub model_loadability: InspectionReadiness,
389 pub requested_load: InspectionReadiness,
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub preparation_admission: Option<PreparationAdmission>,
394 pub text_generation: InspectionReadiness,
396 pub tokenizer: InspectionReadiness,
398 pub chat_template: InspectionReadiness,
400 pub semantic_streaming: InspectionReadiness,
402 pub native_tools: InspectionReadiness,
404 pub multimodal: InspectionReadiness,
406 pub requirements: Vec<InspectionRequirement>,
408 pub issues: Vec<InspectionIssue>,
410}
411
412impl ModelInspectionReport {
413 pub fn unverified(path: &Path, artifact_format: ArtifactFormat) -> Self {
415 Self {
416 path: path.to_path_buf(),
417 artifact_format,
418 model_family: None,
419 architecture: None,
420 gguf_versions: None,
421 checkpoint_shards: None,
422 tensor_count: None,
423 resources: ModelResourceProfile::unmeasured(path.to_path_buf(), artifact_format),
424 tensor_encodings: Vec::new(),
425 expected_modalities: Vec::new(),
426 container: InspectionReadiness::Unverified,
427 architecture_support: InspectionReadiness::Unverified,
428 structural_binding: InspectionReadiness::Unverified,
429 model_loadability: InspectionReadiness::Unverified,
430 requested_load: InspectionReadiness::Unverified,
431 preparation_admission: None,
432 text_generation: InspectionReadiness::Unverified,
433 tokenizer: InspectionReadiness::Unverified,
434 chat_template: InspectionReadiness::Unverified,
435 semantic_streaming: InspectionReadiness::Unverified,
436 native_tools: InspectionReadiness::Unverified,
437 multimodal: InspectionReadiness::Unverified,
438 requirements: Vec::new(),
439 issues: Vec::new(),
440 }
441 }
442
443 pub fn is_loadable(&self) -> bool {
445 self.container == InspectionReadiness::Ready
446 && self.architecture_support == InspectionReadiness::Ready
447 && self.structural_binding == InspectionReadiness::Ready
448 && self.model_loadability == InspectionReadiness::Ready
449 && self.requested_load == InspectionReadiness::Ready
450 && !self
451 .issues
452 .iter()
453 .any(|issue| issue.code == InspectionIssueCode::ValidationUnavailableUntilLoad)
454 }
455
456 pub fn record_artifact_inspection<P>(&mut self, inspection: &ArtifactInspection<P>) {
458 self.path = inspection.path().to_owned();
459 self.artifact_format = inspection.format();
460 self.model_family = Some(inspection.configuration().family().to_owned());
461 self.architecture = Some(inspection.configuration().effective_model_type().to_owned());
462 self.container = InspectionReadiness::Ready;
463 self.architecture_support = InspectionReadiness::Ready;
464 self.structural_binding = InspectionReadiness::Ready;
465 self.model_loadability = InspectionReadiness::Ready;
466 self.tensor_count = Some(inspection.tensors().len());
467
468 let (shards, encodings, stored_bytes, largest_bytes, gguf_versions) =
469 if let Some(validated) = inspection.validated_gguf() {
470 let checkpoint = validated.checkpoint();
471 let encodings = checkpoint
472 .tensors()
473 .map(|tensor| tensor.descriptor().ggml_type)
474 .map(|encoding| (encoding.code(), format!("{encoding:?}")))
475 .collect::<BTreeMap<_, _>>()
476 .into_iter()
477 .map(|(code, name)| ArtifactTensorEncoding {
478 name,
479 ggml_type_code: Some(code),
480 })
481 .collect();
482 let mut total = Some(0_u64);
483 let mut largest = 0_u64;
484 for tensor in checkpoint.tensors() {
485 let bytes = tensor.descriptor().byte_len;
486 total = total.and_then(|value| value.checked_add(bytes));
487 largest = largest.max(bytes);
488 }
489 let versions = checkpoint
490 .shards()
491 .iter()
492 .map(|shard| shard.version())
493 .collect::<BTreeSet<_>>()
494 .into_iter()
495 .collect();
496 (
497 checkpoint.shards().len(),
498 encodings,
499 total,
500 largest,
501 Some(versions),
502 )
503 } else {
504 let mut shards = BTreeSet::new();
505 let mut encodings = BTreeSet::new();
506 let mut total = Some(0_u64);
507 let mut largest = 0_u64;
508 for tensor in inspection.tensors().descriptors() {
509 encodings.insert(safetensors_encoding_name(&tensor.dtype));
510 if let Some(storage) = &tensor.storage {
511 shards.insert(storage.member.clone());
512 total = total.and_then(|value| value.checked_add(storage.length));
513 largest = largest.max(storage.length);
514 } else {
515 total = None;
516 }
517 }
518 (
519 shards.len(),
520 encodings
521 .into_iter()
522 .map(|name| ArtifactTensorEncoding {
523 name,
524 ggml_type_code: None,
525 })
526 .collect(),
527 total,
528 largest,
529 None,
530 )
531 };
532 self.checkpoint_shards = Some(shards);
533 self.tensor_encodings = encodings;
534 self.gguf_versions = gguf_versions;
535 self.resources.model_family = self.model_family.clone();
536 self.resources.architecture = self.architecture.clone();
537 self.resources.tensor_count = self.tensor_count;
538 self.resources.checkpoint_shards = self.checkpoint_shards;
539 match stored_bytes {
540 Some(total) => {
541 self.resources.stored_tensor_bytes =
542 Observed::exact(total, "authoritative portable tensor catalog");
543 self.resources.largest_stored_tensor_bytes =
544 Observed::exact(largest_bytes, "authoritative portable tensor catalog");
545 }
546 None => {
547 self.resources.stored_tensor_bytes =
548 Observed::unavailable("portable payload-byte catalog was incomplete");
549 self.resources.largest_stored_tensor_bytes =
550 Observed::unavailable("portable payload-byte catalog was incomplete");
551 }
552 }
553 }
554
555 pub fn record_preparation_admission(&mut self, admission: PreparationAdmission) {
557 self.preparation_admission = Some(admission);
558 self.requested_load = InspectionReadiness::Ready;
559 }
560
561 pub fn record_architecture_capabilities(
563 &mut self,
564 modalities: InputModalities,
565 embedded_draft_layers: Option<usize>,
566 ) {
567 self.expected_modalities = artifact_modalities(modalities);
568 self.resources.embedded_draft_layers = embedded_draft_layers.map_or_else(
569 || Observed::unsupported("artifact convention does not expose embedded drafting"),
570 |layers| Observed::exact(layers, "normalized architecture configuration"),
571 );
572 }
573
574 pub fn reject_preparation_admission(&mut self, error: PreparationAdmissionError) {
576 use PreparationAdmissionError as Admission;
577 self.preparation_admission = None;
578 self.requested_load = InspectionReadiness::Unsupported;
579 let code = match error {
580 Admission::UnsupportedQuantization(_) => {
581 InspectionIssueCode::UnsupportedQuantizationRequest
582 }
583 Admission::UnsupportedResidency(_) | Admission::ArchitectureParameterBanks => {
584 InspectionIssueCode::UnsupportedResidencyPolicy
585 }
586 Admission::ArchitectureParallelAxis(_) | Admission::BackendParallelAxis(_) => {
587 InspectionIssueCode::UnsupportedParallelTopology
588 }
589 Admission::ArchitectureInputModality(_) | Admission::BackendInputModality(_) => {
590 InspectionIssueCode::MissingProcessor
591 }
592 _ => InspectionIssueCode::UnsupportedArchitecture,
593 };
594 self.issue(
595 code,
596 InspectionSeverity::Error,
597 error.to_string(),
598 Some(self.path.clone()),
599 );
600 }
601
602 pub fn issue(
604 &mut self,
605 code: InspectionIssueCode,
606 severity: InspectionSeverity,
607 detail: impl Into<String>,
608 path: Option<PathBuf>,
609 ) {
610 self.issues.push(InspectionIssue {
611 code,
612 severity,
613 detail: detail.into(),
614 path,
615 metadata_key: None,
616 tensor_name: None,
617 tensor_type_code: None,
618 });
619 }
620}
621
622fn safetensors_encoding_name(dtype: &crate::checkpoint::TensorDtype) -> String {
623 use crate::checkpoint::TensorDtype;
624 match dtype {
625 TensorDtype::Bf16 => "BF16".into(),
626 TensorDtype::Complex64 => "C64".into(),
627 TensorDtype::Encoded(name) if name == "F8_E4M3" => "F8E4M3".into(),
628 TensorDtype::Encoded(name) if name == "F4" => "F4".into(),
629 TensorDtype::Encoded(name) if name == "F8_E8M0" => "F8E8M0".into(),
630 TensorDtype::Encoded(name) if name == "F8_E5M2" => "F8E5M2".into(),
631 TensorDtype::Encoded(name) => format!("Other({name:?})"),
632 dtype => format!("{dtype:?}"),
633 }
634}
635
636pub fn artifact_modalities(modalities: InputModalities) -> Vec<ArtifactModality> {
638 [
639 (modalities.text, ArtifactModality::Text),
640 (modalities.image, ArtifactModality::Image),
641 (modalities.video, ArtifactModality::Video),
642 (modalities.audio, ArtifactModality::Audio),
643 ]
644 .into_iter()
645 .filter_map(|(enabled, modality)| enabled.then_some(modality))
646 .collect()
647}
648
649#[derive(Debug, Clone, Copy, Eq, PartialEq)]
651pub struct MediaFeatureAvailability {
652 pub image: bool,
654 pub audio: bool,
656}
657
658#[derive(Debug, Clone, Copy, Eq, PartialEq)]
660pub enum MediaProjectorRequirement {
661 NotApplicable,
663 Optional,
665 Required,
667}
668
669pub fn media_feature_readiness(
671 expected: &[ArtifactModality],
672 availability: MediaFeatureAvailability,
673) -> InspectionReadiness {
674 if expected == [ArtifactModality::Text] {
675 return InspectionReadiness::NotApplicable;
676 }
677 if expected.iter().all(|modality| match modality {
678 ArtifactModality::Text => true,
679 ArtifactModality::Image | ArtifactModality::Video => availability.image,
680 ArtifactModality::Audio => availability.audio,
681 }) {
682 InspectionReadiness::Ready
683 } else {
684 InspectionReadiness::Unsupported
685 }
686}
687
688pub fn record_processor_inspection(
690 report: &mut ModelInspectionReport,
691 has_processor: bool,
692 availability: MediaFeatureAvailability,
693) {
694 let feature_readiness = media_feature_readiness(&report.expected_modalities, availability);
695 if feature_readiness == InspectionReadiness::NotApplicable {
696 report.multimodal = feature_readiness;
697 return;
698 }
699 if has_processor {
700 report.multimodal = feature_readiness;
701 report.requirements.push(InspectionRequirement {
702 code: InspectionIssueCode::MissingProcessor,
703 readiness: feature_readiness,
704 detail: if feature_readiness == InspectionReadiness::Ready {
705 "authoritative processor plan and required host-media features are available".into()
706 } else {
707 "authoritative processor plan is available, but required host-media features are not enabled".into()
708 },
709 path: None,
710 });
711 } else {
712 report.multimodal = InspectionReadiness::Missing;
713 report.issue(
714 InspectionIssueCode::MissingProcessor,
715 InspectionSeverity::Warning,
716 "authoritative architecture inspection admitted no media processor",
717 Some(report.path.clone()),
718 );
719 }
720}
721
722pub fn record_media_projector_inspection(
724 report: &mut ModelInspectionReport,
725 artifact_path: &Path,
726 requirement: MediaProjectorRequirement,
727 projector: Option<PathBuf>,
728) -> bool {
729 match (requirement, projector) {
730 (MediaProjectorRequirement::NotApplicable, _) => {
731 report.multimodal = InspectionReadiness::NotApplicable;
732 false
733 }
734 (_, Some(path)) => {
735 report.requirements.push(InspectionRequirement {
736 code: InspectionIssueCode::MissingMediaProjector,
737 readiness: InspectionReadiness::Ready,
738 detail: "portable admission validated the architecture-declared media projector"
739 .into(),
740 path: Some(path),
741 });
742 true
743 }
744 (MediaProjectorRequirement::Optional, None) => {
745 report.multimodal = InspectionReadiness::Missing;
746 report.requirements.push(InspectionRequirement {
747 code: InspectionIssueCode::MissingMediaProjector,
748 readiness: InspectionReadiness::Missing,
749 detail: "text loading is available, but media input requires an architecture-declared sibling projector GGUF".into(),
750 path: None,
751 });
752 report.issue(
753 InspectionIssueCode::MissingMediaProjector,
754 InspectionSeverity::Warning,
755 "no sibling media projector was admitted; text loading remains available",
756 Some(artifact_path.to_path_buf()),
757 );
758 false
759 }
760 (MediaProjectorRequirement::Required, None) => {
761 report.multimodal = InspectionReadiness::Missing;
762 report.model_loadability = InspectionReadiness::Invalid;
763 report.requested_load = InspectionReadiness::Invalid;
764 report.requirements.push(InspectionRequirement {
765 code: InspectionIssueCode::MissingMediaProjector,
766 readiness: InspectionReadiness::Missing,
767 detail:
768 "architecture preparation requires a validated sibling media projector GGUF"
769 .into(),
770 path: None,
771 });
772 report.issue(
773 InspectionIssueCode::MissingMediaProjector,
774 InspectionSeverity::Error,
775 "portable admission omitted an architecture-required media projector",
776 Some(artifact_path.to_path_buf()),
777 );
778 false
779 }
780 }
781}
782
783#[cfg(test)]
784mod tests {
785 use super::*;
786
787 #[test]
788 fn report_schema_round_trips_without_a_backend() {
789 let report =
790 ModelInspectionReport::unverified(Path::new("model.gguf"), ArtifactFormat::Gguf);
791 let json = serde_json::to_string(&report).unwrap();
792 let decoded: ModelInspectionReport = serde_json::from_str(&json).unwrap();
793 assert_eq!(decoded, report);
794 }
795
796 #[test]
797 fn neutral_admission_rejection_has_a_stable_report_code() {
798 let mut report =
799 ModelInspectionReport::unverified(Path::new("model"), ArtifactFormat::SafeTensors);
800 report.reject_preparation_admission(PreparationAdmissionError::BackendParallelAxis(
801 crate::ParallelAxis::Pipeline,
802 ));
803 assert_eq!(report.requested_load, InspectionReadiness::Unsupported);
804 assert!(report.preparation_admission.is_none());
805 assert_eq!(
806 report.issues[0].code,
807 InspectionIssueCode::UnsupportedParallelTopology
808 );
809 }
810}