Restore missing required asset/flow packages from the pinned nigig revision; retain upstream script isolation, X11 hover, and widget-core fixes. Validated makepad-widgets data feature with Rust 1.97.1.
704 lines
24 KiB
Rust
704 lines
24 KiB
Rust
//! Import and derived-variant contract fixtures: deterministic goldens,
|
|
//! roundtrips, and fail-closed behavior for the pack-import and processing
|
|
//! document kinds added under schema version 1.
|
|
//!
|
|
//! The golden digests are frozen: a change means the encoding changed for
|
|
//! every peer — that is a schema bump, not a test update.
|
|
|
|
mod common;
|
|
|
|
use common::*;
|
|
use makepad_asset_data::*;
|
|
|
|
/// Frozen golden digests for the import/derived document kinds, in order:
|
|
/// source collection, import revision, watchtower asset id, watchtower
|
|
/// revision, LOD recipe, LOD variant, variant set, resolved map.
|
|
const GOLDENS: [&str; 8] = [
|
|
"scol_44582f1165340930de7e2fae219368cfe76881a4e3450add375635d588eb2572",
|
|
"irev_c1ca2284ec180d5b1a9d4baded72c3a0dfdc0029416f5b65967b05359b6f643c",
|
|
"ast_chopwwsh6hze3ik23elgiamm24",
|
|
"arev_d42a8fda380a647b1370dc9f9aebc0230a7647eca69ec3f8ec6112c66ae0a11a",
|
|
"recp_0bab90f146b206a3cfa0a924d524684a39b2384ceeca25ad5a3fb8da6571c2f4",
|
|
"dvar_eea6fdb11518c5816706698bcecc83f084744e61baabfe7f40ce2b26399b8a65",
|
|
"vset_aa441791256de2141ec46e8dc1996fe743a74830c1747badb80f7627943757b5",
|
|
"rmap_198e11276e6ada7dc33ac303135b2d2dd384b6008ef8b8440fc1f126c756359d",
|
|
];
|
|
|
|
#[test]
|
|
fn goldens_and_roundtrips() {
|
|
let mut actual: Vec<String> = Vec::new();
|
|
|
|
let collection = source_collection();
|
|
actual.push(collection.digest().unwrap().to_string());
|
|
let bytes = collection.to_canonical_bytes().unwrap();
|
|
assert_eq!(
|
|
SourceCollection::from_canonical_bytes(&bytes).unwrap(),
|
|
collection
|
|
);
|
|
|
|
let import = import_manifest();
|
|
let import_rev = import.revision().unwrap();
|
|
actual.push(import_rev.to_string());
|
|
let bytes = import.to_canonical_bytes().unwrap();
|
|
let back = ImportManifest::from_canonical_bytes(&bytes).unwrap();
|
|
assert_eq!(back, import);
|
|
assert_eq!(back.to_canonical_bytes().unwrap(), bytes);
|
|
|
|
// Deterministic asset identity and produced revision.
|
|
let key: PackEntryKey = "models/watchtower".parse().unwrap();
|
|
let asset_id = import.asset_id_for(&key);
|
|
actual.push(asset_id.to_string());
|
|
let produced = import
|
|
.asset_manifest_for(&import.assets[0], &import_rev)
|
|
.unwrap();
|
|
assert_eq!(produced.asset_id, asset_id);
|
|
actual.push(produced.revision().unwrap().to_string());
|
|
// Lineage is pinned: provenance carries the exact import revision plus
|
|
// the collection/pack locator, while the rights record survives VERBATIM
|
|
// — upstream source URL, terms digest, and archive digest included.
|
|
let prov = produced.provenance.as_ref().unwrap();
|
|
assert_eq!(prov.generator, "import");
|
|
assert_eq!(prov.model, "kenney/space-kit");
|
|
assert_eq!(prov.version, "1.0");
|
|
assert_eq!(prov.params_digest, Some(*import_rev.as_bytes()));
|
|
assert_eq!(produced.rights, import.rights);
|
|
assert_eq!(produced.rights.source, "https://kenney.nl/assets/space-kit");
|
|
assert_eq!(produced.rights.terms_digest, Some(sha256(b"CC0-1.0 legal text")));
|
|
assert_eq!(
|
|
produced.rights.source_archive,
|
|
Some(sha256(b"space-kit-1.0.zip"))
|
|
);
|
|
|
|
let recipe = recipe_lod();
|
|
actual.push(recipe.digest().unwrap().to_string());
|
|
let bytes = recipe.to_canonical_bytes().unwrap();
|
|
assert_eq!(ProcessingRecipe::from_canonical_bytes(&bytes).unwrap(), recipe);
|
|
|
|
let base = produced.revision_ref().unwrap();
|
|
let lod = lod_variant(base, 0xa1, 0xb1);
|
|
actual.push(lod.id().unwrap().to_string());
|
|
let bytes = lod.to_canonical_bytes().unwrap();
|
|
assert_eq!(
|
|
DerivedVariantManifest::from_canonical_bytes(&bytes).unwrap(),
|
|
lod
|
|
);
|
|
recipe.validate_result(&lod).unwrap();
|
|
|
|
let thumb = thumb_variant(base, 0xa1, 0xb2);
|
|
recipe_thumbnail().validate_result(&thumb).unwrap();
|
|
|
|
let mut set = VariantSetManifest {
|
|
base,
|
|
variants: vec![lod.id().unwrap(), thumb.id().unwrap()],
|
|
policy_version: RESOLUTION_POLICY_V1,
|
|
};
|
|
set.canonicalize();
|
|
actual.push(set.id().unwrap().to_string());
|
|
let bytes = set.to_canonical_bytes().unwrap();
|
|
assert_eq!(VariantSetManifest::from_canonical_bytes(&bytes).unwrap(), set);
|
|
|
|
let profile = client_profile();
|
|
let bytes = profile.to_canonical_bytes().unwrap();
|
|
assert_eq!(ClientProfile::from_canonical_bytes(&bytes).unwrap(), profile);
|
|
|
|
// Variant order handed to the resolver cannot change the result.
|
|
let map_a = resolve_variants(&set, &[lod.clone(), thumb.clone()], &profile).unwrap();
|
|
let map_b = resolve_variants(&set, &[thumb.clone(), lod.clone()], &profile).unwrap();
|
|
assert_eq!(map_a, map_b);
|
|
actual.push(map_a.digest().unwrap().to_string());
|
|
let bytes = map_a.to_canonical_bytes().unwrap();
|
|
assert_eq!(
|
|
ResolvedVariantMap::from_canonical_bytes(&bytes).unwrap(),
|
|
map_a
|
|
);
|
|
// One entry per role: thumbnail + lod1.
|
|
assert_eq!(map_a.entries.len(), 2);
|
|
assert_eq!(map_a.entries[0].role, VariantRole::Thumbnail);
|
|
assert_eq!(map_a.entries[0].variant, thumb.id().unwrap());
|
|
assert_eq!(map_a.entries[1].role, VariantRole::File(FileRole::Lod1Glb));
|
|
assert_eq!(map_a.entries[1].variant, lod.id().unwrap());
|
|
|
|
assert_eq!(
|
|
actual, GOLDENS,
|
|
"golden digests drifted: a change here is a schema bump, not a test update"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn import_canonicalize_and_field_order() {
|
|
let mut shuffled = import_manifest();
|
|
shuffled.assets.reverse();
|
|
shuffled.assets.iter_mut().for_each(|a| a.files.reverse());
|
|
assert!(matches!(
|
|
shuffled.to_canonical_bytes(),
|
|
Err(AssetDataError::NotSorted { .. })
|
|
));
|
|
shuffled.canonicalize();
|
|
assert_eq!(
|
|
shuffled.to_canonical_bytes().unwrap(),
|
|
import_manifest().to_canonical_bytes().unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn asset_identity_policy_is_version_independent() {
|
|
let import = import_manifest();
|
|
let mut newer = import.clone();
|
|
newer.pack_version = "2.0".into();
|
|
let key: PackEntryKey = "models/watchtower".parse().unwrap();
|
|
// Same asset across pack versions; different import revision.
|
|
assert_eq!(import.asset_id_for(&key), newer.asset_id_for(&key));
|
|
assert_ne!(import.revision().unwrap(), newer.revision().unwrap());
|
|
// Different pack or source forks the identity.
|
|
let mut other_pack = import.clone();
|
|
other_pack.pack_name = "castle-kit".into();
|
|
assert_ne!(import.asset_id_for(&key), other_pack.asset_id_for(&key));
|
|
}
|
|
|
|
#[test]
|
|
fn import_fails_closed() {
|
|
// Forbidden redistribution refuses at validation, before any publication.
|
|
let mut forbidden = import_manifest();
|
|
forbidden.rights.redistribution = Redistribution::Forbidden;
|
|
assert!(matches!(
|
|
forbidden.validate(),
|
|
Err(AssetDataError::Mismatch { .. })
|
|
));
|
|
|
|
// Missing license refuses.
|
|
let mut unlicensed = import_manifest();
|
|
unlicensed.rights.license.clear();
|
|
assert!(matches!(
|
|
unlicensed.validate(),
|
|
Err(AssetDataError::Missing { what: "license" })
|
|
));
|
|
|
|
// Case-colliding spellings refuse outright: the path charset is
|
|
// lowercase-only, so an uppercase spelling is malformed, never a second
|
|
// spelling of the same entry.
|
|
let mut colliding = import_manifest();
|
|
colliding.assets[1].files[0].path = "models/WATCHTOWER.glb".into();
|
|
assert!(matches!(
|
|
colliding.validate(),
|
|
Err(AssetDataError::Malformed { .. })
|
|
));
|
|
// The same path naming a DIFFERENT object refuses.
|
|
colliding.assets[1].files[0].path = "models/watchtower.glb".into();
|
|
assert!(matches!(
|
|
colliding.validate(),
|
|
Err(AssetDataError::Mismatch { what: "shared import path" })
|
|
));
|
|
|
|
// Traversal and absolute paths refuse.
|
|
for bad in ["../escape.glb", "/abs.glb", "a//b.glb", "a/./b.glb", ".hidden"] {
|
|
let mut manifest = import_manifest();
|
|
manifest.assets[0].files[0].file.role = FileRole::Source;
|
|
manifest.assets[0].files[0].path = bad.into();
|
|
assert!(
|
|
matches!(manifest.validate(), Err(AssetDataError::Malformed { .. })),
|
|
"path {bad:?} must refuse"
|
|
);
|
|
}
|
|
|
|
// Unknown mapping policy refuses.
|
|
let mut unknown_policy = import_manifest();
|
|
unknown_policy.policy_version = 2;
|
|
assert!(matches!(
|
|
unknown_policy.validate(),
|
|
Err(AssetDataError::UnsupportedSchema { .. })
|
|
));
|
|
|
|
// A mesh entry without a thumbnail refuses through the produced manifest.
|
|
let mut no_thumb = import_manifest();
|
|
no_thumb.assets[0].thumbnail = None;
|
|
no_thumb.assets[0].metrics.total_bytes -= 300;
|
|
assert!(matches!(
|
|
no_thumb.validate(),
|
|
Err(AssetDataError::Missing { what: "mesh thumbnail" })
|
|
));
|
|
|
|
// Lying byte totals refuse.
|
|
let mut lying = import_manifest();
|
|
lying.assets[0].metrics.total_bytes += 1;
|
|
assert!(matches!(
|
|
lying.validate(),
|
|
Err(AssetDataError::Mismatch { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn shared_pack_paths_only_as_the_exact_same_object() {
|
|
// Kenney packs share texture sheets across models: the SAME canonical
|
|
// object (path + identical file facts) may appear under several assets.
|
|
let mut shared = import_manifest();
|
|
let hull_panel = shared.assets[1].files[0].clone();
|
|
shared.assets[0].files.push(ImportFile {
|
|
path: hull_panel.path.clone(),
|
|
file: AssetFile {
|
|
role: FileRole::Albedo,
|
|
..hull_panel.file
|
|
},
|
|
});
|
|
// Same path but a different role is a DIFFERENT object: refused.
|
|
shared.canonicalize();
|
|
let mut divergent_role = shared.clone();
|
|
divergent_role.assets[0].metrics.total_bytes += 4000;
|
|
assert!(matches!(
|
|
divergent_role.validate(),
|
|
Err(AssetDataError::Mismatch { what: "shared import path" })
|
|
));
|
|
|
|
// Exact same object: allowed, and the import digest is well-defined.
|
|
let mut ok = import_manifest();
|
|
ok.assets[0].files.push(hull_panel.clone());
|
|
ok.assets[0].metrics.total_bytes += 4000;
|
|
ok.assets[0].metrics.max_texture_dim = 2048;
|
|
ok.canonicalize();
|
|
ok.validate().unwrap();
|
|
ok.revision().unwrap();
|
|
|
|
// Same path, same role, different byte length: refused.
|
|
let mut lying = ok.clone();
|
|
for f in &mut lying.assets[0].files {
|
|
if f.path == hull_panel.path {
|
|
f.file.byte_len += 1;
|
|
}
|
|
}
|
|
lying.assets[0].metrics.total_bytes += 1;
|
|
assert!(matches!(
|
|
lying.validate(),
|
|
Err(AssetDataError::Mismatch { what: "shared import path" })
|
|
));
|
|
|
|
// Same path, different blob digest: refused.
|
|
let mut forked = ok.clone();
|
|
for f in &mut forked.assets[0].files {
|
|
if f.path == hull_panel.path {
|
|
f.file.blob = BlobId::hash_of(b"other bytes");
|
|
}
|
|
}
|
|
assert!(matches!(
|
|
forked.validate(),
|
|
Err(AssetDataError::Mismatch { what: "shared import path" })
|
|
));
|
|
|
|
// A file use can never alias a thumbnail use of the same path.
|
|
let mut mixed = import_manifest();
|
|
mixed.assets[1].files.push(ImportFile {
|
|
path: "previews/watchtower.png".into(),
|
|
file: AssetFile {
|
|
role: FileRole::PreviewFront,
|
|
tier: DeviceTier::Any,
|
|
lod: 0,
|
|
media: MediaType::Png,
|
|
blob: blob(0xa3),
|
|
byte_len: 300,
|
|
dims: Some(ImageDims {
|
|
width: 512,
|
|
height: 512,
|
|
}),
|
|
},
|
|
});
|
|
mixed.assets[1].metrics.total_bytes += 300;
|
|
mixed.assets[1].metrics.max_texture_dim = 2048;
|
|
mixed.canonicalize();
|
|
assert!(matches!(
|
|
mixed.validate(),
|
|
Err(AssetDataError::Mismatch { what: "shared import path" })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn windows_hostile_path_segments_refuse() {
|
|
for bad in [
|
|
"models/tower.", // trailing dot: Windows strips it
|
|
"models/con.glb", // reserved device stem
|
|
"aux/texture.png", // reserved device directory
|
|
"models/com1.bin", // reserved numbered device
|
|
"lpt9/x.png", // reserved numbered device directory
|
|
"nul", // bare reserved device
|
|
] {
|
|
let mut manifest = import_manifest();
|
|
manifest.assets[0].files[0].file.role = FileRole::Source;
|
|
manifest.assets[0].files[0].path = bad.into();
|
|
assert!(
|
|
matches!(manifest.validate(), Err(AssetDataError::Malformed { .. })),
|
|
"path {bad:?} must refuse"
|
|
);
|
|
}
|
|
// Names that merely contain a reserved word are fine. The render mesh
|
|
// keeps its role; only its pack path moves.
|
|
for good in ["console/model.glb", "connect.glb", "comix/1.png", "auxiliary.bin"] {
|
|
let mut manifest = import_manifest();
|
|
manifest.assets[0].files[0].path = good.into();
|
|
manifest.validate().unwrap();
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn attribution_required_needs_credits() {
|
|
let mut attributed = import_manifest();
|
|
attributed.rights.redistribution = Redistribution::AttributionRequired;
|
|
attributed.validate().unwrap();
|
|
|
|
attributed.rights.credits.clear();
|
|
assert!(matches!(
|
|
attributed.validate(),
|
|
Err(AssetDataError::Missing {
|
|
what: "credits for attribution-required rights"
|
|
})
|
|
));
|
|
|
|
// Derivative attribution has the same law.
|
|
let mut derivative = import_manifest();
|
|
derivative.rights.derivatives = DerivativePolicy::AttributionRequired;
|
|
derivative.validate().unwrap();
|
|
derivative.rights.credits.clear();
|
|
assert!(matches!(
|
|
derivative.validate(),
|
|
Err(AssetDataError::Missing {
|
|
what: "credits for attribution-required rights"
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn import_alias_must_fit_alias_contract() {
|
|
let mut long = import_manifest();
|
|
long.source_id = "s".repeat(40);
|
|
long.pack_name = "p".repeat(40);
|
|
// 40 + 1 + 40 + 1 + 96 = 178 bytes > MAX_ALIAS_BYTES: the alias this
|
|
// entry would publish under cannot exist, so the import refuses whole.
|
|
let key: PackEntryKey = format!("{}/{}", "k".repeat(48), "m".repeat(47))
|
|
.parse()
|
|
.unwrap();
|
|
long.assets[0].key = key;
|
|
long.canonicalize();
|
|
assert!(matches!(
|
|
long.validate(),
|
|
Err(AssetDataError::OverBudget { .. })
|
|
));
|
|
// The fixture's own aliases fit and are exactly the deterministic form.
|
|
let import = import_manifest();
|
|
assert_eq!(
|
|
import
|
|
.alias_for(&import.assets[0].key)
|
|
.unwrap()
|
|
.as_str(),
|
|
"kenney/space-kit/models/watchtower"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recipe_settings_fail_closed() {
|
|
let mut recipe = recipe_lod();
|
|
recipe.settings = RecipeSettings::MeshLod {
|
|
lod: 3,
|
|
target_triangles: 300,
|
|
};
|
|
assert!(matches!(
|
|
recipe.validate(),
|
|
Err(AssetDataError::Malformed { what: "recipe lod" })
|
|
));
|
|
|
|
recipe.settings = RecipeSettings::ImageTranscode {
|
|
source_role: FileRole::Texture,
|
|
media: ThumbnailMedia::Png,
|
|
quality: 50,
|
|
};
|
|
assert!(matches!(recipe.validate(), Err(AssetDataError::Mismatch { .. })));
|
|
|
|
recipe.settings = RecipeSettings::ImageResize {
|
|
source_role: FileRole::RenderGlb,
|
|
width: 256,
|
|
height: 256,
|
|
media: ThumbnailMedia::Png,
|
|
};
|
|
assert!(matches!(recipe.validate(), Err(AssetDataError::Mismatch { .. })));
|
|
|
|
let mut unknown_schema = recipe_lod();
|
|
unknown_schema.output_schema = 9;
|
|
assert!(matches!(
|
|
unknown_schema.validate(),
|
|
Err(AssetDataError::UnsupportedSchema { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn validate_result_refuses_claim_mismatches() {
|
|
let base = aref(1, 0x11);
|
|
let recipe = recipe_lod();
|
|
|
|
// Over-target triangle count refuses: measured beats claimed.
|
|
let mut over = lod_variant(base, 0xa1, 0xb1);
|
|
over.metrics.triangles = 400;
|
|
assert!(matches!(
|
|
recipe.validate_result(&over),
|
|
Err(AssetDataError::Mismatch { .. })
|
|
));
|
|
|
|
// Wrong recipe digest refuses.
|
|
let mut wrong_recipe = lod_variant(base, 0xa1, 0xb1);
|
|
wrong_recipe.recipe = RecipeDigest::hash_of(b"other");
|
|
assert!(matches!(
|
|
recipe.validate_result(&wrong_recipe),
|
|
Err(AssetDataError::Mismatch { .. })
|
|
));
|
|
|
|
// A thumbnail with wrong dimensions refuses against its recipe.
|
|
let mut wrong_dims = thumb_variant(base, 0xa1, 0xb2);
|
|
wrong_dims.thumbnail.as_mut().unwrap().width = 256;
|
|
assert!(matches!(
|
|
recipe_thumbnail().validate_result(&wrong_dims),
|
|
Err(AssetDataError::Mismatch { .. })
|
|
));
|
|
|
|
// Thumbnail-kind variants cannot smuggle file outputs.
|
|
let mut smuggle = thumb_variant(base, 0xa1, 0xb2);
|
|
smuggle.outputs = lod_variant(base, 0xa1, 0xb3).outputs;
|
|
assert!(smuggle.validate().is_err());
|
|
|
|
// A result that omits the recipe's own input role has untracked lineage.
|
|
let mut no_input = lod_variant(base, 0xa1, 0xb1);
|
|
no_input.inputs = vec![DerivedInput {
|
|
role: FileRole::Albedo,
|
|
blob: blob(0xa1),
|
|
}];
|
|
assert!(matches!(
|
|
recipe.validate_result(&no_input),
|
|
Err(AssetDataError::Missing {
|
|
what: "recipe input role in variant inputs"
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn nondeterministic_tool_refuses_in_v1() {
|
|
let mut recipe = recipe_lod();
|
|
recipe.tool.deterministic = false;
|
|
assert!(matches!(
|
|
recipe.validate(),
|
|
Err(AssetDataError::Malformed {
|
|
what: "nondeterministic tool closure"
|
|
})
|
|
));
|
|
// And it cannot reach a derivation key either: the digest itself
|
|
// validates first.
|
|
assert!(recipe.digest().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn collider_result_enforces_hull_vertex_budget() {
|
|
let base = aref(1, 0x11);
|
|
let recipe = ProcessingRecipe {
|
|
settings: RecipeSettings::MeshCollider {
|
|
max_hull_vertices: 64,
|
|
},
|
|
tool: tool(),
|
|
output_schema: OUTPUT_SCHEMA_V1,
|
|
};
|
|
let collider = |vertices: u32| DerivedVariantManifest {
|
|
base,
|
|
kind: RecipeKind::MeshCollider,
|
|
recipe: recipe.digest().unwrap(),
|
|
inputs: vec![DerivedInput {
|
|
role: FileRole::RenderGlb,
|
|
blob: blob(0xa1),
|
|
}],
|
|
outputs: vec![AssetFile {
|
|
role: FileRole::Collider,
|
|
tier: DeviceTier::Any,
|
|
lod: 0,
|
|
media: MediaType::Bin,
|
|
blob: blob(0xc1),
|
|
byte_len: 128,
|
|
dims: None,
|
|
}],
|
|
thumbnail: None,
|
|
metrics: Metrics {
|
|
total_bytes: 128,
|
|
vertices,
|
|
..Default::default()
|
|
},
|
|
rights: kenney_rights(),
|
|
};
|
|
recipe.validate_result(&collider(48)).unwrap();
|
|
assert!(matches!(
|
|
recipe.validate_result(&collider(65)),
|
|
Err(AssetDataError::Mismatch {
|
|
what: "collider vertices vs hull budget"
|
|
})
|
|
));
|
|
assert!(matches!(
|
|
recipe.validate_result(&collider(0)),
|
|
Err(AssetDataError::Mismatch { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn mixed_output_roles_refuse() {
|
|
let base = aref(1, 0x11);
|
|
let mut mixed = lod_variant(base, 0xa1, 0xb1);
|
|
mixed.outputs.push(AssetFile {
|
|
role: FileRole::Collider,
|
|
tier: DeviceTier::Any,
|
|
lod: 0,
|
|
media: MediaType::Bin,
|
|
blob: blob(0xc2),
|
|
byte_len: 100,
|
|
dims: None,
|
|
});
|
|
mixed.canonicalize();
|
|
mixed.metrics.total_bytes += 100;
|
|
assert!(matches!(
|
|
mixed.validate(),
|
|
Err(AssetDataError::Mismatch {
|
|
what: "mixed variant output roles"
|
|
})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn resolved_map_is_atomic_overrides_over_base() {
|
|
// The map overrides exactly the roles it names; every other base role is
|
|
// retained unchanged from the base manifest.
|
|
let import = import_manifest();
|
|
let import_rev = import.revision().unwrap();
|
|
let produced = import
|
|
.asset_manifest_for(&import.assets[0], &import_rev)
|
|
.unwrap();
|
|
let base = produced.revision_ref().unwrap();
|
|
|
|
let lod = lod_variant(base, 0xa1, 0xb1);
|
|
let thumb = thumb_variant(base, 0xa1, 0xb2);
|
|
let mut set = VariantSetManifest {
|
|
base,
|
|
variants: vec![lod.id().unwrap(), thumb.id().unwrap()],
|
|
policy_version: RESOLUTION_POLICY_V1,
|
|
};
|
|
set.canonicalize();
|
|
let map = resolve_variants(&set, &[lod.clone(), thumb.clone()], &client_profile()).unwrap();
|
|
|
|
// Overridden roles.
|
|
let lod_override = map.override_for(VariantRole::File(FileRole::Lod1Glb)).unwrap();
|
|
assert_eq!(lod_override.variant, lod.id().unwrap());
|
|
assert_eq!(lod_override.blobs, vec![blob(0xb1)]);
|
|
let thumb_override = map.override_for(VariantRole::Thumbnail).unwrap();
|
|
assert_eq!(thumb_override.variant, thumb.id().unwrap());
|
|
|
|
// Roles the map does not name stay the base revision's own files: the
|
|
// render mesh and collider are served exactly as the manifest pins them.
|
|
assert!(map.override_for(VariantRole::File(FileRole::RenderGlb)).is_none());
|
|
assert!(map.override_for(VariantRole::File(FileRole::Collider)).is_none());
|
|
assert!(produced.files.iter().any(|f| f.role == FileRole::RenderGlb));
|
|
assert!(produced.files.iter().any(|f| f.role == FileRole::Collider));
|
|
}
|
|
|
|
#[test]
|
|
fn derivation_key_is_order_independent_and_duplicate_closed() {
|
|
let base = aref(1, 0x11);
|
|
let recipe = recipe_lod().digest().unwrap();
|
|
let a = DerivedInput {
|
|
role: FileRole::RenderGlb,
|
|
blob: blob(1),
|
|
};
|
|
let b = DerivedInput {
|
|
role: FileRole::Albedo,
|
|
blob: blob(2),
|
|
};
|
|
let k1 = derivation_key(&base, &recipe, &[a, b]).unwrap();
|
|
let k2 = derivation_key(&base, &recipe, &[b, a]).unwrap();
|
|
assert_eq!(k1, k2);
|
|
assert!(k1.to_string().starts_with("dkey_"));
|
|
|
|
// Any component change changes the key.
|
|
let other_recipe = recipe_thumbnail().digest().unwrap();
|
|
assert_ne!(k1, derivation_key(&base, &other_recipe, &[a, b]).unwrap());
|
|
assert_ne!(
|
|
k1,
|
|
derivation_key(&aref(1, 0x12), &recipe, &[a, b]).unwrap()
|
|
);
|
|
assert_ne!(k1, derivation_key(&base, &recipe, &[a]).unwrap());
|
|
|
|
assert!(matches!(
|
|
derivation_key(&base, &recipe, &[a, a]),
|
|
Err(AssetDataError::Duplicate { .. })
|
|
));
|
|
assert!(matches!(
|
|
derivation_key(&base, &recipe, &[]),
|
|
Err(AssetDataError::Malformed { .. })
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn resolution_fails_closed() {
|
|
let base = aref(1, 0x11);
|
|
let lod = lod_variant(base, 0xa1, 0xb1);
|
|
let thumb = thumb_variant(base, 0xa1, 0xb2);
|
|
let mut set = VariantSetManifest {
|
|
base,
|
|
variants: vec![lod.id().unwrap(), thumb.id().unwrap()],
|
|
policy_version: RESOLUTION_POLICY_V1,
|
|
};
|
|
set.canonicalize();
|
|
let profile = client_profile();
|
|
|
|
// A manifest not in the set refuses.
|
|
let stranger = lod_variant(base, 0xa1, 0xb9);
|
|
assert!(matches!(
|
|
resolve_variants(&set, &[stranger, thumb.clone()], &profile),
|
|
Err(AssetDataError::Mismatch { .. })
|
|
));
|
|
// Wrong count refuses.
|
|
assert!(matches!(
|
|
resolve_variants(&set, &[lod.clone()], &profile),
|
|
Err(AssetDataError::Mismatch { .. })
|
|
));
|
|
// Wrong base refuses.
|
|
let mut foreign = lod_variant(aref(2, 0x21), 0xa1, 0xb1);
|
|
foreign.canonicalize();
|
|
assert!(matches!(
|
|
resolve_variants(&set, &[foreign, thumb.clone()], &profile),
|
|
Err(AssetDataError::Mismatch { .. })
|
|
));
|
|
// Policy version mismatch refuses.
|
|
let mut wrong_policy = profile;
|
|
wrong_policy.policy_version = 7;
|
|
assert!(resolve_variants(&set, &[lod.clone(), thumb.clone()], &wrong_policy).is_err());
|
|
|
|
// A role whose only variant is incompatible refuses instead of
|
|
// substituting: GLB not accepted, but the LOD role exists in the set.
|
|
let mut no_glb = profile;
|
|
no_glb.accept_glb = false;
|
|
assert!(matches!(
|
|
resolve_variants(&set, &[lod.clone(), thumb.clone()], &no_glb),
|
|
Err(AssetDataError::Missing { what: "compatible variant for role" })
|
|
));
|
|
|
|
// Resolution never invents content: an empty set cannot even exist.
|
|
let empty = VariantSetManifest {
|
|
base,
|
|
variants: vec![],
|
|
policy_version: RESOLUTION_POLICY_V1,
|
|
};
|
|
assert!(empty.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn new_document_kinds_do_not_cross_decode() {
|
|
let import_bytes = import_manifest().to_canonical_bytes().unwrap();
|
|
assert!(matches!(
|
|
SourceCollection::from_canonical_bytes(&import_bytes),
|
|
Err(AssetDataError::BadDocKind { .. })
|
|
));
|
|
let recipe_bytes = recipe_lod().to_canonical_bytes().unwrap();
|
|
assert!(matches!(
|
|
DerivedVariantManifest::from_canonical_bytes(&recipe_bytes),
|
|
Err(AssetDataError::BadDocKind { .. })
|
|
));
|
|
// And the frozen kinds refuse the new ones.
|
|
assert!(matches!(
|
|
AssetManifest::from_canonical_bytes(&import_bytes),
|
|
Err(AssetDataError::BadDocKind { .. })
|
|
));
|
|
}
|