use std::path::Path;
use openusd::{pcp, sdf, usd};
const ASSETS: &str = "vendor/core-spec-supplemental-release_dec2025/composition/tests/assets";
mod pcp_json {
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,
}
impl std::fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Format::Text => "text",
Format::Binary => "binary",
})
}
}
fn variant_fallbacks() -> pcp::VariantFallbackMap {
pcp::VariantFallbackMap::new().add("standin", ["render"])
}
fn open_stage(entry: &Path) -> usd::Stage {
usd::Stage::builder()
.variant_fallbacks(variant_fallbacks())
.open(entry.to_str().unwrap())
.unwrap()
}
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: pcp_json::Baseline = serde_json::from_str(&json).expect("parse pcp.json");
let entry = match format {
Format::Text => test_dir.join("usda").join(&baseline.entry),
Format::Binary => test_dir.join(&baseline.entry),
};
if !entry.exists() {
return;
}
if UNREPRODUCIBLE_GOLDEN.contains(&name) {
assert_prims_exist(name, format, &baseline, &entry);
} else {
assert_dump_matches(name, format, &test_dir, &baseline, &entry);
}
}
fn assert_prims_exist(name: &str, format: Format, baseline: &pcp_json::Baseline, entry: &Path) {
if !baseline.errors.is_empty() || baseline.composing.is_empty() {
return;
}
let stage = open_stage(entry);
let mut prims = Vec::new();
stage
.traverse(usd::PrimPredicate::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}"));
}
}
let props = stage.prim_at(prim_path.as_str()).property_names().unwrap_or_default();
for prop in &expected.property_names {
if !props.contains(prop) {
failures.push(format!("missing property: {prim_path}.{prop}"));
}
}
}
assert!(
failures.is_empty(),
"composition test {name} ({format}) failed:\n {}",
failures.join("\n "),
);
}
const UNREPRODUCIBLE_GOLDEN: &[&str] = &[
"BasicInherits_root",
"ErrorRelocateWithVariantSelection_root",
"SubrootReferenceAndVariants_root",
"BasicPayload_root",
"RelocateToNone_root",
];
mod pcp_txt {
use std::path::Path;
use openusd::{pcp, sdf, usd};
fn separator() -> String {
"-".repeat(72)
}
fn display_name(canonical_base: Option<&Path>, identifier: &str) -> String {
if let (Some(base), Ok(id)) = (canonical_base, std::fs::canonicalize(identifier)) {
if let Ok(rel) = id.strip_prefix(base) {
return rel
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/");
}
}
Path::new(identifier)
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| identifier.to_string())
}
fn site_line(out: &mut String, canonical_base: Option<&Path>, identifier: &str, path: &sdf::Path) {
use std::fmt::Write as _;
let _ = writeln!(out, " {:<20} {}", display_name(canonical_base, identifier), path);
}
fn time_offset_line(
out: &mut String,
canonical_base: Option<&Path>,
layer_id: &str,
path: Option<&sdf::Path>,
arc: &str,
offset: sdf::LayerOffset,
) {
use std::fmt::Write as _;
let layer = display_name(canonical_base, layer_id);
let (off, scale) = (offset.offset, offset.scale);
match path {
Some(path) => {
let _ = writeln!(
out,
" {layer:<20} {:<15} {arc:<10} (offset={off:.2}, scale={scale:.2})",
path.to_string(),
);
}
None => {
let _ = writeln!(out, " {layer:<32} {arc:<10} (offset={off:.2}, scale={scale:.2})");
}
}
}
fn walk_nodes(index: &pcp::PrimIndex) -> Vec<pcp::NodeId> {
let Some(root) = index.root() else {
return Vec::new();
};
let mut out = Vec::new();
let mut stack: Vec<pcp::NodeId> = index.children(root).iter().rev().copied().collect();
while let Some(id) = stack.pop() {
out.push(id);
stack.extend(index.children(id).iter().rev().copied());
}
out
}
fn name_list(names: &[String]) -> String {
let quoted: Vec<String> = names.iter().map(|n| format!("'{n}'")).collect();
format!("[{}]", quoted.join(", "))
}
fn write_grouped<T>(
out: &mut String,
header: &str,
mut groups: Vec<(sdf::Path, Vec<T>)>,
mut emit_item: impl FnMut(&mut String, &T),
) {
use std::fmt::Write as _;
if groups.is_empty() {
return;
}
groups.sort_by(|a, b| a.0.to_string().cmp(&b.0.to_string()));
let _ = writeln!(out, "{header}:");
for (prop_path, items) in &groups {
let _ = writeln!(out, "{prop_path}:");
for item in items {
emit_item(out, item);
}
}
out.push('\n');
}
pub fn pcp_dump(name: &str, entry: &str, base: &Path, stage: &usd::Stage) -> String {
use std::fmt::Write as _;
let sep = separator();
let mut out = String::new();
let canonical_base = std::fs::canonicalize(base).ok();
let base = canonical_base.as_deref();
let _ = writeln!(out, "Loading @composition/tests/assets/{name}/usda/{entry}@");
out.push('\n');
let _ = writeln!(out, "{sep}");
let _ = writeln!(out, "Layer Stack:");
for identifier in stage.layer_stack() {
let _ = writeln!(out, " {}", display_name(base, &identifier));
}
out.push('\n');
let mut prims = Vec::new();
stage
.traverse(usd::PrimPredicate::ALL, |path| prims.push(path.clone()))
.unwrap();
for path in &prims {
let prim = stage.prim_at(path.clone());
let _ = writeln!(out, "{sep}");
let _ = writeln!(out, "Results for composing <{path}>");
out.push('\n');
let _ = writeln!(out, "Prim Stack:");
for (identifier, spec_path) in prim.prim_stack().unwrap() {
site_line(&mut out, base, &identifier, &spec_path);
}
out.push('\n');
let index = prim.prim_index().graph().unwrap();
let walk = walk_nodes(&index);
let has_offsets = walk.iter().any(|&id| {
let node = index.node(id);
!node.map_to_parent.time_offset().is_identity()
|| node.layer_stack().iter().any(|(_, off)| !off.is_identity())
});
if has_offsets {
let _ = writeln!(out, "Time Offsets:");
for id in walk {
let node = index.node(id);
let layer_id = stage.layer_identifier(node.layer_id()).unwrap_or_default();
let arc = match node.arc {
pcp::ArcType::Root => "root",
pcp::ArcType::Inherit => "inherit",
pcp::ArcType::Variant => "variant",
pcp::ArcType::Relocate => "relocate",
pcp::ArcType::Reference => "reference",
pcp::ArcType::Payload => "payload",
pcp::ArcType::Specialize => "specialize",
};
time_offset_line(
&mut out,
base,
&layer_id,
Some(&node.path),
arc,
node.map_to_root.time_offset(),
);
for &(layer, off) in node.layer_stack() {
if !off.is_identity() {
let layer_id = stage.layer_identifier(layer).unwrap_or_default();
time_offset_line(&mut out, base, &layer_id, None, "sublayer", off);
}
}
}
out.push('\n');
}
let selections = prim.variant_sets().get_all_variant_selections().unwrap();
if !selections.is_empty() {
let _ = writeln!(out, "Variant Selections:");
for (set, sel) in &selections {
let _ = writeln!(out, " {{{set} = {sel}}}");
}
out.push('\n');
}
let (children, prohibited) = prim.prim_index().child_names().unwrap();
if !children.is_empty() {
let _ = writeln!(out, "Child names:");
let _ = writeln!(out, " {}", name_list(&children));
out.push('\n');
}
if !prohibited.is_empty() {
let _ = writeln!(out, "Prohibited child names:");
let _ = writeln!(out, " {}", name_list(&prohibited));
out.push('\n');
}
let properties = prim.property_names().unwrap();
if !properties.is_empty() {
let _ = writeln!(out, "Property names:");
let _ = writeln!(out, " {}", name_list(&properties));
out.push('\n');
}
let mut stacks: Vec<(sdf::Path, Vec<(String, sdf::Path)>)> = Vec::new();
for name in &properties {
let prop = prim.attribute(name);
let stack = prop.property_stack().unwrap();
if !stack.is_empty() {
stacks.push((prop.path().clone(), stack));
}
}
write_grouped(&mut out, "Property stacks", stacks, |out, (identifier, spec_path)| {
site_line(out, base, identifier, spec_path);
});
let rel_paths: std::collections::HashSet<sdf::Path> = prim
.relationships()
.unwrap()
.iter()
.map(|rel| rel.path().clone())
.collect();
let mut rel_targets: Vec<(sdf::Path, Vec<sdf::Path>)> = Vec::new();
let mut attr_conns: Vec<(sdf::Path, Vec<sdf::Path>)> = Vec::new();
let mut deleted: Vec<(sdf::Path, Vec<sdf::Path>)> = Vec::new();
for name in &properties {
let Some(prop_path) = path.append_property(name).ok() else {
continue;
};
if rel_paths.contains(&prop_path) {
let (targets, del) = prim.relationship(name).compute_targets().unwrap();
if !targets.is_empty() {
rel_targets.push((prop_path.clone(), targets));
}
if !del.is_empty() {
deleted.push((prop_path, del));
}
} else {
let (conns, del) = prim.attribute(name).compute_connections().unwrap();
if !conns.is_empty() {
attr_conns.push((prop_path.clone(), conns));
}
if !del.is_empty() {
deleted.push((prop_path, del));
}
}
}
let emit_target = |out: &mut String, target: &sdf::Path| {
let _ = writeln!(out, " {target}");
};
write_grouped(&mut out, "Relationship targets", rel_targets, emit_target);
write_grouped(&mut out, "Attribute connections", attr_conns, emit_target);
write_grouped(&mut out, "Deleted target paths", deleted, emit_target);
}
out
}
pub fn first_diff_lines(actual: &str, expected: &str, limit: usize) -> String {
use std::fmt::Write as _;
let actual: Vec<&str> = actual.lines().collect();
let expected: Vec<&str> = expected.lines().collect();
let mut report = String::new();
let mut shown = 0;
for i in 0..actual.len().max(expected.len()) {
let a = actual.get(i).copied().unwrap_or("<eof>");
let g = expected.get(i).copied().unwrap_or("<eof>");
if a != g {
let _ = writeln!(report, " line {}:\n actual: {a}\n golden: {g}", i + 1);
shown += 1;
if shown >= limit {
let _ = writeln!(report, " …");
break;
}
}
}
report
}
fn arc_cannot_verb(arc: pcp::ArcType) -> &'static str {
match arc {
pcp::ArcType::Inherit | pcp::ArcType::Specialize => "inherit from",
pcp::ArcType::Relocate => "be relocated from",
_ => "reference",
}
}
fn arc_present_verb(arc: pcp::ArcType) -> &'static str {
match arc {
pcp::ArcType::Inherit | pcp::ArcType::Specialize => "inherits from",
pcp::ArcType::Relocate => "is relocated from",
_ => "references",
}
}
pub fn error_trailer(name: &str, errors: &[pcp::Error], base: Option<&Path>, order: &[sdf::Path]) -> String {
use std::fmt::Write as _;
let layer_id = |id: &str| format!("composition/tests/assets/{name}/usda/{}", display_name(base, id));
let entries: Vec<(Option<&sdf::Path>, String)> = errors
.iter()
.filter_map(|e| match e {
pcp::Error::UnresolvedLayer {
asset_path,
arc: pcp::ArcType::Payload,
introduced_by,
site_path,
} => Some((
Some(site_path),
format!(
"Could not open asset @{}@ for payload introduced by @{}@<{site_path}>.",
layer_id(asset_path),
layer_id(introduced_by),
),
)),
pcp::Error::UnresolvedPrimPath {
arc,
target_layer,
prim_path,
introduced_by,
site_path,
} if matches!(arc, pcp::ArcType::Payload | pcp::ArcType::Reference) => {
let kind = if matches!(arc, pcp::ArcType::Payload) {
"payload"
} else {
"reference"
};
Some((
Some(site_path),
format!(
"Unresolved {kind} prim path @{}@<{prim_path}> introduced by @{}@<{site_path}>",
layer_id(target_layer),
layer_id(introduced_by),
),
))
}
pcp::Error::ProhibitedRelocationSource {
arc,
site,
site_layer,
target,
target_layer,
reloc_source,
reloc_layer,
composing,
} => {
let verb = arc_cannot_verb(*arc);
Some((
Some(composing),
format!(
"@{}@<{site}>\nCANNOT {verb}:\n@{}@<{target}>\n\
which is a prohibited child of its parent because it would require allowing opinions \
from the source of a relocation at @{}@<{reloc_source}>.",
layer_id(site_layer),
layer_id(target_layer),
layer_id(reloc_layer),
),
))
}
pcp::Error::OpinionAtRelocationSource {
source_path,
layer,
composing,
} => Some((
Some(composing),
format!(
"The layer @{}@ has an invalid opinion at the relocation source path <{source_path}>, \
which will be ignored.",
layer_id(layer),
),
)),
pcp::Error::InvalidExternalTargetPath {
is_connection,
target,
property,
layer,
arc,
arc_root,
composing,
} => {
let kind = if *is_connection {
"attribute connection"
} else {
"relationship target"
};
let arc = match arc {
pcp::ArcType::Inherit => "inherit",
pcp::ArcType::Specialize => "specializes",
pcp::ArcType::Payload => "payload",
pcp::ArcType::Variant => "variant",
_ => "reference",
};
Some((
Some(composing),
format!(
"The {kind} <{}> from <{}> in layer @{}@ refers to a path outside the scope of the {arc} \
from <{}>. Ignoring.",
target,
property,
layer_id(layer),
arc_root,
),
))
}
pcp::Error::InvalidInstanceTargetPath {
is_connection,
target,
property,
layer,
composing,
} => {
let kind = if *is_connection {
"attribute connection"
} else {
"relationship target"
};
Some((
Some(composing),
format!(
"The {kind} <{}> from <{}> in layer @{}@ is authored in a class but refers to an instance \
of that class. Ignoring.",
target,
property,
layer_id(layer),
),
))
}
pcp::Error::InconsistentPropertyType {
property,
defining_layer,
defining_path,
defining_is_attribute,
conflicting_layer,
conflicting_path,
conflicting_is_attribute,
composing,
} => {
let kind = |is_attr: bool| if is_attr { "an attribute spec" } else { "a relationship spec" };
Some((
Some(composing),
format!(
"The property <{}> has inconsistent spec types. \
The defining spec is @{}@<{}> and is {}. \
The conflicting spec is @{}@<{}> and is {}. The conflicting spec will be ignored.",
property,
layer_id(defining_layer),
defining_path,
kind(*defining_is_attribute),
layer_id(conflicting_layer),
conflicting_path,
kind(*conflicting_is_attribute),
),
))
}
pcp::Error::SublayerCycle { root_layer, seen_layer } => Some((
None,
format!(
"Sublayer hierarchy with root layer @{}@ has cycles. \
Detected when layer @{}@ was seen in the layer stack for the second time.",
layer_id(root_layer),
layer_id(seen_layer),
),
)),
pcp::Error::InvalidRelocate {
source_path,
target_path,
layer,
reason,
} => Some((
None,
format!(
"Relocation from <{source_path}> to <{target_path}> authored at @{}@</> is invalid and will be ignored: \
{reason}",
layer_id(layer),
),
)),
pcp::Error::ArcCycle(info) => {
let mut msg = format!("Cycle detected:\n@{}@<{}>", layer_id(&info.root_layer), info.composing);
let last = info.hops.len() - 1;
for (i, hop) in info.hops.iter().enumerate() {
let prefix = if i == 0 { "" } else { "which " };
let verb = if i == last {
format!("CANNOT {}", arc_cannot_verb(hop.arc))
} else {
arc_present_verb(hop.arc).to_string()
};
msg.push_str(&format!("\n{prefix}{verb}:\n@{}@<{}>", layer_id(&hop.layer), hop.path));
}
msg.push('\n');
Some((Some(&info.composing), msg))
}
pcp::Error::SameTargetRelocations { target, sources } => {
let list = sources
.iter()
.map(|(s, l)| format!("relocation from <{s}> authored at @{}@</>", layer_id(l)))
.collect::<Vec<_>>()
.join("; ");
Some((
None,
format!(
"The path <{}> is the target of multiple relocations from different sources. \
The following relocates to this target are invalid and will be ignored: {list}.",
target,
),
))
}
pcp::Error::ConflictingRelocation {
source_path,
target_path,
layer,
other_source_path,
other_target_path,
other_layer,
reason,
} => Some((
None,
format!(
"Relocation from <{}> to <{}> authored at @{}@</> conflicts with another relocation \
from <{}> to <{}> authored at @{}@</> and will be ignored: {}",
source_path,
target_path,
layer_id(layer),
other_source_path,
other_target_path,
layer_id(other_layer),
reason,
),
)),
_ => None,
})
.collect();
if entries.is_empty() {
return String::new();
}
let mut per_prim: Vec<(sdf::Path, Vec<String>)> = Vec::new();
let mut layer_stack: Vec<String> = Vec::new();
for (site, msg) in entries {
match site {
Some(path) => match per_prim.iter_mut().find(|(p, _)| p == path) {
Some((_, msgs)) => msgs.push(msg),
None => per_prim.push((path.clone(), vec![msg])),
},
None => layer_stack.push(msg),
}
}
let order_index = |p: &sdf::Path| order.iter().position(|q| q == p).unwrap_or(usize::MAX);
per_prim.sort_by_key(|(p, _)| order_index(p));
let mut sections: Vec<(String, Vec<String>)> = per_prim
.into_iter()
.map(|(path, msgs)| (format!("Errors while composing <{path}>"), msgs))
.collect();
if !layer_stack.is_empty() {
sections.push(("Errors while computing Layer Stack".to_string(), layer_stack));
}
let mut out = String::new();
out.push('\n');
for (header, msgs) in §ions {
let _ = writeln!(out, "{}", separator());
let _ = writeln!(out, "{header}");
out.push('\n');
for msg in msgs {
let _ = writeln!(out, "{msg}");
}
out.push('\n');
}
let _ = writeln!(out, "ERROR: Unexpected error(s) encountered during test!");
out
}
}
fn assert_dump_matches(name: &str, format: Format, test_dir: &Path, baseline: &pcp_json::Baseline, entry: &Path) {
let expected = std::fs::read_to_string(test_dir.join("pcp.txt"))
.expect("read pcp.txt")
.replace("\r\n", "\n");
let stage = usd::Stage::builder()
.variant_fallbacks(variant_fallbacks())
.open(entry.to_str().unwrap())
.unwrap();
let base = entry.parent().expect("entry has a parent directory");
let mut actual = pcp_txt::pcp_dump(name, &baseline.entry, base, &stage);
let canonical_base = std::fs::canonicalize(base).ok();
let mut order = Vec::new();
stage
.traverse(usd::PrimPredicate::ALL, |p| order.push(p.clone()))
.unwrap();
actual.push_str(&pcp_txt::error_trailer(
name,
&stage.composition_errors(),
canonical_base.as_deref(),
&order,
));
if actual != expected {
let dir = Path::new("target").join("pcp_out");
let _ = std::fs::create_dir_all(&dir);
let actual_path = dir.join(format!("{name}.{format}.actual.txt"));
let _ = std::fs::write(&actual_path, &actual);
panic!(
"composition pcp mismatch for {name} ({format})\n wrote {}\n{}",
actual_path.display(),
pcp_txt::first_diff_lines(&actual, &expected, 6),
);
}
}
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 cycle_termination {
use super::*;
fn open(asset: &str) -> usd::Stage {
let entry = Path::new(ASSETS).join(asset).join("usda").join("root.usd");
open_stage(&entry)
}
#[test]
fn error_arc_cycle_terminates() {
let stage = open("ErrorArcCycle_root");
let mut count = 0;
stage.traverse(usd::PrimPredicate::ALL, |_| count += 1).unwrap();
assert!(count > 0, "traversal composed at least one prim");
let group_root = stage.prim_at(sdf::path("/GroupRoot").unwrap());
assert_eq!(group_root.child_names().unwrap(), vec!["ChildA"]);
let yap_child = stage.prim_at(sdf::path("/YetAnotherParent/Child").unwrap());
assert!(yap_child.child_names().unwrap().is_empty());
let corec = stage.prim_at(sdf::path("/CoRecursiveParent1/Child1/Child2").unwrap());
assert!(corec.child_names().unwrap().is_empty());
}
#[test]
fn bug92827_invalid_relocate_dropped() {
let stage = open("bug92827_root");
let a = stage.prim_at(sdf::path("/Rig/Other/A").unwrap());
let (children, prohibited) = a.prim_index().child_names().unwrap();
assert_eq!(children, vec!["Instance"]);
assert_eq!(prohibited, vec!["B"]);
}
}
#[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_at(sdf::path("/Root").unwrap()).child_names().unwrap();
assert_eq!(children, vec!["C", "D", "A", "B"]);
}
#[test]
fn property_order_ignored_in_usd_mode() {
let stage = open_fixture();
let props = stage.prim_at(sdf::path("/Props").unwrap()).property_names().unwrap();
assert_eq!(props, vec!["x", "y", "z"]);
}
}
#[cfg(test)]
mod value_resolution {
use std::collections::HashMap;
use super::*;
use openusd::sdf::{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 = stage.prim_at(sdf::path("/InheritOnly").unwrap()).specifier().unwrap();
assert_eq!(value, Some(Specifier::Class));
}
#[test]
fn specifier_all_over_resolves_to_over() {
let stage = open_fixture();
let value = stage.prim_at(sdf::path("/AllOver").unwrap()).specifier().unwrap();
assert_eq!(value, Some(Specifier::Over));
}
#[test]
fn specifier_def_resolves_to_def() {
let stage = open_fixture();
let value = stage.prim_at(sdf::path("/DefPrim").unwrap()).specifier().unwrap();
assert_eq!(value, Some(Specifier::Def));
}
#[test]
fn variability_weakest_opinion_wins() {
let stage = open_fixture();
let value = stage
.prim_at(sdf::path("/VarTest").unwrap())
.attribute("attr")
.variability()
.unwrap();
assert_eq!(value, Some(Variability::Uniform));
}
#[test]
fn custom_any_true() {
let stage = open_fixture();
let value = stage
.prim_at(sdf::path("/CustomTest").unwrap())
.attribute("attr")
.is_custom()
.unwrap();
assert!(value);
}
#[test]
fn dictionary_values_compose_recursively() {
let stage = open_fixture();
let value = stage.prim_at(sdf::path("/DictTest").unwrap()).custom_data().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 = stage.custom_layer_data().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"));
}
}