use std::path::Path;
use openusd::{sdf, usd};
const ASSETS: &str = "vendor/core-spec-supplemental-release_dec2025/composition/tests/assets";
mod schema {
use std::collections::HashMap;
#[derive(serde::Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Baseline {
pub entry: String,
#[serde(default)]
pub composing: HashMap<String, PrimData>,
#[serde(default)]
pub errors: Vec<String>,
}
#[derive(serde::Deserialize, Debug)]
pub struct PrimData {
#[serde(default, rename = "Child names")]
pub child_names: Vec<String>,
#[serde(default, rename = "Property names")]
pub property_names: Vec<String>,
}
}
#[derive(Clone, Copy)]
enum Format {
Text,
Binary,
}
fn run(name: &str, format: Format) {
let test_dir = Path::new(ASSETS).join(name);
let baseline_path = test_dir.join("pcp.json");
let json =
std::fs::read_to_string(&baseline_path).unwrap_or_else(|e| panic!("read {}: {e}", baseline_path.display()));
let baseline: schema::Baseline = serde_json::from_str(&json).expect("parse pcp.json");
if !baseline.errors.is_empty() {
return;
}
if baseline.composing.is_empty() {
return;
}
let entry = match format {
Format::Text => test_dir.join("usda").join(&baseline.entry),
Format::Binary => test_dir.join(&baseline.entry),
};
if !entry.exists() {
return;
}
let stage = usd::Stage::builder()
.on_error(|_| Ok(()))
.open(entry.to_str().unwrap())
.unwrap();
let mut prims = Vec::new();
stage.traverse_all(|path| prims.push(path.to_string())).unwrap();
let mut failures = Vec::new();
for (prim_path, expected) in &baseline.composing {
if !prims.iter().any(|p| p == prim_path) {
failures.push(format!("missing prim: {prim_path}"));
continue;
}
for child in &expected.child_names {
let child_path = format!("{prim_path}/{child}");
if !prims.iter().any(|p| p == &child_path) {
failures.push(format!("missing child: {child_path}"));
}
}
for prop in &expected.property_names {
let prop_path = format!("{prim_path}.{prop}");
if !stage.has_spec(sdf::path(&prop_path).unwrap()).unwrap_or(false) {
failures.push(format!("missing property: {prop_path}"));
}
}
}
assert!(
failures.is_empty(),
"composition test {name} ({format}) failed:\n {}",
failures.join("\n "),
format = match format {
Format::Text => "text",
Format::Binary => "binary",
},
);
}
macro_rules! composition_tests {
($($name:ident),* $(,)?) => {
composition_tests!(@expand $($name),*);
};
(@expand $($name:ident),*) => {
$(
composition_tests!(@one $name);
)*
};
(@one $name:ident) => {
#[cfg(test)]
#[allow(non_snake_case)]
mod $name {
use super::*;
#[test]
fn text() { run(stringify!($name), Format::Text); }
#[test]
fn binary() { run(stringify!($name), Format::Binary); }
}
};
}
composition_tests! {
BasicAncestralReference_root,
BasicDuplicateSublayer_root,
BasicInherits_root,
BasicInstancing_root,
BasicInstancingAndNestedInstances_root,
BasicInstancingAndVariants_root,
BasicListEditing_root,
BasicListEditingWithInherits_root,
BasicLocalAndGlobalClassCombination_root,
BasicNestedPayload_root,
BasicNestedVariants_root,
BasicNestedVariantsWithSameName_root,
BasicOwner_root,
BasicPayload_root,
BasicPayloadDiamond_root,
BasicReference_session,
BasicReferenceAndClass_root,
BasicReferenceAndClassDiamond_root,
BasicReferenceDiamond_root,
BasicRelocateToAnimInterface_root,
BasicRelocateToAnimInterfaceAsNewRootPrim_root,
BasicSpecializes_root,
BasicSpecializesAndInherits_root,
BasicSpecializesAndReferences_root,
BasicSpecializesAndVariants_root,
BasicTimeOffset_root,
BasicVariantWithConnections_root,
BasicVariantWithReference_root,
bug69932_root,
bug74847_root,
bug92827_root,
case1_root,
ElidedAncestralRelocates_root,
ErrorArcCycle_root,
ErrorConnectionPermissionDenied_root,
ErrorInconsistentProperties_root,
ErrorInvalidAuthoredRelocates_root,
ErrorInvalidConflictingRelocates_root,
ErrorInvalidInstanceTargetPath_root,
ErrorInvalidPayload_root,
ErrorInvalidPreRelocateTargetPath_root,
ErrorInvalidReferenceToRelocationSource_root,
ErrorInvalidTargetPath_root,
ErrorOpinionAtRelocationSource_root,
ErrorOwner_root,
ErrorPermissionDenied_root,
ErrorRelocateWithVariantSelection_root,
ErrorSublayerCycle_root,
ExpressionsInPayloads_root,
ExpressionsInReferences_root,
ImpliedAndAncestralInherits_ComplexEvaluation_root,
ImpliedAndAncestralInherits_root,
PayloadsAndAncestralArcs_root,
PayloadsAndAncestralArcs2_root,
PayloadsAndAncestralArcs3_root,
ReferenceListOpsWithOffsets_root,
RelativePathPayloads_root,
RelativePathReferences_root,
RelocatePrimsWithSameName_root,
RelocateToNone_root,
SpecializesAndAncestralArcs_root,
SpecializesAndAncestralArcs2_root,
SpecializesAndAncestralArcs3_root,
SpecializesAndAncestralArcs4_root,
SpecializesAndAncestralArcs5_root,
SpecializesAndVariants_root,
SpecializesAndVariants2_root,
SpecializesAndVariants3_root,
SpecializesAndVariants4_root,
SubrootInheritsAndVariants_root,
SubrootReferenceAndClasses_root,
SubrootReferenceAndRelocates_root,
SubrootReferenceAndVariants_root,
SubrootReferenceAndVariants2_root,
SubrootReferenceNonCycle_root,
TimeCodesPerSecond_root,
TimeCodesPerSecond_root_12fps,
TimeCodesPerSecond_root_24tcps_12fps,
TimeCodesPerSecond_root_48tcps,
TimeCodesPerSecond_session,
TimeCodesPerSecond_session_24fps,
TimeCodesPerSecond_session_48tcps,
TrickyClassHierarchy_root,
TrickyConnectionToRelocatedAttribute_root,
TrickyInheritsAndRelocates_root,
TrickyInheritsAndRelocates2_root,
TrickyInheritsAndRelocates3_root,
TrickyInheritsAndRelocates4_root,
TrickyInheritsAndRelocates5_root,
TrickyInheritsAndRelocatesToNewRootPrim_root,
TrickyInheritsInVariants_root,
TrickyInheritsInVariants2_root,
TrickyListEditedTargetPaths_root,
TrickyLocalClassHierarchyWithRelocates_root,
TrickyMultipleRelocations_root,
TrickyMultipleRelocations2_root,
TrickyMultipleRelocations3_root,
TrickyMultipleRelocations4_root,
TrickyMultipleRelocations5_root,
TrickyMultipleRelocationsAndClasses_root,
TrickyMultipleRelocationsAndClasses2_root,
TrickyNestedClasses_root,
TrickyNestedClasses2_root,
TrickyNestedClasses3_root,
TrickyNestedClasses4_root,
TrickyNestedSpecializes_root,
TrickyNestedSpecializes2_root,
TrickyNestedVariants_root,
TrickyNonLocalVariantSelection_root,
TrickyRelocatedTargetInVariant_root,
TrickyRelocationOfPrimFromPayload_root,
TrickyRelocationOfPrimFromVariant_root,
TrickyRelocationSquatter_root,
TrickySpecializesAndInherits_root,
TrickySpecializesAndInherits2_root,
TrickySpecializesAndInherits3_root,
TrickySpecializesAndRelocates_root,
TrickySpookyInherits_root,
TrickySpookyInheritsInSymmetricArmRig_root,
TrickySpookyInheritsInSymmetricBrowRig_root,
TrickySpookyVariantSelection_root,
TrickySpookyVariantSelectionInClass_root,
TrickyVariantAncestralSelection_root,
TrickyVariantIndependentSelection_root,
TrickyVariantInPayload_root,
TrickyVariantOverrideOfLocalClass_root,
TrickyVariantOverrideOfRelocatedPrim_root,
TrickyVariantSelectionInVariant_root,
TrickyVariantSelectionInVariant2_root,
TrickyVariantWeakerSelection_root,
TrickyVariantWeakerSelection2_root,
TrickyVariantWeakerSelection3_root,
TrickyVariantWeakerSelection4_root,
TypicalReferenceToChargroup_root,
TypicalReferenceToChargroupWithRename_root,
TypicalReferenceToRiggedModel_root,
VariantSpecializesAndReference_root,
VariantSpecializesAndReferenceSurprisingBehavior_root,
}
#[cfg(test)]
mod reorder {
use super::*;
fn open_fixture() -> usd::Stage {
usd::Stage::open("fixtures/reorder.usda").expect("open reorder fixture")
}
#[test]
fn prim_order_reorders_named_children() {
let stage = open_fixture();
let children = stage.prim_children(sdf::path("/Root").unwrap()).unwrap();
assert_eq!(children, vec!["C", "B", "A", "D"]);
}
#[test]
fn property_order_reorders_named_properties() {
let stage = open_fixture();
let props = stage.prim_properties(sdf::path("/Props").unwrap()).unwrap();
assert_eq!(props, vec!["y", "x", "z"]);
}
}
#[cfg(test)]
mod value_resolution {
use std::collections::HashMap;
use super::*;
use openusd::sdf::{FieldKey, Specifier, Value, Variability};
fn open_fixture() -> usd::Stage {
usd::Stage::open("fixtures/value_resolution.usda").expect("open value_resolution fixture")
}
fn dictionary<'a>(dict: &'a HashMap<String, Value>, key: &str) -> &'a HashMap<String, Value> {
match dict.get(key) {
Some(Value::Dictionary(value)) => value,
other => panic!("expected dictionary at {key:?}, got {other:?}"),
}
}
fn string<'a>(dict: &'a HashMap<String, Value>, key: &str) -> &'a str {
match dict.get(key) {
Some(Value::String(value) | Value::Token(value)) => value,
other => panic!("expected string/token at {key:?}, got {other:?}"),
}
}
#[test]
fn specifier_inherit_only_resolves_to_class() {
let stage = open_fixture();
let value: Option<Value> = stage
.field(sdf::path("/InheritOnly").unwrap(), FieldKey::Specifier)
.unwrap();
assert_eq!(value, Some(Value::Specifier(Specifier::Class)));
}
#[test]
fn specifier_all_over_resolves_to_over() {
let stage = open_fixture();
let value: Option<Value> = stage
.field(sdf::path("/AllOver").unwrap(), FieldKey::Specifier)
.unwrap();
assert_eq!(value, Some(Value::Specifier(Specifier::Over)));
}
#[test]
fn specifier_def_resolves_to_def() {
let stage = open_fixture();
let value: Option<Value> = stage
.field(sdf::path("/DefPrim").unwrap(), FieldKey::Specifier)
.unwrap();
assert_eq!(value, Some(Value::Specifier(Specifier::Def)));
}
#[test]
fn variability_weakest_opinion_wins() {
let stage = open_fixture();
let value: Option<Value> = stage
.field(sdf::path("/VarTest.attr").unwrap(), FieldKey::Variability)
.unwrap();
assert_eq!(value, Some(Value::Variability(Variability::Uniform)));
}
#[test]
fn custom_any_true() {
let stage = open_fixture();
let value: Option<Value> = stage
.field(sdf::path("/CustomTest.attr").unwrap(), FieldKey::Custom)
.unwrap();
assert_eq!(value, Some(Value::Bool(true)));
}
#[test]
fn dictionary_values_compose_recursively() {
let stage = open_fixture();
let value: Option<Value> = stage
.field(sdf::path("/DictTest").unwrap(), FieldKey::CustomData)
.unwrap();
let Some(Value::Dictionary(dict)) = value else {
panic!("customData should resolve to a dictionary");
};
assert_eq!(string(&dict, "strongOnly"), "strong");
assert_eq!(string(&dict, "weakOnly"), "weak");
assert_eq!(string(&dict, "strongOver"), "strong");
let nested = dictionary(&dict, "nested");
assert_eq!(string(nested, "strongNested"), "strong");
assert_eq!(string(nested, "weakNested"), "weak");
let deep = dictionary(nested, "deep");
assert_eq!(string(deep, "conflict"), "strong");
assert_eq!(string(deep, "strongDeep"), "strong");
assert_eq!(string(deep, "weakDeep"), "weak");
assert_eq!(string(&dict, "strongScalarWins"), "strong-scalar");
let strong_dict = dictionary(&dict, "strongDictWins");
assert_eq!(string(strong_dict, "strongNested"), "strong");
}
#[test]
fn layer_metadata_dictionary_uses_root_layer_only() {
let stage = open_fixture();
let value: Option<Value> = stage.field(sdf::Path::abs_root(), FieldKey::CustomLayerData).unwrap();
let Some(Value::Dictionary(dict)) = value else {
panic!("customLayerData should resolve to a dictionary");
};
assert_eq!(string(&dict, "rootOnly"), "root");
assert!(!dict.contains_key("weakOnly"));
let nested = dictionary(&dict, "nested");
assert_eq!(string(nested, "rootNested"), "root");
assert!(!nested.contains_key("weakNested"));
}
}