use std::collections::TryReserveError;
use std::error::Error;
use std::fmt;
use serde::Serialize;
use crate::diagnostic_registry::DiagnosticKind;
use crate::{
DiagnosticDetails, ErrorCode, ErrorDescriptor, OutputFormat, ReferenceProfileIdentity,
};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
#[serde(transparent)]
pub struct DiagnosticCode(&'static str);
impl DiagnosticCode {
pub const NORMALIZATION_MISSING_ELEMENT_ID: Self = Self("normalization.missing-element-id");
pub const NORMALIZATION_DUPLICATE_ELEMENT_ID: Self = Self("normalization.duplicate-element-id");
pub const NORMALIZATION_MISSING_TIMESTAMP: Self = Self("normalization.missing-timestamp");
pub const NORMALIZATION_ZERO_ROUGH_SEED: Self = Self("normalization.zero-rough-seed");
pub const NORMALIZATION_RELATIONSHIP_CYCLE: Self = Self("normalization.relationship-cycle");
pub const COMPATIBILITY_UNSUPPORTED_ELEMENT_TYPE: Self =
Self("compatibility.unsupported-element-type");
pub const COMPATIBILITY_MALFORMED_ELEMENT: Self = Self("compatibility.malformed-element");
pub const COMPATIBILITY_UNSUPPORTED_PROPERTY: Self = Self("compatibility.unsupported-property");
pub const COMPATIBILITY_UNSUPPORTED_PROPERTY_VALUE: Self =
Self("compatibility.unsupported-property-value");
pub const COMPATIBILITY_KNOWN_ELEMENT_REPLACED: Self =
Self("compatibility.known-element-replaced");
pub const ASSET_SUPPLEMENT_CONFLICT: Self = Self("asset.supplement-conflict");
pub const ASSET_MEDIA_TYPE_MISMATCH: Self = Self("asset.media-type-mismatch");
pub const ASSET_MALFORMED_FILE_RECORD: Self = Self("asset.malformed-file-record");
pub const IMAGE_MISSING_FILE: Self = Self("image.missing-file");
pub const IMAGE_FORBIDDEN_RESOURCE: Self = Self("image.forbidden-resource");
pub const IMAGE_ANIMATION_FLATTENED: Self = Self("image.animation-flattened");
pub const IMAGE_UNSUPPORTED_FORMAT: Self = Self("image.unsupported-format");
pub const IMAGE_DECODE_FAILED: Self = Self("image.decode-failed");
pub const IMAGE_UNSAFE_SVG_CONTENT_REMOVED: Self = Self("image.unsafe-svg-content-removed");
pub const LINK_UNSAFE_OMITTED: Self = Self("link.unsafe-omitted");
pub const COLOR_INVALID: Self = Self("color.invalid");
pub const FONT_LIBERATION_SANS_SUBSTITUTED: Self = Self("font.liberation-sans-substituted");
pub const FONT_NOTO_EMOJI_SUBSTITUTED: Self = Self("font.noto-emoji-substituted");
pub const FONT_MISSING_GLYPH: Self = Self("font.missing-glyph");
pub const RESOURCE_ELEMENT_COMPLEXITY_REPLACED: Self =
Self("resource.element-complexity-replaced");
pub const RESOURCE_ELEMENT_WORK_EXCEEDED: Self = Self("resource.element-work-exceeded");
pub const RESOURCE_ASSET_BYTES_EXCEEDED: Self = Self("resource.asset-bytes-exceeded");
pub const RESOURCE_ASSET_WORK_EXCEEDED: Self = Self("resource.asset-work-exceeded");
pub const RESOURCE_IMAGE_DIMENSIONS_EXCEEDED: Self = Self("resource.image-dimensions-exceeded");
pub const RESOURCE_IMAGE_PIXELS_EXCEEDED: Self = Self("resource.image-pixels-exceeded");
pub const RESOURCE_NESTED_SVG_COMPLEXITY_EXCEEDED: Self =
Self("resource.nested-svg-complexity-exceeded");
pub const RESOURCE_NESTED_ASSET_DEPTH_EXCEEDED: Self =
Self("resource.nested-asset-depth-exceeded");
pub const RESOURCE_COORDINATE_RANGE_EXCEEDED: Self = Self("resource.coordinate-range-exceeded");
pub const RESOURCE_OPTIONAL_METADATA_OMITTED: Self = Self("resource.optional-metadata-omitted");
pub const METADATA_PROVENANCE_SERIALIZATION_FAILED: Self =
Self("metadata.provenance-serialization-failed");
pub const METADATA_DIAGNOSTIC_SUMMARY_SERIALIZATION_FAILED: Self =
Self("metadata.diagnostic-summary-serialization-failed");
pub const METADATA_SCENE_SERIALIZATION_FAILED: Self =
Self("metadata.scene-serialization-failed");
pub const METADATA_SCENE_COMPRESSION_FAILED: Self = Self("metadata.scene-compression-failed");
pub const METADATA_SCENE_EMBEDDING_FAILED: Self = Self("metadata.scene-embedding-failed");
pub const METADATA_MANIFEST_SERIALIZATION_FAILED: Self =
Self("metadata.manifest-serialization-failed");
pub const METADATA_MANIFEST_EMBEDDING_FAILED: Self = Self("metadata.manifest-embedding-failed");
pub const PDF_RASTER_FALLBACK: Self = Self("pdf.raster-fallback");
pub(crate) const ALL: [Self; 42] = [
Self::NORMALIZATION_MISSING_ELEMENT_ID,
Self::NORMALIZATION_DUPLICATE_ELEMENT_ID,
Self::NORMALIZATION_MISSING_TIMESTAMP,
Self::NORMALIZATION_ZERO_ROUGH_SEED,
Self::NORMALIZATION_RELATIONSHIP_CYCLE,
Self::COMPATIBILITY_UNSUPPORTED_ELEMENT_TYPE,
Self::COMPATIBILITY_MALFORMED_ELEMENT,
Self::COMPATIBILITY_UNSUPPORTED_PROPERTY,
Self::COMPATIBILITY_UNSUPPORTED_PROPERTY_VALUE,
Self::COMPATIBILITY_KNOWN_ELEMENT_REPLACED,
Self::ASSET_SUPPLEMENT_CONFLICT,
Self::ASSET_MEDIA_TYPE_MISMATCH,
Self::ASSET_MALFORMED_FILE_RECORD,
Self::IMAGE_MISSING_FILE,
Self::IMAGE_FORBIDDEN_RESOURCE,
Self::IMAGE_ANIMATION_FLATTENED,
Self::IMAGE_UNSUPPORTED_FORMAT,
Self::IMAGE_DECODE_FAILED,
Self::IMAGE_UNSAFE_SVG_CONTENT_REMOVED,
Self::LINK_UNSAFE_OMITTED,
Self::COLOR_INVALID,
Self::FONT_LIBERATION_SANS_SUBSTITUTED,
Self::FONT_NOTO_EMOJI_SUBSTITUTED,
Self::FONT_MISSING_GLYPH,
Self::RESOURCE_ELEMENT_COMPLEXITY_REPLACED,
Self::RESOURCE_ELEMENT_WORK_EXCEEDED,
Self::RESOURCE_ASSET_BYTES_EXCEEDED,
Self::RESOURCE_ASSET_WORK_EXCEEDED,
Self::RESOURCE_IMAGE_DIMENSIONS_EXCEEDED,
Self::RESOURCE_IMAGE_PIXELS_EXCEEDED,
Self::RESOURCE_NESTED_SVG_COMPLEXITY_EXCEEDED,
Self::RESOURCE_NESTED_ASSET_DEPTH_EXCEEDED,
Self::RESOURCE_COORDINATE_RANGE_EXCEEDED,
Self::RESOURCE_OPTIONAL_METADATA_OMITTED,
Self::METADATA_PROVENANCE_SERIALIZATION_FAILED,
Self::METADATA_DIAGNOSTIC_SUMMARY_SERIALIZATION_FAILED,
Self::METADATA_SCENE_SERIALIZATION_FAILED,
Self::METADATA_SCENE_COMPRESSION_FAILED,
Self::METADATA_SCENE_EMBEDDING_FAILED,
Self::METADATA_MANIFEST_SERIALIZATION_FAILED,
Self::METADATA_MANIFEST_EMBEDDING_FAILED,
Self::PDF_RASTER_FALLBACK,
];
pub(crate) fn from_stable_str(code: &str) -> Option<Self> {
Self::ALL.into_iter().find(|known| known.as_str() == code)
}
pub const fn as_str(self) -> &'static str {
self.0
}
}
impl fmt::Display for DiagnosticCode {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticSeverity {
Info,
Warning,
}
impl DiagnosticSeverity {
pub const fn as_str(self) -> &'static str {
match self {
Self::Info => "info",
Self::Warning => "warning",
}
}
}
impl fmt::Display for DiagnosticSeverity {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticEffect {
InputRepair,
Visual,
Semantic,
Metadata,
}
impl DiagnosticEffect {
pub const fn as_str(self) -> &'static str {
match self {
Self::InputRepair => "input_repair",
Self::Visual => "visual",
Self::Semantic => "semantic",
Self::Metadata => "metadata",
}
}
pub(crate) fn from_manifest(effect: &str) -> Option<Self> {
match effect {
"input_repair" => Some(Self::InputRepair),
"visual" => Some(Self::Visual),
"semantic" => Some(Self::Semantic),
"metadata" => Some(Self::Metadata),
_ => None,
}
}
}
impl fmt::Display for DiagnosticEffect {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct PendingDiagnostic {
code: DiagnosticCode,
severity: DiagnosticSeverity,
format: OutputFormat,
effects: Vec<DiagnosticEffect>,
element_id: Option<String>,
json_pointer: String,
message: String,
details: Option<Box<DiagnosticDetails>>,
}
fn set_identity_effects(
effects: &mut Vec<DiagnosticEffect>,
format: OutputFormat,
visual: bool,
) -> Result<(), TryReserveError> {
effects.try_reserve(3)?;
effects.clear();
effects.push(DiagnosticEffect::InputRepair);
if visual {
effects.push(DiagnosticEffect::Visual);
if format != OutputFormat::Png {
effects.push(DiagnosticEffect::Semantic);
}
}
Ok(())
}
impl PendingDiagnostic {
pub(crate) fn omit_unbounded_known_element_fallback(&mut self) -> Result<(), TryReserveError> {
if self.code != DiagnosticCode::COMPATIBILITY_UNSUPPORTED_PROPERTY_VALUE {
return Ok(());
}
if let Some(details) = self.details.as_deref_mut() {
if matches!(details, DiagnosticDetails::Compatibility { recovery:crate::Recovery::Placeholder, recipe_id:Some(recipe) }
if recipe==crate::diagnostic_registry::FallbackRecipe::KnownElementUnavailable.id())
{
*details = DiagnosticKind::UnsupportedPropertyValue {
recovery: crate::Recovery::Omitted,
recipe: None,
}
.into_details()?;
}
}
Ok(())
}
pub(crate) fn set_identity_effects(&mut self, visual: bool) -> Result<(), TryReserveError> {
set_identity_effects(&mut self.effects, self.format, visual)
}
pub(crate) fn new(
kind: DiagnosticKind<'_>,
format: OutputFormat,
effects: &[DiagnosticEffect],
element_id: Option<&str>,
json_pointer: &str,
message: &str,
) -> Result<Self, TryReserveError> {
let mut owned_effects = Vec::new();
owned_effects.try_reserve_exact(effects.len())?;
owned_effects.extend_from_slice(effects);
Ok(Self {
code: kind.code(),
severity: kind.severity(),
format,
effects: owned_effects,
element_id: element_id.map(try_owned).transpose()?,
json_pointer: try_owned(json_pointer)?,
message: try_owned(message)?,
details: Some(Box::new(kind.into_details()?)),
})
}
pub(crate) const fn code(&self) -> DiagnosticCode {
self.code
}
pub(crate) fn element_id(&self) -> Option<&str> {
self.element_id.as_deref()
}
pub(crate) fn json_pointer(&self) -> &str {
&self.json_pointer
}
pub(crate) fn scope_malformed_image_recovery(
&mut self,
error_status: impl FnOnce(&str) -> bool,
) -> Result<(), TryReserveError> {
if self.code != DiagnosticCode::ASSET_MALFORMED_FILE_RECORD {
return Ok(());
}
if let Some(DiagnosticDetails::Asset { asset_id, .. }) = self.details.as_deref() {
let kind = DiagnosticKind::ImageUnavailable {
failure: crate::diagnostic_registry::ImageFailure::MalformedFileRecord,
asset_id,
recovery: crate::diagnostic_registry::ImageRecovery::for_format(
self.format,
error_status(asset_id),
),
};
self.details = Some(Box::new(kind.into_details()?));
}
Ok(())
}
pub(crate) fn scope_forbidden_resource_recovery(
&mut self,
has_viewport: impl FnOnce(&str) -> bool,
) -> Result<(), TryReserveError> {
if self.code != DiagnosticCode::IMAGE_FORBIDDEN_RESOURCE {
return Ok(());
}
if let Some(details) = self.details.as_deref_mut() {
if let DiagnosticDetails::Asset { asset_id, .. } = details {
let kind = DiagnosticKind::ForbiddenResource {
asset_id,
recovery: crate::diagnostic_registry::LocalRecovery::placeholder_if(
has_viewport(asset_id),
),
};
*details = kind.into_details()?;
}
}
Ok(())
}
#[cfg(test)]
pub(crate) fn effects(&self) -> &[DiagnosticEffect] {
&self.effects
}
#[cfg(test)]
pub(crate) fn details(&self) -> Option<&DiagnosticDetails> {
self.details.as_deref()
}
pub(crate) fn into_location(self) -> (Option<String>, String) {
(self.element_id, self.json_pointer)
}
pub(crate) fn retained(self, order: DiagnosticOrder) -> CompatibilityDiagnostic {
CompatibilityDiagnostic {
code: self.code,
severity: self.severity,
format: self.format,
effects: self.effects,
element_id: self.element_id,
json_pointer: self.json_pointer,
message: self.message,
details: self.details,
order,
}
}
}
fn try_owned(value: &str) -> Result<String, TryReserveError> {
let mut owned = String::new();
owned.try_reserve_exact(value.len())?;
owned.push_str(value);
Ok(owned)
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct CompatibilityDiagnostic {
code: DiagnosticCode,
severity: DiagnosticSeverity,
format: OutputFormat,
effects: Vec<DiagnosticEffect>,
element_id: Option<String>,
json_pointer: String,
message: String,
details: Option<Box<DiagnosticDetails>>,
#[serde(skip)]
order: DiagnosticOrder,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) struct DiagnosticOrder {
pub(crate) owner_index: u64,
pub(crate) source_position: u64,
pub(crate) retention_ordinal: u64,
pub(crate) attribution: DiagnosticAttribution,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub(crate) enum DiagnosticAttribution {
SceneGlobal,
Target,
}
impl DiagnosticOrder {
pub(crate) fn retained(
owner_index: Option<u64>,
source_position: Option<u64>,
retention_ordinal: u64,
attribution: DiagnosticAttribution,
) -> Self {
Self {
owner_index: owner_index.unwrap_or(u64::MAX),
source_position: source_position.unwrap_or(u64::MAX),
retention_ordinal,
attribution,
}
}
}
impl CompatibilityDiagnostic {
pub(crate) fn omit_surface_fallback(&mut self, has_link: bool) -> Result<(), TryReserveError> {
if let Some(DiagnosticDetails::Compatibility {
recovery,
recipe_id,
}) = self.details.as_deref_mut()
{
if *recovery == crate::Recovery::Placeholder {
if self.code == DiagnosticCode::COMPATIBILITY_MALFORMED_ELEMENT
&& has_link
&& self.format != OutputFormat::Png
&& !self.effects.contains(&DiagnosticEffect::Semantic)
{
self.effects.try_reserve(1)?;
self.effects.push(DiagnosticEffect::Semantic);
}
*recovery = crate::Recovery::Omitted;
*recipe_id = None;
}
}
Ok(())
}
pub(crate) fn set_forbidden_resource_recovery(
&mut self,
paints: bool,
) -> Result<(), TryReserveError> {
if self.code == DiagnosticCode::IMAGE_FORBIDDEN_RESOURCE {
if let Some(details) = self.details.as_deref_mut() {
if let DiagnosticDetails::Asset { asset_id, .. } = details {
*details = DiagnosticKind::ForbiddenResource {
asset_id,
recovery: crate::diagnostic_registry::LocalRecovery::placeholder_if(paints),
}
.into_details()?;
}
}
}
Ok(())
}
pub(crate) fn set_identity_effects(&mut self, visual: bool) -> Result<(), TryReserveError> {
set_identity_effects(&mut self.effects, self.format, visual)
}
pub const fn code(&self) -> DiagnosticCode {
self.code
}
pub const fn severity(&self) -> DiagnosticSeverity {
self.severity
}
pub const fn format(&self) -> OutputFormat {
self.format
}
pub fn effects(&self) -> &[DiagnosticEffect] {
&self.effects
}
pub fn element_id(&self) -> Option<&str> {
self.element_id.as_deref()
}
pub fn json_pointer(&self) -> &str {
&self.json_pointer
}
pub fn message(&self) -> &str {
&self.message
}
pub fn details(&self) -> Option<&DiagnosticDetails> {
self.details.as_deref()
}
pub(crate) fn details_mut(&mut self) -> Option<&mut DiagnosticDetails> {
self.details.as_deref_mut()
}
pub(crate) const fn order(&self) -> DiagnosticOrder {
self.order
}
pub(crate) fn attribute_to_target(&mut self) {
self.order.attribution = DiagnosticAttribution::Target;
}
pub(crate) fn replaces_occurrence(&self) -> bool {
self.effects.contains(&DiagnosticEffect::Visual)
&& matches!(
self.details().and_then(DiagnosticDetails::recovery),
Some(crate::Recovery::Placeholder | crate::Recovery::Omitted)
)
}
}
impl fmt::Display for CompatibilityDiagnostic {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{}: {} [{}]",
self.severity, self.message, self.code
)?;
if let Some(element_id) = &self.element_id {
write!(formatter, " (element {element_id})")?;
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct CompatibilityReport {
pub(crate) schema_version: u32,
pub(crate) diagnostics: Vec<CompatibilityDiagnostic>,
}
impl CompatibilityReport {
pub const fn empty() -> Self {
Self {
schema_version: 1,
diagnostics: Vec::new(),
}
}
pub const fn schema_version(&self) -> u32 {
self.schema_version
}
pub fn diagnostics(&self) -> &[CompatibilityDiagnostic] {
&self.diagnostics
}
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
pub fn len(&self) -> usize {
self.diagnostics.len()
}
}
impl Default for CompatibilityReport {
fn default() -> Self {
Self::empty()
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Artifact {
pub(crate) format: OutputFormat,
pub(crate) bytes: Vec<u8>,
}
impl Artifact {
pub const fn format(&self) -> OutputFormat {
self.format
}
pub const fn media_type(&self) -> &'static str {
self.format.media_type()
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub fn into_bytes(self) -> Vec<u8> {
self.bytes
}
}
#[derive(Clone, Debug)]
pub struct RenderOutcome {
pub(crate) artifact: Artifact,
pub(crate) report: CompatibilityReport,
pub(crate) profile: &'static ReferenceProfileIdentity,
}
impl RenderOutcome {
pub const fn artifact(&self) -> &Artifact {
&self.artifact
}
pub const fn report(&self) -> &CompatibilityReport {
&self.report
}
pub fn into_parts(self) -> (Artifact, CompatibilityReport) {
(self.artifact, self.report)
}
#[doc(hidden)]
pub const fn profile_identity(&self) -> &'static ReferenceProfileIdentity {
self.profile
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "snake_case")]
pub enum RenderErrorCategory {
Input,
Capability,
ResourceLimit,
NoMeaningfulArtifact,
Encoding,
Internal,
}
impl RenderErrorCategory {
pub const fn as_str(self) -> &'static str {
match self {
Self::Input => "input",
Self::Capability => "capability",
Self::ResourceLimit => "resource_limit",
Self::NoMeaningfulArtifact => "no_meaningful_artifact",
Self::Encoding => "encoding",
Self::Internal => "internal",
}
}
}
impl fmt::Display for RenderErrorCategory {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug)]
pub struct RenderError {
pub(crate) category: RenderErrorCategory,
pub(crate) code: ErrorCode,
pub(crate) message: Box<str>,
pub(crate) format: OutputFormat,
pub(crate) element_id: Option<String>,
pub(crate) json_pointer: Box<str>,
pub(crate) details: Option<Box<DiagnosticDetails>>,
pub(crate) report: CompatibilityReport,
}
impl RenderError {
pub const fn category(&self) -> RenderErrorCategory {
self.category
}
pub const fn code(&self) -> ErrorCode {
self.code
}
pub const fn format(&self) -> OutputFormat {
self.format
}
pub fn element_id(&self) -> Option<&str> {
self.element_id.as_deref()
}
pub fn json_pointer(&self) -> &str {
&self.json_pointer
}
pub fn details(&self) -> Option<&DiagnosticDetails> {
self.details.as_deref()
}
pub const fn report(&self) -> &CompatibilityReport {
&self.report
}
pub fn descriptor(&self) -> ErrorDescriptor {
ErrorDescriptor::render(
self.category,
self.code,
self.to_string(),
self.format,
self.element_id.clone(),
self.json_pointer.to_string(),
self.details.as_deref().cloned(),
)
}
}
impl fmt::Display for RenderError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.message)?;
if let Some(crossing) = self.details().and_then(DiagnosticDetails::limit_crossing) {
write!(formatter, ": {crossing}")?;
}
Ok(())
}
}
impl Error for RenderError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::diagnostic_registry::{ImageFailure, ImageRecovery, LocalRecovery};
use crate::{Renderer, ResourceMeasurement, ResourceValue, ValueKind};
fn retained_first(pending: PendingDiagnostic) -> CompatibilityDiagnostic {
pending.retained(DiagnosticOrder::retained(
None,
None,
0,
DiagnosticAttribution::SceneGlobal,
))
}
#[test]
fn owned_outcome_and_render_failure_expose_only_complete_contract_values() {
let diagnostic = retained_first(
PendingDiagnostic::new(
DiagnosticKind::MalformedElement(LocalRecovery::Placeholder),
OutputFormat::Svg,
&[DiagnosticEffect::Visual],
Some("element-1"),
"/elements/0",
"malformed element replaced",
)
.expect("the diagnostic allocates"),
);
let report = CompatibilityReport {
schema_version: 1,
diagnostics: vec![diagnostic],
};
let profile = Renderer::new().profile_identity();
let outcome = RenderOutcome {
artifact: Artifact {
format: OutputFormat::Svg,
bytes: b"<svg/>".to_vec(),
},
report: report.clone(),
profile,
};
assert_eq!(outcome.artifact().bytes(), b"<svg/>");
assert_eq!(outcome.artifact().media_type(), "image/svg+xml");
assert_eq!(outcome.report(), &report);
assert!(std::ptr::eq(outcome.profile_identity(), profile));
assert_eq!(
serde_json::to_string(outcome.report()).expect("serialize diagnosed report"),
concat!(
"{\"schema_version\":1,\"diagnostics\":[{",
"\"code\":\"compatibility.malformed-element\",",
"\"severity\":\"warning\",",
"\"format\":\"svg\",",
"\"effects\":[\"visual\"],",
"\"element_id\":\"element-1\",",
"\"json_pointer\":\"/elements/0\",",
"\"message\":\"malformed element replaced\",",
"\"details\":{\"kind\":\"compatibility\",",
"\"recovery\":\"placeholder\",",
"\"recipe_id\":\"renderer.malformed-element.v1\"}}]}"
)
);
let (artifact, returned_report) = outcome.into_parts();
assert_eq!(artifact.into_bytes(), b"<svg/>");
assert_eq!(returned_report, report);
let error = RenderError {
category: RenderErrorCategory::ResourceLimit,
code: ErrorCode::RESOURCE_OUTPUT_BYTES_EXCEEDED,
message: "output byte limit exceeded".into(),
format: OutputFormat::Svg,
element_id: None,
json_pointer: Box::from(""),
details: Some(Box::new(DiagnosticDetails::ResourceLimit {
field: "max_output_bytes",
configured: ResourceValue::Integer(134_217_728),
value_kind: ValueKind::Bytes,
measurement: ResourceMeasurement::Attempted {
value: ResourceValue::Integer(134_217_729),
},
recovery: None,
})),
report: returned_report,
};
assert_eq!(error.report().len(), 1);
assert_eq!(error.category(), RenderErrorCategory::ResourceLimit);
assert_eq!(
serde_json::to_string(&error.descriptor()).expect("serialize render descriptor"),
concat!(
"{\"family\":\"render\",",
"\"category\":\"resource_limit\",",
"\"code\":\"resource.output-bytes-exceeded\",",
"\"message\":\"output byte limit exceeded: needs 134217729 bytes, ",
"but max_output_bytes is 134217728\",",
"\"format\":\"svg\",",
"\"element_id\":null,",
"\"json_pointer\":\"\",",
"\"details\":{\"kind\":\"resource_limit\",",
"\"field\":\"max_output_bytes\",",
"\"configured\":134217728,",
"\"value_kind\":\"bytes\",",
"\"measurement\":{\"kind\":\"attempted\",\"value\":134217729},",
"\"recovery\":null}}"
)
);
}
#[test]
fn report_preserves_every_diagnostic_in_supplied_canonical_order() {
let diagnostic = |kind, pointer: &str| {
retained_first(
PendingDiagnostic::new(
kind,
OutputFormat::Png,
&[DiagnosticEffect::Visual],
None,
pointer,
"diagnosed",
)
.expect("the diagnostic allocates"),
)
};
let report = CompatibilityReport {
schema_version: 1,
diagnostics: vec![
diagnostic(DiagnosticKind::InvalidColor, "/elements/0/strokeColor"),
diagnostic(
DiagnosticKind::ImageUnavailable {
failure: ImageFailure::DecodeFailed {
media_type: "image/png",
},
asset_id: "image",
recovery: ImageRecovery::for_format(OutputFormat::Png, false),
},
"/files/image/dataURL",
),
],
};
assert_eq!(report.len(), 2);
assert_eq!(
report.diagnostics()[0].code(),
DiagnosticCode::COLOR_INVALID
);
assert_eq!(
report.diagnostics()[1].code(),
DiagnosticCode::IMAGE_DECODE_FAILED
);
}
}