use std::borrow::Cow;
use std::cell::{Cell, RefCell};
use std::collections::HashMap;
use std::fs;
use std::path::Path as FsPath;
use std::rc::Rc;
use std::sync::Arc;
use openusd::Result;
use openusd::ar::Resolver as _;
use openusd::usd::{
CommittedChange, EditTarget, EditTargetArc, InitialLoadSet, LoadPolicy, PrimPredicate, PrimStatus, Stage,
StageAuthoringError, StagePopulationMask, StageSink,
};
use openusd::usdz::ArchiveWriter;
use openusd::{ar, gf, kind, pcp, sdf, tf, usd};
#[derive(Default)]
#[allow(clippy::type_complexity)]
struct RecordingSink {
after: Option<Box<dyn Fn(&Stage, &CommittedChange<'_>)>>,
edit_target: Option<Box<dyn Fn(&Stage)>>,
muting: Option<Box<dyn Fn(&Stage, &str, bool, &[sdf::Path])>>,
load_rules: Option<Box<dyn Fn(&Stage, &[sdf::Path])>>,
}
impl StageSink for RecordingSink {
fn after_commit(&self, stage: &Stage, change: &CommittedChange<'_>) {
if let Some(f) = &self.after {
f(stage, change);
}
}
fn edit_target_changed(&self, stage: &Stage) {
if let Some(f) = &self.edit_target {
f(stage);
}
}
fn layer_muting_changed(&self, stage: &Stage, layer: &str, muted: bool, resynced: &[sdf::Path]) {
if let Some(f) = &self.muting {
f(stage, layer, muted, resynced);
}
}
fn load_rules_changed(&self, stage: &Stage, resynced: &[sdf::Path]) {
if let Some(f) = &self.load_rules {
f(stage, resynced);
}
}
}
#[allow(clippy::type_complexity)]
#[derive(Default)]
struct RecordingLayerSink {
before: Option<Box<dyn Fn(&sdf::PendingLayerChange<'_>) -> Result<(), sdf::sink::Error>>>,
after: Option<Box<dyn Fn(&str, &sdf::ChangeList)>>,
}
impl sdf::LayerSink for RecordingLayerSink {
fn before_commit(&self, change: &sdf::PendingLayerChange<'_>) -> Result<(), sdf::sink::Error> {
match &self.before {
Some(f) => f(change),
None => Ok(()),
}
}
fn after_commit(&self, layer: &str, changes: &sdf::ChangeList) {
if let Some(f) = &self.after {
f(layer, changes);
}
}
}
const VENDOR_COMPOSITION: &str = concat!(
env!("CARGO_WORKSPACE_DIR"),
"vendor/usd-wg-assets/test_assets/foundation/stage_composition"
);
fn manifest_dir() -> String {
std::env::var("CARGO_MANIFEST_DIR").unwrap()
}
fn composition_path(relative: &str) -> String {
format!("{VENDOR_COMPOSITION}/{relative}")
}
fn fixture_path(relative: &str) -> String {
format!("{}/fixtures/{relative}", manifest_dir())
}
fn child_names(stage: &Stage, path: impl sdf::IntoPath) -> Result<Vec<String>> {
Ok(stage.prim(path)?.child_names()?.into_iter().map(String::from).collect())
}
fn open_text(dir: &tempfile::TempDir, text: &str) -> Result<Stage> {
let root = dir.path().join("root.usda");
fs::write(&root, text)?;
Stage::open(root.to_str().unwrap())
}
fn prop_names(stage: &Stage, path: impl sdf::IntoPath) -> Result<Vec<String>> {
Ok(stage
.prim(path)?
.property_names()?
.into_iter()
.map(String::from)
.collect())
}
fn connections(stage: &Stage, attr: &sdf::Path) -> Result<Vec<sdf::Path>> {
stage.attribute(attr)?.connections()
}
fn rel_targets(stage: &Stage, rel: &sdf::Path) -> Result<Vec<sdf::Path>> {
stage.relationship(rel)?.targets()
}
fn fwd_targets(stage: &Stage, rel: &sdf::Path) -> Result<Vec<sdf::Path>> {
stage.relationship(rel)?.forwarded_targets()
}
fn unresolved_sublayer_count(stage: &Stage, asset_path: &str) -> usize {
stage
.composition_errors()
.iter()
.filter(
|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedSublayer { asset_path: a, .. } if a == asset_path),
)
.count()
}
fn reports_unresolved_sublayer(stage: &Stage, asset_path: &str) -> bool {
unresolved_sublayer_count(stage, asset_path) > 0
}
#[derive(Default, Debug)]
struct Notices {
resynced: Vec<sdf::Path>,
changed_info_only: Vec<sdf::Path>,
asset_paths_resynced: Vec<sdf::Path>,
muting: Vec<sdf::Path>,
load_rules: Vec<sdf::Path>,
}
fn capture_notices(stage: &Stage) -> Rc<RefCell<Notices>> {
let notices = Rc::new(RefCell::new(Notices::default()));
let (commit, muting, load_rules) = (notices.clone(), notices.clone(), notices.clone());
stage.add_sink(RecordingSink {
after: Some(Box::new(move |_stage: &Stage, change: &CommittedChange<'_>| {
let mut n = commit.borrow_mut();
n.resynced.extend(change.resynced.iter().cloned());
n.changed_info_only.extend(change.changed_info_only.iter().cloned());
n.asset_paths_resynced
.extend(change.asset_paths_resynced.iter().cloned());
})),
muting: Some(Box::new(
move |_stage: &Stage, _layer: &str, _muted, resynced: &[sdf::Path]| {
muting.borrow_mut().muting.extend(resynced.iter().cloned());
},
)),
load_rules: Some(Box::new(move |_stage: &Stage, resynced: &[sdf::Path]| {
load_rules.borrow_mut().load_rules.extend(resynced.iter().cloned());
})),
..Default::default()
});
notices
}
fn first_prototype(stage: &Stage) -> Result<sdf::Path> {
Ok(stage
.prototypes()?
.first()
.cloned()
.expect("the scene instances no prim"))
}
fn paths(paths: &[&str]) -> Vec<sdf::Path> {
paths.iter().map(|p| sdf::path(p).expect("valid path")).collect()
}
fn identifier_by_leaf(stage: &Stage, leaf: &str) -> String {
stage
.layer_identifiers()
.into_iter()
.find(|id| FsPath::new(id).ends_with(leaf))
.expect("layer is loaded")
}
#[test]
fn internal_target_root_diagnostic() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(&session, "#usda 1.0\n")?;
for field in ["references", "payload"] {
for target in ["/Missing", "/Present/Missing"] {
fs::write(
&root,
format!("#usda 1.0\ndef \"Present\" {{}}\ndef \"Root\" ({field} = <{target}>) {{}}\n"),
)?;
let stage = Stage::builder()
.session_layer(session.to_str().unwrap())
.open(root.to_str().unwrap())?;
assert!(stage.prim("/Root")?.is_valid()?);
let errors = stage.composition_errors();
let target_layer = errors
.iter()
.find_map(|error| match error {
pcp::CompositionDiagnostic::UnresolvedPrimPath { target_layer, .. } => Some(target_layer),
_ => None,
})
.expect("missing target diagnostic");
assert_eq!(target_layer, stage.root_layer().identifier(), "{field} = <{target}>");
}
}
Ok(())
}
#[test]
fn missing_sublayer_retained() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n(\n subLayers = [@missing.usda@]\n)\ndef \"Root\" {}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert!(stage.composition_errors().iter().any(|error| matches!(
error,
pcp::CompositionDiagnostic::UnresolvedSublayer {
asset_path,
introduced_by,
} if asset_path == "missing.usda" && introduced_by.ends_with("root.usda")
)));
assert!(stage.prim("/Root")?.is_valid()?);
Ok(())
}
#[test]
fn muted_branch_suppresses_missing() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let muted = dir.path().join("muted.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@muted.usda@]\n)\n")?;
fs::write(&muted, "#usda 1.0\n(\n subLayers = [@missing.usda@]\n)\n")?;
let root_path = root.to_str().unwrap();
let plain = Stage::open(root_path)?;
assert!(
reports_unresolved_sublayer(&plain, "missing.usda"),
"an unmuted missing sublayer must be reported, got {:?}",
plain.composition_errors()
);
let muted_stage = Stage::builder().mute(["muted.usda"]).open(root_path)?;
assert!(
!reports_unresolved_sublayer(&muted_stage, "missing.usda"),
"a missing sublayer under a muted branch must raise no diagnostic, got {:?}",
muted_stage.composition_errors()
);
Ok(())
}
#[test]
fn muted_missing_sublayer_suppressed() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@gone.usda@]\n)\n")?;
let root_path = root.to_str().unwrap();
let plain = Stage::open(root_path)?;
assert!(
reports_unresolved_sublayer(&plain, "gone.usda"),
"an unmuted missing sublayer is reported"
);
let muted = Stage::builder().mute(["gone.usda"]).open(root_path)?;
assert!(
!reports_unresolved_sublayer(&muted, "gone.usda"),
"muting the missing sublayer suppresses its diagnostic, got {:?}",
muted.composition_errors()
);
Ok(())
}
#[test]
fn muted_diamond_keeps_active() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0\n(\n subLayers = [@muted.usda@, @active.usda@]\n)\n",
)?;
fs::write(
dir.path().join("muted.usda"),
"#usda 1.0\n(\n subLayers = [@shared_missing.usda@]\n)\n",
)?;
fs::write(
dir.path().join("active.usda"),
"#usda 1.0\n(\n subLayers = [@shared_missing.usda@]\n)\n",
)?;
let root_path = dir.path().join("root.usda");
let root_path = root_path.to_str().unwrap();
let stage = Stage::builder().mute(["muted.usda"]).open(root_path)?;
assert_eq!(
unresolved_sublayer_count(&stage, "shared_missing.usda"),
1,
"the unmuted branch's missing sublayer must be reported exactly once, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn muted_diamond_keeps_descendant() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0\n(\n subLayers = [@muted.usda@, @active.usda@]\n)\n",
)?;
fs::write(
dir.path().join("muted.usda"),
"#usda 1.0\n(\n subLayers = [@shared.usda@]\n)\n",
)?;
fs::write(
dir.path().join("active.usda"),
"#usda 1.0\n(\n subLayers = [@shared.usda@]\n)\n",
)?;
fs::write(
dir.path().join("shared.usda"),
"#usda 1.0\n(\n subLayers = [@missing.usda@]\n)\n",
)?;
let root_path = dir.path().join("root.usda");
let root_path = root_path.to_str().unwrap();
let stage = Stage::builder().mute(["muted.usda"]).open(root_path)?;
assert!(
reports_unresolved_sublayer(&stage, "missing.usda"),
"the shared layer is reachable through the unmuted branch, so its missing sublayer must be reported, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn unmute_restores_diagnostic() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0\n(\n subLayers = [@muted.usda@]\n)\n",
)?;
fs::write(
dir.path().join("muted.usda"),
"#usda 1.0\n(\n subLayers = [@missing.usda@]\n)\n",
)?;
let root_path = dir.path().join("root.usda");
let root_path = root_path.to_str().unwrap();
let stage = Stage::builder().mute(["muted.usda"]).open(root_path)?;
assert!(
!reports_unresolved_sublayer(&stage, "missing.usda"),
"while muted the missing sublayer must be silent, got {:?}",
stage.composition_errors()
);
stage.unmute_layer("muted.usda");
assert!(
reports_unresolved_sublayer(&stage, "missing.usda"),
"unmuting the branch must restore its missing-sublayer diagnostic, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn eviction_rederives_diagnostic() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0\n(\n subLayers = [@s.usda@]\n)\ndef \"A\" (\n references = @target.usda@\n) {}\n",
)?;
fs::write(
dir.path().join("s.usda"),
"#usda 1.0\nover \"A\" {\n custom int x = 1\n}\n",
)?;
fs::write(
dir.path().join("target.usda"),
"#usda 1.0\n(\n subLayers = [@missing.usda@]\n defaultPrim = \"T\"\n)\ndef \"T\" {}\n",
)?;
let root_path = dir.path().join("root.usda");
let root_path = root_path.to_str().unwrap();
let stage = Stage::open(root_path)?;
let _ = child_names(&stage, "/A")?;
assert!(
reports_unresolved_sublayer(&stage, "missing.usda"),
"the reached target's missing sublayer is reported"
);
stage.mute_layer("s.usda");
assert!(
!reports_unresolved_sublayer(&stage, "missing.usda"),
"the orphaned target's diagnostic retires with its stack, got {:?}",
stage.composition_errors()
);
let _ = child_names(&stage, "/A")?;
assert!(
reports_unresolved_sublayer(&stage, "missing.usda"),
"recomposition re-derives the diagnostic, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn mute_loaded_target_suppresses() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0\ndef \"A\" (\n references = @target.usda@\n) {}\n",
)?;
fs::write(
dir.path().join("target.usda"),
"#usda 1.0\n(\n subLayers = [@missing.usda@]\n defaultPrim = \"T\"\n)\ndef \"T\" {}\n",
)?;
let root_path = dir.path().join("root.usda");
let root_path = root_path.to_str().unwrap();
let stage = Stage::open(root_path)?;
let _ = child_names(&stage, "/A")?;
assert!(
reports_unresolved_sublayer(&stage, "missing.usda"),
"the loaded target's missing sublayer is reported"
);
stage.mute_layer("target.usda");
let _ = child_names(&stage, "/A")?;
assert!(
!reports_unresolved_sublayer(&stage, "missing.usda"),
"muting the target suppresses its own sublayer diagnostic, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn duplicate_missing_reported_once() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0\n(\n subLayers = [@missing.usda@, @missing.usda@]\n)\n",
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().unwrap())?;
assert_eq!(
unresolved_sublayer_count(&stage, "missing.usda"),
1,
"a duplicate missing sublayer is reported once, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn lazy_ref_missing_sublayer() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let target = dir.path().join("target.usda");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @target.usda@\n) {}\n")?;
fs::write(
&target,
"#usda 1.0\n(\n subLayers = [@missing.usda@]\n defaultPrim = \"P\"\n)\ndef \"P\" {\n custom double x = 1\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(1.0)),
"the reference target loads despite its missing sublayer"
);
assert!(
stage.composition_errors().iter().any(|error| matches!(
error,
pcp::CompositionDiagnostic::UnresolvedSublayer { asset_path, introduced_by }
if asset_path == "missing.usda" && introduced_by.ends_with("target.usda")
)),
"expected UnresolvedSublayer, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn lazy_ref_unreadable_target() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let target = dir.path().join("broken.usda");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @broken.usda@\n) {}\n")?;
fs::write(&target, "#usda 1.0\ndef Broken {{{ not valid\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
assert!(stage.prim("/P")?.is_valid()?, "/P still composes without the arc");
assert!(
stage.composition_errors().iter().any(|error| matches!(
error,
pcp::CompositionDiagnostic::MalformedLayer { asset_path, reason, .. }
if asset_path.contains("broken.usda") && !reason.is_empty()
)),
"expected MalformedLayer carrying the parse error, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn failed_load_retried_after_edit() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let target = dir.path().join("target.usda");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @target.usda@\n) {}\n")?;
fs::write(&target, "#usda 1.0\ndef Broken {{{ not valid\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
assert!(stage.prim("/P")?.is_valid()?);
assert!(
stage.composition_errors().iter().any(|e| matches!(
e,
pcp::CompositionDiagnostic::MalformedLayer { asset_path, .. } if asset_path.contains("target.usda")
)),
"the unreadable target is reported malformed"
);
fs::write(
&target,
"#usda 1.0\n(\n defaultPrim = \"P\"\n)\ndef \"P\" {\n custom double x = 7\n}\n",
)?;
stage.define_prim("/Trigger")?;
assert_eq!(
stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(7.0)),
"the repaired reference composes once the failure is cleared"
);
Ok(())
}
#[test]
fn instance_proxy_cold_query() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let proto = dir.path().join("proto.usda");
fs::write(
&root,
"#usda 1.0\ndef \"World\" {\n def \"Inst\" (\n instanceable = true\n references = @proto.usda@\n ) {}\n}\n",
)?;
fs::write(
&proto,
"#usda 1.0\n(\n defaultPrim = \"Proto\"\n)\ndef \"Proto\" {\n def \"Child\" {\n custom double x = 3\n }\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage
.attribute("/World/Inst/Child.x")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(3.0))
);
Ok(())
}
#[test]
fn lazy_ref_inherited_expr_var() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir(dir.path().join("prod"))?;
let root = dir.path().join("root.usda");
let target = dir.path().join("target.usda");
let over = dir.path().join("prod").join("over.usda");
fs::write(
&root,
"#usda 1.0\n(\n expressionVariables = { string V = \"prod\" }\n)\ndef \"P\" (\n references = @target.usda@\n) {}\n",
)?;
fs::write(
&target,
"#usda 1.0\n(\n defaultPrim = \"P\"\n subLayers = [@`\"${V}/over.usda\"`@]\n)\ndef \"P\" {}\n",
)?;
fs::write(&over, "#usda 1.0\ndef \"P\" {\n custom double x = 9\n}\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(9.0)),
"the target's `${{V}}` sublayer resolves against the referrer's variable"
);
Ok(())
}
#[test]
fn expr_sublayer_composes() -> Result<()> {
let stage = Stage::open(&fixture_path("expr_sublayer.usda"))?;
assert_eq!(stage.layer_count(), 2, "root + the expression-resolved sublayer");
assert_eq!(
stage.root_prims()?.iter().map(|t| t.as_str()).collect::<Vec<_>>(),
["World"],
"the expression sublayer's prim composes onto the stage"
);
Ok(())
}
#[test]
fn sublayer_expr_var_ignored() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let a = dir.path().join("a.usda");
let b = dir.path().join("b.usda");
let leaf = dir.path().join("leaf.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@a.usda@]\n)\n")?;
fs::write(
&a,
"#usda 1.0\n(\n expressionVariables = { string V = \"leaf\" }\n subLayers = [@b.usda@]\n)\n",
)?;
fs::write(&b, "#usda 1.0\n(\n subLayers = [@`\"${V}.usda\"`@]\n)\n")?;
fs::write(&leaf, "#usda 1.0\ndef \"P\" {\n custom double x = 7\n}\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
None,
"`a` is a sublayer, so its `V` is ignored and `b`'s expression sublayer does not resolve"
);
Ok(())
}
#[test]
fn cross_ref_expr_sublayer() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let mid = dir.path().join("mid.usda");
let target = dir.path().join("target.usda");
let over = dir.path().join("over.usda");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @mid.usda@\n) {}\n")?;
fs::write(
&mid,
"#usda 1.0\n(\n defaultPrim = \"P\"\n expressionVariables = { string V = \"over\" }\n)\ndef \"P\" (\n references = @target.usda@\n) {}\n",
)?;
fs::write(
&target,
"#usda 1.0\n(\n defaultPrim = \"P\"\n subLayers = [@`\"${V}.usda\"`@]\n)\ndef \"P\" {}\n",
)?;
fs::write(&over, "#usda 1.0\ndef \"P\" {\n custom double x = 9\n}\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(9.0)),
"the referrer's variable resolves the target's `${{V}}` sublayer"
);
Ok(())
}
#[test]
fn dual_context_same_pass() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\ndef \"M\" (\n references = [@s1.usda@, @s2.usda@]\n) {}\n",
)?;
for (name, sel) in [("s1.usda", "x"), ("s2.usda", "y")] {
fs::write(
dir.path().join(name),
format!(
"#usda 1.0\n(\n defaultPrim = \"P\"\n expressionVariables = {{ string V = \"{sel}\" }}\n)\ndef \"P\" (\n references = @t.usda@\n) {{}}\n",
),
)?;
}
fs::write(
dir.path().join("t.usda"),
"#usda 1.0\n(\n defaultPrim = \"P\"\n subLayers = [@`\"${V}.usda\"`@]\n)\ndef \"P\" {}\n",
)?;
fs::write(
dir.path().join("x.usda"),
"#usda 1.0\ndef \"P\" {\n custom double vx = 1\n}\n",
)?;
fs::write(
dir.path().join("y.usda"),
"#usda 1.0\ndef \"P\" {\n custom double vy = 2\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage
.attribute("/M.vx")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(1.0)),
"s1's V=x resolves the target's sublayer under the first arc"
);
assert_eq!(
stage
.attribute("/M.vy")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(2.0)),
"s2's V=y resolves the target's sublayer under the second arc"
);
Ok(())
}
#[test]
fn session_var_edit_loads() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@`\"${WHICH}.usda\"`@]\n)\n")?;
fs::write(
&session,
"#usda 1.0\n(\n expressionVariables = { string WHICH = \"a\" }\n)\n",
)?;
fs::write(
dir.path().join("a.usda"),
"#usda 1.0\ndef \"A\" {\n custom double x = 1\n}\n",
)?;
fs::write(
dir.path().join("b.usda"),
"#usda 1.0\ndef \"B\" {\n custom double y = 2\n}\n",
)?;
let stage = Stage::builder()
.session_layer(session.to_str().unwrap())
.open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/A.x")?.get::<f64>()?, Some(1.0), "WHICH=a at open");
assert_eq!(stage.attribute("/B.y")?.get::<f64>()?, None, "b.usda is not loaded");
let session_id = stage.session_layer().expect("session layer").identifier().to_string();
stage.layer_mut(&session_id).expect("session layer is live").edit(|e| {
e.set_expression_variables(HashMap::from([(
"WHICH".to_string(),
sdf::Value::String("b".to_string()),
)]))
})?;
assert_eq!(
stage.attribute("/B.y")?.get::<f64>()?,
Some(2.0),
"the edit loads the newly selected b.usda"
);
assert_eq!(
stage.attribute("/A.x")?.get::<f64>()?,
None,
"a.usda's selection dropped"
);
Ok(())
}
#[test]
fn target_var_edit_loads() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @t.usda@\n) {}\n")?;
fs::write(
dir.path().join("t.usda"),
"#usda 1.0\n(\n defaultPrim = \"P\"\n expressionVariables = { string V = \"a\" }\n subLayers = [@`\"${V}.usda\"`@]\n)\ndef \"P\" {}\n",
)?;
fs::write(
dir.path().join("a.usda"),
"#usda 1.0\ndef \"P\" {\n custom double x = 1\n}\n",
)?;
fs::write(
dir.path().join("b.usda"),
"#usda 1.0\ndef \"P\" {\n custom double y = 2\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/P.x")?.get::<f64>()?, Some(1.0), "V=a selects a.usda");
let target_id = stage
.layer_identifiers()
.into_iter()
.find(|id| FsPath::new(id).ends_with("t.usda"))
.expect("t.usda is loaded");
stage.layer_mut(&target_id).expect("target layer is live").edit(|e| {
e.set_expression_variables(HashMap::from([("V".to_string(), sdf::Value::String("b".to_string()))]))
})?;
assert_eq!(
stage.attribute("/P.y")?.get::<f64>()?,
Some(2.0),
"the edit loads the newly selected b.usda into the target stack"
);
Ok(())
}
#[test]
fn vars_edit_notifies_assets() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
(
expressionVariables = { string FREE = "x" }
)
def "Other" {
custom double o = 1
}
"#,
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/Other.o")?.get::<f64>()?, Some(1.0));
assert!(stage.is_indexed(&sdf::path("/Other")?));
let notices = capture_notices(&stage);
stage.set_expression_variables(HashMap::from([(
"FREE".to_string(),
sdf::Value::String("y".to_string()),
)]))?;
{
let notices = notices.borrow();
assert!(
notices.resynced.is_empty(),
"no prim recorded the variable, so nothing resynced, got {:?}",
notices.resynced
);
assert_eq!(
notices.asset_paths_resynced,
paths(&["/"]),
"the root stack's asset values may re-resolve stage-wide"
);
assert_eq!(
notices.changed_info_only,
paths(&["/"]),
"the edit is an ordinary layer-metadata info change on the pseudo-root"
);
}
assert!(
stage.is_indexed(&sdf::path("/Other")?),
"no prim recorded the variable, so its index survives the edit"
);
Ok(())
}
#[test]
fn sublayer_vars_no_notice() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@sub.usda@]\n)\n")?;
fs::write(
dir.path().join("sub.usda"),
"#usda 1.0\ndef \"P\" {\n custom double x = 1\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/P.x")?.get::<f64>()?, Some(1.0));
assert!(stage.is_indexed(&sdf::path("/P")?));
let notices = capture_notices(&stage);
let sub_id = identifier_by_leaf(&stage, "sub.usda");
stage.layer_mut(&sub_id).expect("sublayer is live").edit(|e| {
e.set_expression_variables(HashMap::from([("V".to_string(), sdf::Value::String("x".to_string()))]))
})?;
assert!(
stage.is_indexed(&sdf::path("/P")?),
"no composed variable changed, so the index survives"
);
let notices = notices.borrow();
assert!(
notices.resynced.is_empty() && notices.asset_paths_resynced.is_empty(),
"a vars edit that changed no composed set publishes nothing, got {notices:?}"
);
Ok(())
}
#[test]
fn target_vars_notify_referrer() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "Ref" (
references = @t.usda@
) {}
def "Other" {
custom double o = 1
}
"#,
)?;
fs::write(
dir.path().join("t.usda"),
r#"#usda 1.0
(
defaultPrim = "P"
expressionVariables = { string NAME = "a" }
)
def "P" {
asset inputs:file = @`"./${NAME}.png"`@
}
"#,
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let evaluated = |stage: &Stage| -> Result<Option<String>> {
Ok(stage
.attribute("/Ref.inputs:file")?
.get::<sdf::AssetPath>()?
.and_then(|a| a.evaluated_path().map(str::to_string)))
};
assert_eq!(evaluated(&stage)?.as_deref(), Some("./a.png"));
assert_eq!(stage.attribute("/Other.o")?.get::<f64>()?, Some(1.0));
let notices = capture_notices(&stage);
let target_id = identifier_by_leaf(&stage, "t.usda");
stage.layer_mut(&target_id).expect("target layer is live").edit(|e| {
e.set_expression_variables(HashMap::from([(
"NAME".to_string(),
sdf::Value::String("b".to_string()),
)]))
})?;
assert_eq!(
evaluated(&stage)?.as_deref(),
Some("./b.png"),
"the value-time expression re-evaluates against the edited variables"
);
{
let notices = notices.borrow();
assert_eq!(
notices.asset_paths_resynced,
paths(&["/Ref"]),
"only the prims composing against the edited stack may re-resolve"
);
assert!(
notices.resynced.is_empty(),
"no prim read the variable during composition, got {:?}",
notices.resynced
);
}
Ok(())
}
#[test]
fn instance_vars_notify_prototype() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "A" (
references = @t.usda@
instanceable = true
) {}
def "B" (
references = @t.usda@
instanceable = true
) {}
"#,
)?;
fs::write(
dir.path().join("t.usda"),
r#"#usda 1.0
(
defaultPrim = "P"
expressionVariables = { string NAME = "a" }
)
def "P" {
def "Inner" { custom double y = 2 }
def "Nested" (
references = @u.usda@
instanceable = true
) {}
}
"#,
)?;
fs::write(
dir.path().join("u.usda"),
r#"#usda 1.0
(
defaultPrim = "Q"
)
def "Q" {
custom double z = 3
}
"#,
)?;
let edit_target_vars = |stage: &Stage| -> Result<()> {
let target_id = identifier_by_leaf(stage, "t.usda");
stage.layer_mut(&target_id).expect("target layer is live").edit(|e| {
e.set_expression_variables(HashMap::from([(
"NAME".to_string(),
sdf::Value::String("b".to_string()),
)]))
})?;
Ok(())
};
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/A/Inner.y")?.get::<f64>()?, Some(2.0));
assert_eq!(stage.attribute("/B/Inner.y")?.get::<f64>()?, Some(2.0));
assert_eq!(stage.attribute("/A/Nested.z")?.get::<f64>()?, Some(3.0));
let notices = capture_notices(&stage);
edit_target_vars(&stage)?;
assert!(stage.prim("/A")?.is_valid()?);
assert_eq!(
notices.borrow().asset_paths_resynced,
paths(&["/A", "/B", "/__Prototype_0"]),
"both instance roots and the shared prototype compose against the edited stack; \
the nested prototype holds only u.usda content, so it is untouched"
);
let cold = Stage::open(root.to_str().unwrap())?;
assert!(cold.prim("/A")?.is_valid()?);
assert!(cold.prim("/B")?.is_valid()?);
let cold_notices = capture_notices(&cold);
edit_target_vars(&cold)?;
assert!(cold.prim("/A")?.is_valid()?);
assert_eq!(cold_notices.borrow().asset_paths_resynced, paths(&["/A", "/B"]));
Ok(())
}
#[test]
fn session_var_swap_loads() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(&root, "#usda 1.0\n")?;
fs::write(
&session,
"#usda 1.0\n(\n expressionVariables = { string S = \"sa\" }\n subLayers = [@`\"${S}.usda\"`@]\n)\n",
)?;
fs::write(
dir.path().join("sa.usda"),
"#usda 1.0\ndef \"SA\" {\n custom double a = 1\n}\n",
)?;
fs::write(
dir.path().join("sb.usda"),
"#usda 1.0\ndef \"SB\" {\n custom double b = 2\n}\n",
)?;
let stage = Stage::builder()
.session_layer(session.to_str().unwrap())
.open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/SA.a")?.get::<f64>()?, Some(1.0), "S=sa at open");
assert_eq!(stage.attribute("/SB.b")?.get::<f64>()?, None, "sb.usda is not loaded");
let session_id = stage.session_layer().expect("session layer").identifier().to_string();
stage.layer_mut(&session_id).expect("session layer is live").edit(|e| {
e.set_expression_variables(HashMap::from([("S".to_string(), sdf::Value::String("sb".to_string()))]))
})?;
assert_eq!(
stage.attribute("/SB.b")?.get::<f64>()?,
Some(2.0),
"the edit loads the newly selected session sublayer"
);
assert_eq!(
stage.attribute("/SA.a")?.get::<f64>()?,
None,
"the old selection drops out of the session region"
);
Ok(())
}
#[test]
fn session_insert_layer() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(&root, "#usda 1.0\n")?;
fs::write(&session, "#usda 1.0\n")?;
let stage = Stage::builder()
.session_layer(session.to_str().unwrap())
.open(root.to_str().unwrap())?;
let session_id = stage.session_layer().expect("session layer").identifier().to_string();
let extra = opinion_layer("extra.usda", 7.0)?;
stage.insert_layer(&session_id, 0, extra, sdf::LayerOffset::IDENTITY)?;
assert_eq!(
stage.attribute("/A.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(7.0)),
"the inserted session sublayer's opinion composes"
);
Ok(())
}
#[test]
fn session_missing_heals() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(&root, "#usda 1.0\n")?;
fs::write(&session, "#usda 1.0\n(\n subLayers = [@late.usda@]\n)\n")?;
let stage = Stage::builder()
.session_layer(session.to_str().unwrap())
.open(root.to_str().unwrap())?;
let errors = stage.composition_errors();
assert_eq!(
errors.len(),
1,
"one missing session sublayer, one diagnostic: {errors:?}"
);
assert!(matches!(
&errors[0],
pcp::CompositionDiagnostic::UnresolvedSublayer { .. }
));
fs::write(
dir.path().join("late.usda"),
"#usda 1.0\ndef \"L\" {\n custom double x = 4\n}\n",
)?;
stage.define_prim("/Poke")?;
assert_eq!(
stage.attribute("/L.x")?.get::<f64>()?,
Some(4.0),
"the repaired session sublayer loads and composes"
);
assert!(
stage.composition_errors().is_empty(),
"the healed entry stops reporting, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn mute_exposes_selection_loads() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(
&root,
"#usda 1.0\n(\n expressionVariables = { string WHICH = \"b\" }\n subLayers = [@`\"${WHICH}.usda\"`@]\n)\n",
)?;
fs::write(
&session,
"#usda 1.0\n(\n expressionVariables = { string WHICH = \"a\" }\n)\n",
)?;
fs::write(
dir.path().join("a.usda"),
"#usda 1.0\ndef \"A\" {\n custom double x = 1\n}\n",
)?;
fs::write(
dir.path().join("b.usda"),
"#usda 1.0\ndef \"B\" {\n custom double y = 2\n}\n",
)?;
let stage = Stage::builder()
.session_layer(session.to_str().unwrap())
.open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/A.x")?.get::<f64>()?,
Some(1.0),
"the session's WHICH=a wins"
);
assert_eq!(stage.attribute("/B.y")?.get::<f64>()?, None, "b.usda is not loaded");
let session_id = stage.session_layer().expect("session layer").identifier().to_string();
assert!(stage.is_indexed(&sdf::path("/A")?), "reading /A.x composed its index");
let notices = capture_notices(&stage);
stage.mute_layer(session_id);
assert_eq!(notices.borrow().muting, paths(&["/A", "/B"]));
assert!(!stage.is_indexed(&sdf::path("/A")?), "the mute dropped /A's index");
assert_eq!(
stage.attribute("/B.y")?.get::<f64>()?,
Some(2.0),
"muting the session exposes the root's WHICH=b and loads its selection"
);
Ok(())
}
#[test]
fn self_selected_sublayer_loads() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n(\n subLayers = [@t.usda@]\n)\ndef \"R\" (\n references = @t.usda@</P>\n) {}\n",
)?;
fs::write(
dir.path().join("t.usda"),
"#usda 1.0\n(\n expressionVariables = { string V = \"a\" }\n subLayers = [@`\"${V}.usda\"`@]\n)\ndef \"P\" {}\n",
)?;
fs::write(
dir.path().join("a.usda"),
"#usda 1.0\nover \"P\" {\n custom double x = 3\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/R.x")?.get::<f64>()?,
Some(3.0),
"the target's own V selects a.usda for its reference stack"
);
Ok(())
}
#[test]
fn literal_sublayer_edit_loads() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(&root, "#usda 1.0\ndef \"W\" {}\n")?;
fs::write(
dir.path().join("extra.usda"),
"#usda 1.0\n(\n subLayers = [@nested.usda@]\n)\ndef \"E\" {\n custom double x = 1\n}\n",
)?;
fs::write(
dir.path().join("nested.usda"),
"#usda 1.0\ndef \"N\" {\n custom double y = 2\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let root_id = stage.root_layer().identifier().to_string();
stage.layer_mut(&root_id).expect("root layer is live").edit(|e| {
e.pseudo_root_mut()
.expect("pseudo-root")
.insert_sublayer(0, "extra.usda", sdf::LayerOffset::IDENTITY);
Ok(())
})?;
assert_eq!(
stage.attribute("/E.x")?.get::<f64>()?,
Some(1.0),
"the authored literal sublayer loads"
);
assert_eq!(
stage.attribute("/N.y")?.get::<f64>()?,
Some(2.0),
"its nested sublayer loads with it"
);
Ok(())
}
#[test]
fn failed_selection_terminates() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n(\n subLayers = [@`\"${WHICH}.usda\"`@]\n)\ndef \"W\" {\n custom double w = 0\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
stage.set_expression_variables(HashMap::from([(
"WHICH".to_string(),
sdf::Value::String("missing".to_string()),
)]))?;
assert_eq!(
stage.attribute("/W.w")?.get::<f64>()?,
Some(0.0),
"the stage composes without the missing selection"
);
let errors = stage.composition_errors();
assert!(
errors
.iter()
.any(|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedSublayer { asset_path, .. } if asset_path == "missing.usda")),
"the failed open is reported: {errors:?}"
);
assert_eq!(
stage.attribute("/W.w")?.get::<f64>()?,
Some(0.0),
"the failure is terminal, not re-demanded per query"
);
fs::write(
dir.path().join("late.usda"),
"#usda 1.0\ndef \"L\" {\n custom double z = 5\n}\n",
)?;
stage.set_expression_variables(HashMap::from([(
"WHICH".to_string(),
sdf::Value::String("late".to_string()),
)]))?;
assert_eq!(
stage.attribute("/L.z")?.get::<f64>()?,
Some(5.0),
"the retried selection loads once the edit re-demands it"
);
let errors = stage.composition_errors();
assert!(
!errors
.iter()
.any(|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedSublayer { asset_path, .. } if asset_path == "missing.usda")),
"the obsolete failure is dropped once the selection changes: {errors:?}"
);
Ok(())
}
#[test]
fn shared_missing_per_referrer() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@p1.usda@, @p2.usda@]\n)\n")?;
fs::write(dir.path().join("p1.usda"), "#usda 1.0\ndef \"A\" {}\n")?;
fs::write(dir.path().join("p2.usda"), "#usda 1.0\ndef \"B\" {}\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
let layer_named = |name: &str| {
stage
.layer_identifiers()
.into_iter()
.find(|id| FsPath::new(id).ends_with(name))
.expect("sublayer is loaded")
};
for name in ["p1.usda", "p2.usda"] {
stage.layer_mut(&layer_named(name)).expect("layer is live").edit(|e| {
e.pseudo_root_mut().expect("pseudo-root").insert_sublayer(
0,
"shared_missing.usda",
sdf::LayerOffset::IDENTITY,
);
Ok(())
})?;
}
let referrers = |stage: &Stage| -> Vec<String> {
stage
.composition_errors()
.into_iter()
.filter_map(|e| match e {
pcp::CompositionDiagnostic::UnresolvedSublayer {
asset_path,
introduced_by,
} if asset_path == "shared_missing.usda" => Some(introduced_by),
_ => None,
})
.collect()
};
let both = referrers(&stage);
assert_eq!(both.len(), 2, "one diagnostic per referrer: {both:?}");
stage.mute_layer(layer_named("p1.usda"));
let remaining = referrers(&stage);
assert_eq!(
remaining.len(),
1,
"the unmuted referrer keeps its diagnostic: {remaining:?}"
);
assert!(
FsPath::new(&remaining[0]).ends_with("p2.usda"),
"the surviving diagnostic names the unmuted referrer: {remaining:?}"
);
Ok(())
}
#[test]
fn same_round_shared_selection() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n(\n expressionVariables = { string V = \"a\" }\n)\ndef \"P1\" (\n references = @t1.usda@</P>\n) {}\ndef \"P2\" (\n references = @t2.usda@</P>\n) {}\n",
)?;
for name in ["t1.usda", "t2.usda"] {
fs::write(
dir.path().join(name),
"#usda 1.0\n(\n subLayers = [@`\"${V}.usda\"`@]\n)\ndef \"P\" {}\n",
)?;
}
fs::write(
dir.path().join("a.usda"),
"#usda 1.0\nover \"P\" {\n custom double x = 1\n}\n",
)?;
fs::write(
dir.path().join("shared.usda"),
"#usda 1.0\nover \"P\" {\n custom double y = 7\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/P1.x")?.get::<f64>()?, Some(1.0));
assert_eq!(stage.attribute("/P2.x")?.get::<f64>()?, Some(1.0));
stage.set_expression_variables(HashMap::from([(
"V".to_string(),
sdf::Value::String("shared".to_string()),
)]))?;
assert_eq!(
stage.attribute("/P1.y")?.get::<f64>()?,
Some(7.0),
"the stack whose demand opened the layer recomposes"
);
assert_eq!(
stage.attribute("/P2.y")?.get::<f64>()?,
Some(7.0),
"the stack whose demand found the layer interned recomposes too"
);
Ok(())
}
#[test]
fn sublayer_failure_keeps_arc_loadable() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n(\n subLayers = [@late.usda@]\n)\ndef \"P\" (\n references = @late.usda@</L>\n) {}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let errors = stage.composition_errors();
assert!(
errors.iter().any(
|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedSublayer { asset_path, .. } if asset_path == "late.usda")
),
"the missing sublayer is reported at open: {errors:?}"
);
fs::write(
dir.path().join("late.usda"),
"#usda 1.0\ndef \"L\" {\n custom double z = 5\n}\n",
)?;
assert_eq!(
stage.attribute("/P.z")?.get::<f64>()?,
Some(5.0),
"the appeared file loads through the reference with no edit"
);
let errors = stage.composition_errors();
assert!(
!errors
.iter()
.any(|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedSublayer { .. })),
"the healed sublayer diagnostic drops: {errors:?}"
);
Ok(())
}
#[test]
fn repaired_sublayer_reloads() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@late.usda@]\n)\ndef \"W\" {}\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
let errors = stage.composition_errors();
assert!(
errors.iter().any(
|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedSublayer { asset_path, .. } if asset_path == "late.usda")
),
"the missing sublayer is reported at open: {errors:?}"
);
fs::write(
dir.path().join("late.usda"),
"#usda 1.0\ndef \"L\" {\n custom double z = 5\n}\n",
)?;
stage.define_prim("/X")?;
assert_eq!(
stage.attribute("/L.z")?.get::<f64>()?,
Some(5.0),
"the repaired sublayer loads on the next edit"
);
let errors = stage.composition_errors();
assert!(
!errors
.iter()
.any(|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedSublayer { .. })),
"the healed diagnostic drops: {errors:?}"
);
Ok(())
}
#[test]
fn mute_retries_resolvable() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n(\n subLayers = [@other.usda@, @late.usda@]\n)\ndef \"W\" {}\n",
)?;
fs::write(dir.path().join("other.usda"), "#usda 1.0\ndef \"O\" {}\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
assert!(
stage.composition_errors().iter().any(
|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedSublayer { asset_path, .. } if asset_path == "late.usda")
),
"the missing sublayer is reported at open"
);
fs::write(
dir.path().join("late.usda"),
"#usda 1.0\ndef \"L\" {\n custom double z = 5\n}\n",
)?;
let other = stage
.layer_identifiers()
.into_iter()
.find(|id| FsPath::new(id).ends_with("other.usda"))
.expect("other.usda is loaded");
stage.mute_layer(other);
assert_eq!(
stage.attribute("/L.z")?.get::<f64>()?,
Some(5.0),
"the mute's rebuild retries the now-resolvable sublayer"
);
Ok(())
}
#[test]
fn dual_spelling_reports_once() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n(\n subLayers = [@./missing.usda@, @././missing.usda@]\n)\ndef \"W\" {}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let count = |stage: &Stage| {
stage
.composition_errors()
.into_iter()
.filter(
|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedSublayer { asset_path, .. } if asset_path.contains("missing.usda")),
)
.count()
};
assert_eq!(count(&stage), 1, "one canonical failure, one diagnostic at open");
stage.define_prim("/X")?;
assert_eq!(count(&stage), 1, "one diagnostic after the runtime retry");
Ok(())
}
#[test]
fn search_and_file_relative_spellings_differ() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n(\n subLayers = [@missing.usda@, @./missing.usda@]\n)\ndef \"W\" {}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
unresolved_sublayer_count(&stage, "missing.usda"),
1,
"the search path reports"
);
assert_eq!(
unresolved_sublayer_count(&stage, "./missing.usda"),
1,
"the file-relative path reports"
);
fs::write(
dir.path().join("missing.usda"),
"#usda 1.0\ndef \"L\" {\n custom double z = 5\n}\n",
)?;
stage.define_prim("/X")?;
assert_eq!(
stage.attribute("/L.z")?.get::<f64>()?,
Some(5.0),
"the appeared file loads"
);
assert!(
!reports_unresolved_sublayer(&stage, "missing.usda"),
"the search path heals"
);
assert!(
!reports_unresolved_sublayer(&stage, "./missing.usda"),
"the file-relative path heals"
);
let loaded = stage
.layer_identifiers()
.into_iter()
.filter(|id| FsPath::new(id).ends_with("missing.usda"))
.count();
assert_eq!(loaded, 1, "both spellings anchor to the one file beside the root");
Ok(())
}
#[test]
fn expr_failure_reported_once() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@`${WHICH}`@]\n)\ndef \"W\" {}\n")?;
fs::write(
dir.path().join("fix.usda"),
"#usda 1.0\ndef \"F\" {\n custom double q = 3\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let errors = stage.composition_errors();
assert_eq!(errors.len(), 1, "one failing expression, one diagnostic: {errors:?}");
assert!(matches!(
&errors[0],
pcp::CompositionDiagnostic::InvalidExpression { .. }
));
stage.set_expression_variables(HashMap::from([(
"WHICH".to_string(),
sdf::Value::String("fix.usda".to_string()),
)]))?;
assert_eq!(
stage.attribute("/F.q")?.get::<f64>()?,
Some(3.0),
"the fixed expression selects and loads"
);
assert!(
stage.composition_errors().is_empty(),
"the healed expression stops reporting"
);
Ok(())
}
#[test]
fn lazy_ref_inside_usdz_resolves() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let package = dir.path().join("package.usdz");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @package.usdz@\n) {}\n")?;
{
let mut writer = ArchiveWriter::create(&package)?;
writer.add_layer(
"scene.usda",
b"#usda 1.0\n(\n defaultPrim = \"P\"\n)\ndef \"P\" (\n references = @other.usda@\n) {}\n",
)?;
writer.add_layer(
"other.usda",
b"#usda 1.0\n(\n defaultPrim = \"P\"\n)\ndef \"P\" {\n custom int probe = 7\n}\n",
)?;
writer.finish()?;
}
let stage = Stage::open(root.to_str().unwrap())?;
assert!(stage.prim("/P")?.is_valid()?, "/P composes from the package");
assert!(
stage.composition_errors().is_empty(),
"the in-package reference should resolve cleanly, got {:?}",
stage.composition_errors()
);
assert_eq!(
stage
.attribute("/P.probe")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Int(7)),
"the sibling layer's opinion composes through the in-package reference"
);
Ok(())
}
#[test]
fn lazy_ref_inside_usdz_missing() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let package = dir.path().join("package.usdz");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @package.usdz@\n) {}\n")?;
{
let mut writer = ArchiveWriter::create(&package)?;
writer.add_layer(
"scene.usda",
b"#usda 1.0\n(\n defaultPrim = \"P\"\n)\ndef \"P\" (\n references = @other.usda@\n) {}\n",
)?;
writer.finish()?;
}
let stage = Stage::open(root.to_str().unwrap())?;
assert!(
stage.prim("/P")?.is_valid()?,
"/P still composes from the package layer"
);
assert!(
stage.composition_errors().iter().any(|error| matches!(
error,
pcp::CompositionDiagnostic::UnresolvedLayer { asset_path, .. } if asset_path.ends_with("other.usda]")
)),
"expected UnresolvedLayer for the missing in-package target, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn lazy_ref_empty_usdz_malformed() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let package = dir.path().join("empty.usdz");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @empty.usdz@\n) {}\n")?;
ArchiveWriter::create(&package)?.finish()?;
let stage = Stage::open(root.to_str().unwrap())?;
assert!(stage.prim("/P")?.is_valid()?, "/P still composes from its own opinion");
assert!(
stage.composition_errors().iter().any(|error| matches!(
error,
pcp::CompositionDiagnostic::MalformedLayer { asset_path, reason, .. }
if asset_path.ends_with("empty.usdz") && reason.contains("USDZ archive")
)),
"expected MalformedLayer with the package read reason, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn usdz_subdir_first_layer_anchors() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let package = dir.path().join("package.usdz");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @package.usdz@\n) {}\n")?;
{
let mut writer = ArchiveWriter::create(&package)?;
writer.add_layer(
"Scenes/root.usda",
b"#usda 1.0\n(\n defaultPrim = \"P\"\n)\ndef \"P\" (\n references = @other.usda@\n) {}\n",
)?;
writer.add_layer(
"Scenes/other.usda",
b"#usda 1.0\n(\n defaultPrim = \"P\"\n)\ndef \"P\" {\n custom int probe = 9\n}\n",
)?;
writer.finish()?;
}
let stage = Stage::open(root.to_str().unwrap())?;
assert!(
stage.composition_errors().is_empty(),
"the sub-directory in-package reference should resolve cleanly, got {:?}",
stage.composition_errors()
);
assert_eq!(
stage
.attribute("/P.probe")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Int(9)),
"the sibling under Scenes/ composes through the in-package reference"
);
Ok(())
}
#[test]
fn asset_value_usdz_is_package_path() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let package = dir.path().join("model.usdz");
fs::write(&root, "#usda 1.0\ndef \"P\" {\n custom asset a = @model.usdz@\n}\n")?;
{
let mut writer = ArchiveWriter::create(&package)?;
writer.add_layer("root.usda", b"#usda 1.0\ndef \"M\" {}\n")?;
writer.finish()?;
}
let stage = Stage::open(root.to_str().unwrap())?;
let value = stage
.attribute("/P.a")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?
.expect("asset value resolves");
let asset = value.try_as_asset_path().expect("attribute is asset-typed");
let resolved = asset.resolved_path().expect("asset path is resolved");
assert!(
resolved.ends_with("model.usdz"),
"asset value should resolve to the bare package path, got {resolved:?}",
);
assert!(
!resolved.contains('['),
"asset value must not be anchored into the package, got {resolved:?}",
);
Ok(())
}
#[test]
fn lazy_ref_corrupt_sublayer() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let target = dir.path().join("target.usda");
let broken = dir.path().join("broken.usda");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @target.usda@\n) {}\n")?;
fs::write(
&target,
"#usda 1.0\n(\n subLayers = [@broken.usda@]\n defaultPrim = \"P\"\n)\ndef \"P\" {\n custom double x = 1\n}\n",
)?;
fs::write(&broken, "#usda 1.0\ndef Broken {{{ not valid\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(1.0)),
"the target composes despite its corrupt sublayer"
);
assert!(
stage.composition_errors().iter().any(|error| matches!(
error,
pcp::CompositionDiagnostic::MalformedSublayer { asset_path, introduced_by, reason }
if asset_path == "broken.usda" && introduced_by.ends_with("target.usda") && !reason.is_empty()
)),
"expected MalformedSublayer carrying the parse error, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn mute_nested_reference_target() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir(dir.path().join("sub"))?;
let root = dir.path().join("root.usda");
let mid = dir.path().join("sub").join("mid.usda");
let model = dir.path().join("sub").join("model.usda");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @sub/mid.usda@\n) {}\n")?;
fs::write(
&mid,
"#usda 1.0\n(\n defaultPrim = \"P\"\n)\ndef \"P\" (\n references = @model.usda@\n) {}\n",
)?;
fs::write(
&model,
"#usda 1.0\n(\n defaultPrim = \"P\"\n)\ndef \"P\" {\n custom double x = 1\n}\n",
)?;
let opened = Rc::new(RefCell::new(Vec::new()));
let stage = Stage::builder()
.resolver(RecordingResolver::new(opened.clone()))
.mute(["sub/model.usda"])
.open(root.to_str().unwrap())?;
assert!(stage.prim("/P")?.is_valid()?);
let opened_has = |needle: &str| opened.borrow().iter().any(|p| p.contains(needle));
assert!(opened_has("mid.usda"), "the nested referrer must load");
assert!(
!opened_has("model.usda"),
"the muted nested target must never be opened, got {:?}",
opened.borrow()
);
Ok(())
}
#[test]
fn mute_target_under_nested_sublayer() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir(dir.path().join("detail"))?;
let root = dir.path().join("root.usda");
let target = dir.path().join("target.usda");
let extra = dir.path().join("detail").join("extra.usda");
let model = dir.path().join("detail").join("model.usda");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @target.usda@\n) {}\n")?;
fs::write(
&target,
"#usda 1.0\n(\n defaultPrim = \"P\"\n subLayers = [@detail/extra.usda@]\n)\ndef \"P\" {}\n",
)?;
fs::write(&extra, "#usda 1.0\ndef \"P\" (\n references = @model.usda@\n) {}\n")?;
fs::write(
&model,
"#usda 1.0\n(\n defaultPrim = \"P\"\n)\ndef \"P\" {\n custom double x = 1\n}\n",
)?;
let opened = Rc::new(RefCell::new(Vec::new()));
let stage = Stage::builder()
.resolver(RecordingResolver::new(opened.clone()))
.mute(["detail/model.usda"])
.open(root.to_str().unwrap())?;
assert!(stage.prim("/P")?.is_valid()?);
let opened_has = |needle: &str| opened.borrow().iter().any(|p| p.contains(needle));
assert!(opened_has("extra.usda"), "the authoring sublayer must load");
assert!(
!opened_has("model.usda"),
"the muted target under the nested sublayer must not be opened, got {:?}",
opened.borrow()
);
Ok(())
}
#[test]
fn mute_alternate_spelling() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let weak = dir.path().join("weak.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@weak.usda@]\n)\ndef \"P\" {}\n")?;
fs::write(&weak, "#usda 1.0\ndef \"P\" {\n custom double x = 1\n}\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
let abs = weak.to_str().unwrap();
let canonical = ar::DefaultResolver::new().create_identifier(abs, None);
let read_x = || stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0));
let muted = Rc::new(RefCell::new(Vec::<String>::new()));
let unmuted = Rc::new(RefCell::new(Vec::<String>::new()));
let _token = {
let (muted, unmuted) = (muted.clone(), unmuted.clone());
stage.add_sink(RecordingSink {
muting: Some(Box::new(move |_stage, layer, is_muted, _resynced: &[sdf::Path]| {
if is_muted {
muted.borrow_mut().push(layer.to_string());
} else {
unmuted.borrow_mut().push(layer.to_string());
}
})),
..Default::default()
})
};
assert_eq!(
read_x()?,
Some(sdf::Value::Double(1.0)),
"weak contributes x until muted"
);
stage.mute_layer("weak.usda");
assert!(
stage.is_layer_muted(abs),
"the absolute spelling reads the same muted layer"
);
stage.mute_layer(abs);
assert_eq!(
stage.muted_layers(),
vec![canonical.clone()],
"both spellings are one canonical entry"
);
assert_eq!(read_x()?, None, "the muted weak layer contributes nothing");
stage.unmute_layer(abs);
assert!(
!stage.is_layer_muted("weak.usda"),
"unmuting any spelling unmutes the layer"
);
assert!(stage.muted_layers().is_empty());
assert_eq!(
read_x()?,
Some(sdf::Value::Double(1.0)),
"the unmuted weak layer contributes again"
);
assert_eq!(*muted.borrow(), vec![canonical.clone()]);
assert_eq!(*unmuted.borrow(), vec![canonical]);
Ok(())
}
#[test]
fn mute_open_dedup() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let weak = dir.path().join("weak.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@weak.usda@]\n)\ndef \"P\" {}\n")?;
fs::write(&weak, "#usda 1.0\ndef \"P\" {\n custom double x = 1\n}\n")?;
let abs = weak.to_str().unwrap();
let stage = Stage::builder().mute(["weak.usda", abs]).open(root.to_str().unwrap())?;
assert_eq!(
stage.muted_layers().len(),
1,
"two spellings of one loaded layer seed a single mute, got {:?}",
stage.muted_layers()
);
assert!(stage.is_layer_muted("weak.usda") && stage.is_layer_muted(abs));
assert_eq!(
stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
None,
"the muted weak layer contributes nothing"
);
Ok(())
}
#[test]
fn mute_nested_sublayer() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir(dir.path().join("sub"))?;
let root = dir.path().join("root.usda");
let mid = dir.path().join("sub").join("mid.usda");
let weak = dir.path().join("sub").join("weak.usda");
fs::write(
&root,
"#usda 1.0\n(\n subLayers = [@sub/mid.usda@]\n)\ndef \"P\" {}\n",
)?;
fs::write(&mid, "#usda 1.0\n(\n subLayers = [@weak.usda@]\n)\n")?;
fs::write(&weak, "#usda 1.0\ndef \"P\" {\n custom double x = 1\n}\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
let abs = weak.to_str().unwrap();
let read_x = || stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0));
assert_eq!(
read_x()?,
Some(sdf::Value::Double(1.0)),
"the nested sublayer contributes x"
);
stage.mute_layer("sub/weak.usda");
assert!(
stage.is_layer_muted(abs),
"the muted nested layer reads as muted by absolute path"
);
assert_eq!(read_x()?, None, "the muted nested sublayer drops from the stack");
stage.unmute_layer(abs);
assert!(
!stage.is_layer_muted("sub/weak.usda"),
"unmuting by an alternate spelling unmutes it"
);
assert_eq!(
read_x()?,
Some(sdf::Value::Double(1.0)),
"the unmuted nested sublayer contributes again"
);
Ok(())
}
#[test]
fn bad_expr_sublayer_dropped() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let target = dir.path().join("target.usda");
let over = dir.path().join("over.usda");
fs::write(&root, "#usda 1.0\ndef \"P\" (\n references = @target.usda@\n) {}\n")?;
fs::write(
&target,
"#usda 1.0\n(\n defaultPrim = \"P\"\n subLayers = [@over.usda@, @`\"${UNDEFINED}.usda\"`@]\n)\ndef \"P\" {\n custom double x = 1\n}\n",
)?;
fs::write(&over, "#usda 1.0\ndef \"P\" {\n custom double y = 2\n}\n")?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(1.0)),
"the target's own opinion composes despite the bad expression sublayer"
);
assert_eq!(
stage.attribute("/P.y")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(2.0)),
"the valid sublayer still composes"
);
assert!(
!stage.composition_errors().is_empty(),
"the dropped expression sublayer is reported"
);
Ok(())
}
#[test]
fn open_single_layer() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
assert_eq!(stage.layer_count(), 1);
assert_eq!(stage.default_prim().as_deref(), Some("World"));
assert_eq!(
stage.root_prims()?.iter().map(|t| t.as_str()).collect::<Vec<_>>(),
["World"]
);
Ok(())
}
#[test]
fn traverse_uses_default_predicate() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
let mut prims = Vec::new();
stage.traverse(PrimPredicate::DEFAULT, |p| prims.push(p.as_str().to_string()))?;
assert_eq!(prims, vec!["/World", "/World/CubeActive"]);
Ok(())
}
#[test]
fn traverse_all_visits_every_composed_prim() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
let mut prims = Vec::new();
stage.traverse(PrimPredicate::ALL, |p| prims.push(p.as_str().to_string()))?;
assert_eq!(prims, vec!["/World", "/World/CubeInactive", "/World/CubeActive"]);
Ok(())
}
#[test]
fn sublayer_children_union() -> Result<()> {
let path = fixture_path("sublayer_override.usda");
let stage = Stage::open(&path)?;
let children = child_names(&stage, "/World")?;
assert!(children.contains(&"Cube".to_string()), "Cube from base layer");
assert!(children.contains(&"Sphere".to_string()), "Sphere from override layer");
Ok(())
}
#[test]
fn sublayer_prims_from_weaker_layer() -> Result<()> {
let path = composition_path("subLayer/sublayer_same_folder.usda");
let stage = Stage::open(&path)?;
assert_eq!(stage.layer_count(), 2);
assert_eq!(stage.default_prim().as_deref(), Some("World"));
let mut prims = Vec::new();
stage.traverse(PrimPredicate::DEFAULT, |p| prims.push(p.as_str().to_string()))?;
assert!(prims.contains(&"/World/Cube".to_string()));
Ok(())
}
#[test]
fn reference_default_prim_from_external_layer() -> Result<()> {
let path = composition_path("references/reference_same_folder.usda");
let stage = Stage::open(&path)?;
let children = child_names(&stage, "/World")?;
assert!(
children.contains(&"Cube".to_string()),
"Cube from referenced layer should appear under /World"
);
Ok(())
}
#[test]
fn reference_explicit_prim_path() -> Result<()> {
let path = fixture_path("ref_prim.usda");
let stage = Stage::open(&path)?;
let children = child_names(&stage, "/World/RefPrim")?;
assert!(
children.contains(&"Child".to_string()),
"referenced children should be namespace-remapped"
);
Ok(())
}
#[test]
fn inherit_from_class() -> Result<()> {
let path = composition_path("class_inherit.usda");
let stage = Stage::open(&path)?;
let props = prop_names(&stage, "/World/cubeWithoutSetColor")?;
assert!(
props.contains(&"primvars:displayColor".to_string()),
"inherited property should be visible"
);
Ok(())
}
#[test]
fn payload_pulls_children() -> Result<()> {
let path = composition_path("payload/payload_same_folder.usda");
let stage = Stage::open(&path)?;
let children = child_names(&stage, "/World")?;
assert!(
children.contains(&"Cube".to_string()),
"Cube from payload layer should appear under /World"
);
Ok(())
}
fn open_with_session() -> Result<Stage> {
let root = fixture_path("session_root.usda");
let session = fixture_path("session_layer.usda");
Stage::builder().session_layer(&session).open(&root)
}
#[test]
fn session_var_loads_sublayer() -> Result<()> {
let root = fixture_path("session_expr_sublayer/root.usda");
let session = fixture_path("session_expr_sublayer/session.usda");
let stage = Stage::builder().session_layer(&session).open(&root)?;
assert_eq!(
stage.attribute("/A.x")?.get::<f64>()?,
Some(1.0),
"the session WHICH variable loads and resolves the root's expression sublayer"
);
Ok(())
}
#[test]
fn session_sublayer_var_ignored() -> Result<()> {
let resolves = |session_body: &str| -> Result<Option<f64>> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@`\"${WHICH}.usda\"`@]\n)\n")?;
fs::write(&session, session_body)?;
fs::write(
dir.path().join("sub.usda"),
"#usda 1.0\n(\n expressionVariables = { string WHICH = \"a\" }\n)\n",
)?;
fs::write(
dir.path().join("a.usda"),
"#usda 1.0\ndef \"A\" {\n custom double x = 1\n}\n",
)?;
let stage = Stage::builder()
.session_layer(session.to_str().expect("utf-8 temp path"))
.open(root.to_str().expect("utf-8 temp path"))?;
stage.attribute("/A.x")?.get::<f64>()
};
assert_eq!(
resolves("#usda 1.0\n(\n subLayers = [@sub.usda@]\n)\n")?,
None,
"a session sublayer's WHICH must not resolve the root's expression sublayer",
);
assert_eq!(
resolves("#usda 1.0\n(\n expressionVariables = { string WHICH = \"a\" }\n)\n")?,
Some(1.0),
"the session root's WHICH resolves the root's expression sublayer",
);
Ok(())
}
#[test]
fn session_sublayer_root_var() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(
&root,
"#usda 1.0\n(\n expressionVariables = { string CHILD = \"strong\" }\n)\n",
)?;
fs::write(&session, "#usda 1.0\n(\n subLayers = [@`\"${CHILD}.usda\"`@]\n)\n")?;
fs::write(
dir.path().join("strong.usda"),
"#usda 1.0\ndef \"A\" {\n custom double x = 1\n}\n",
)?;
let stage = Stage::builder()
.session_layer(session.to_str().expect("utf-8 temp path"))
.open(root.to_str().expect("utf-8 temp path"))?;
assert_eq!(
stage.attribute("/A.x")?.get::<f64>()?,
Some(1.0),
"the session sublayer resolves the stage root's CHILD to strong.usda",
);
Ok(())
}
#[test]
fn unmute_session_root_subtree() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(
&root,
"#usda 1.0\n(\n expressionVariables = { string CHILD = \"strong\" }\n)\ndef \"A\" {\n custom double z = 0\n}\n",
)?;
fs::write(&session, "#usda 1.0\n(\n subLayers = [@`\"${CHILD}.usda\"`@]\n)\n")?;
fs::write(
dir.path().join("strong.usda"),
"#usda 1.0\ndef \"A\" {\n def \"Child\" {\n custom double y = 5\n }\n}\n",
)?;
let stage = Stage::builder()
.session_layer(session.to_str().expect("utf-8 temp path"))
.open(root.to_str().expect("utf-8 temp path"))?;
assert!(
stage.prim("/A/Child")?.is_valid()?,
"the stage root's CHILD selects strong.usda in the session"
);
stage.mute_layer(session.to_str().expect("utf-8 temp path"));
assert!(
!stage.prim("/A/Child")?.is_valid()?,
"muting the session root prunes the selected strong.usda"
);
stage.unmute_layer(session.to_str().expect("utf-8 temp path"));
assert!(
stage.prim("/A/Child")?.is_valid()?,
"unmuting restores the pruned session subtree"
);
Ok(())
}
#[test]
fn packaged_root_mute_anchor() -> Result<()> {
let dir = tempfile::tempdir()?;
let package = dir.path().join("package.usdz");
{
let mut writer = ArchiveWriter::create(&package)?;
writer.add_layer("dir/root.usda", b"#usda 1.0\n")?;
writer.add_layer(
"dir/session.usda",
b"#usda 1.0\n(\n subLayers = [@strong.usda@, @weak.usda@]\n)\n",
)?;
writer.add_layer(
"dir/strong.usda",
b"#usda 1.0\ndef \"A\" {\n custom double x = 2\n}\n",
)?;
writer.add_layer("dir/weak.usda", b"#usda 1.0\ndef \"A\" {\n custom double x = 1\n}\n")?;
writer.finish()?;
}
let package = package.to_string_lossy();
let session = format!("{package}[dir/session.usda]");
let stage = Stage::builder()
.session_layer(session)
.mute(["strong.usda"])
.open(&package)?;
assert_eq!(
stage.attribute("/A.x")?.get::<f64>()?,
Some(1.0),
"mute(\"strong.usda\") is anchored relative to the packaged root layer, dropping strong's opinion"
);
Ok(())
}
#[test]
fn no_session_layer_by_default() -> Result<()> {
let stage = Stage::open(&fixture_path("session_root.usda"))?;
assert!(!stage.has_session_layer());
assert!(stage.session_layer().is_none());
assert_eq!(stage.layer_count(), 1);
Ok(())
}
#[test]
fn session_layer_does_not_affect_default_prim() -> Result<()> {
let stage = open_with_session()?;
assert_eq!(stage.default_prim().as_deref(), Some("World"));
Ok(())
}
#[test]
fn session_layer_preserves_children() -> Result<()> {
let stage = open_with_session()?;
let children = child_names(&stage, "/World")?;
assert!(
children.contains(&"Child".to_string()),
"root layer's children should be visible: got {children:?}"
);
Ok(())
}
#[test]
fn api_schemas_returns_applied_schemas() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
let geo = sdf::Path::new("/World/Geo")?;
let schemas = stage.prim(geo.clone())?.api_schemas()?;
assert!(schemas.contains(&tf::Token::from("MaterialBindingAPI")));
assert!(schemas.contains(&tf::Token::from("SkelBindingAPI")));
Ok(())
}
#[test]
fn api_schemas_compose_list_ops() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def Xform "World"
{
def Mesh "Geo" (
append apiSchemas = ["WeakAPI", "RemovedAPI"]
)
{
}
}
"#,
)?;
fs::write(
dir.path().join("middle.usda"),
r#"#usda 1.0
(
subLayers = [
@weak.usda@
]
)
over "World"
{
over "Geo" (
prepend apiSchemas = ["StrongAPI"]
)
{
}
}
"#,
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
(
subLayers = [
@middle.usda@
]
)
over "World"
{
over "Geo" (
delete apiSchemas = ["RemovedAPI"]
)
{
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let schemas = stage.prim(sdf::Path::new("/World/Geo")?)?.api_schemas()?;
assert_eq!(schemas, vec![tf::Token::from("StrongAPI"), tf::Token::from("WeakAPI")]);
Ok(())
}
#[test]
fn api_schemas_compose_reorder_list_op() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def Xform "World"
{
def Mesh "Geo" (
apiSchemas = ["A", "B", "C"]
)
{
}
}
"#,
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
(
subLayers = [
@weak.usda@
]
)
over "World"
{
over "Geo" (
reorder apiSchemas = ["C", "A"]
)
{
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let schemas = stage.prim(sdf::Path::new("/World/Geo")?)?.api_schemas()?;
assert_eq!(
schemas,
vec![tf::Token::from("C"), tf::Token::from("A"), tf::Token::from("B")]
);
Ok(())
}
#[test]
fn api_schemas_via_inherit() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
class "_Base" (
prepend apiSchemas = ["BaseAPI"]
)
{
}
def Xform "World"
{
def Mesh "Geo" (
inherits = </_Base>
prepend apiSchemas = ["LocalAPI"]
)
{
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let geo = sdf::Path::new("/World/Geo")?;
assert_eq!(
stage.prim(geo.clone())?.api_schemas()?,
vec![tf::Token::from("LocalAPI"), tf::Token::from("BaseAPI")],
);
assert!(stage.prim(geo.clone())?.has_api_schema("BaseAPI")?);
assert!(stage.prim(geo.clone())?.has_api_schema("LocalAPI")?);
Ok(())
}
#[test]
fn api_schemas_via_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("asset.usda"),
r#"#usda 1.0
(
defaultPrim = "Source"
)
def Mesh "Source" (
prepend apiSchemas = ["AssetAPI"]
)
{
}
"#,
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def Xform "World"
{
def "Geo" (
references = @asset.usda@
prepend apiSchemas = ["LocalAPI"]
)
{
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let geo = sdf::Path::new("/World/Geo")?;
assert_eq!(
stage.prim(geo.clone())?.api_schemas()?,
vec![tf::Token::from("LocalAPI"), tf::Token::from("AssetAPI")],
);
Ok(())
}
#[test]
fn api_schemas_via_variant() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def Xform "World"
{
def Mesh "Geo" (
variants = {
string mode = "full"
}
prepend variantSets = "mode"
prepend apiSchemas = ["LocalAPI"]
)
{
variantSet "mode" = {
"full" (
prepend apiSchemas = ["VariantAPI"]
) {
}
"empty" {
}
}
}
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let geo = sdf::Path::new("/World/Geo")?;
let schemas = stage.prim(geo.clone())?.api_schemas()?;
assert!(
schemas.contains(&tf::Token::from("VariantAPI")),
"variant contribution missing: {schemas:?}",
);
assert!(
schemas.contains(&tf::Token::from("LocalAPI")),
"local contribution missing: {schemas:?}",
);
Ok(())
}
#[test]
fn api_schemas_property_path() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
let prim = sdf::Path::new("/World/Geo")?;
let prop = sdf::Path::new("/World/Geo.points")?;
assert_eq!(stage.prim(prop)?.api_schemas()?, stage.prim(prim)?.api_schemas()?);
Ok(())
}
#[test]
fn connection_paths_compose_list_ops() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def Shader "Mat"
{
color3f outputs:out
append color3f inputs:in.connect = [</Mat.outputs:out>]
}
"#,
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
(
subLayers = [
@weak.usda@
]
)
over "Mat"
{
prepend color3f inputs:in.connect = [</Mat.outputs:strong>]
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let conns = connections(&stage, &sdf::Path::new("/Mat.inputs:in")?)?;
assert_eq!(
conns,
vec![
sdf::Path::new("/Mat.outputs:strong")?,
sdf::Path::new("/Mat.outputs:out")?
]
);
Ok(())
}
#[test]
fn relationship_targets_compose_list_ops() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def "Set"
{
def "A" {}
def "B" {}
append rel members = [</Set/B>]
}
"#,
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
(
subLayers = [
@weak.usda@
]
)
over "Set"
{
prepend rel members = [</Set/A>]
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let targets = rel_targets(&stage, &sdf::Path::new("/Set.members")?)?;
assert_eq!(targets, vec![sdf::Path::new("/Set/A")?, sdf::Path::new("/Set/B")?]);
Ok(())
}
#[test]
fn relationship_targets_remap_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("asset.usda"),
r#"#usda 1.0
(
defaultPrim = "Source"
)
def "Source"
{
def "Child" {}
rel members = [</Source/Child>]
}
"#,
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "Inst" (
references = @asset.usda@
)
{
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let targets = rel_targets(&stage, &sdf::Path::new("/Inst.members")?)?;
assert_eq!(targets, vec![sdf::Path::new("/Inst/Child")?]);
Ok(())
}
#[test]
fn forwarded_targets_honor_mask() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "Vis"
{
rel chain = [</Hidden.rel>]
rel direct = [</Hidden>]
}
def "Hidden"
{
rel rel = [</Hidden/Geom>]
def "Geom" {}
}
"#,
)?;
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/Vis"])?)
.open(root.to_str().expect("utf-8 temp path"))?;
assert_eq!(
fwd_targets(&stage, &sdf::Path::new("/Vis.chain")?)?,
paths(&["/Hidden.rel"])
);
assert_eq!(
fwd_targets(&stage, &sdf::Path::new("/Vis.direct")?)?,
vec![sdf::Path::new("/Hidden")?]
);
Ok(())
}
#[test]
fn connection_paths_remap_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("asset.usda"),
r#"#usda 1.0
(
defaultPrim = "Source"
)
def Shader "Source"
{
color3f outputs:out
color3f inputs:in.connect = [</Source.outputs:out>]
}
"#,
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def Shader "Mat" (
references = @asset.usda@
)
{
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let input = sdf::Path::new("/Mat.inputs:in")?;
let output = sdf::Path::new("/Mat.outputs:out")?;
assert_eq!(connections(&stage, &input)?, vec![output.clone()]);
let graph = usd::ConnectionGraph::from_stage(&stage)?;
assert_eq!(graph.sources(&input), std::slice::from_ref(&output));
assert_eq!(graph.sinks(&output), &[input]);
Ok(())
}
#[test]
fn api_schemas_empty_for_prim_without_schemas() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
let props = sdf::Path::new("/World/Props")?;
assert!(stage.prim(props)?.api_schemas()?.is_empty());
Ok(())
}
#[test]
fn has_api_schema_matches_applied() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
let geo = sdf::Path::new("/World/Geo")?;
assert!(stage.prim(geo.clone())?.has_api_schema("MaterialBindingAPI")?);
assert!(!stage.prim(geo.clone())?.has_api_schema("SkelRootAPI")?);
Ok(())
}
#[test]
fn type_name_returns_prim_type() -> Result<()> {
let stage = Stage::open("fixtures/api_schemas.usda")?;
assert_eq!(
stage.prim(sdf::Path::new("/World/Geo")?)?.type_name()?.as_deref(),
Some("Mesh")
);
assert_eq!(
stage.prim(sdf::Path::new("/World")?)?.type_name()?.as_deref(),
Some("Xform")
);
Ok(())
}
fn open_stage_queries_fixture() -> Result<Stage> {
Stage::open("fixtures/stage_queries.usda")
}
#[test]
fn active_loaded() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert!(stage.prim("/World/ActiveParent/Child")?.is_active()?);
assert!(stage.prim("/World/ActiveParent/Child")?.is_loaded()?);
assert!(!stage.prim("/World/InactiveParent")?.is_active()?);
assert!(!stage.prim("/World/InactiveParent/Child")?.is_active()?);
assert!(!stage.prim("/World/InactiveParent/Child")?.is_loaded()?);
assert!(!stage.prim("/World/Missing")?.is_active()?);
Ok(())
}
struct RecordingResolver {
inner: ar::DefaultResolver,
opened: Rc<RefCell<Vec<String>>>,
}
impl RecordingResolver {
fn new(opened: Rc<RefCell<Vec<String>>>) -> Self {
Self {
inner: ar::DefaultResolver::new(),
opened,
}
}
}
impl ar::Resolver for RecordingResolver {
fn create_identifier(&self, asset_path: &str, anchor: Option<&ar::ResolvedPath>) -> String {
self.inner.create_identifier(asset_path, anchor)
}
fn resolve(&self, asset_path: &str) -> Option<ar::ResolvedPath> {
self.inner.resolve(asset_path)
}
fn resolve_for_new_asset(&self, asset_path: &str) -> Option<ar::ResolvedPath> {
self.inner.resolve_for_new_asset(asset_path)
}
fn open_asset(&self, resolved_path: &ar::ResolvedPath) -> std::io::Result<Box<dyn ar::Asset>> {
self.opened.borrow_mut().push(resolved_path.to_string());
self.inner.open_asset(resolved_path)
}
fn identity(&self) -> String {
self.inner.identity()
}
fn is_context_dependent_path(&self, asset_path: &str) -> bool {
self.inner.is_context_dependent_path(asset_path)
}
fn in_memory_identifier<'a>(&self, asset_path: &'a str) -> Cow<'a, str> {
self.inner.in_memory_identifier(asset_path)
}
}
#[test]
fn lazy_reference_loads_on_demand() -> Result<()> {
let path = composition_path("references/reference_same_folder.usda");
let opened = Rc::new(RefCell::new(Vec::new()));
let stage = Stage::builder()
.resolver(RecordingResolver::new(opened.clone()))
.open(&path)?;
let opened_has = |needle: &str| opened.borrow().iter().any(|p| p.contains(needle));
assert!(opened_has("reference_same_folder"));
assert!(!opened_has("_stage.usda"), "reference target must not load at open");
assert_eq!(stage.layer_count(), 1);
let _ = child_names(&stage, "/World")?;
assert!(opened_has("_stage.usda"), "composing the prim must load its reference");
assert_eq!(stage.layer_count(), 2);
let target_opens = opened.borrow().iter().filter(|p| p.contains("_stage.usda")).count();
assert_eq!(target_opens, 1, "the reference target loads exactly once");
Ok(())
}
#[test]
fn root_layers_read_once() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let session = dir.path().join("session.usda");
fs::write(&root, "#usda 1.0\ndef \"World\" {}\n")?;
fs::write(&session, "#usda 1.0\n")?;
let opened = Rc::new(RefCell::new(Vec::new()));
Stage::builder()
.resolver(RecordingResolver::new(opened.clone()))
.session_layer(session.to_str().unwrap())
.open(root.to_str().unwrap())?;
let opens = |name: &str| opened.borrow().iter().filter(|path| path.ends_with(name)).count();
assert_eq!(opens("root.usda"), 1);
assert_eq!(opens("session.usda"), 1);
Ok(())
}
#[test]
fn muted_reference_target_not_opened() -> Result<()> {
let path = composition_path("references/reference_same_folder.usda");
let target = composition_path("references/_stage.usda");
let opened = Rc::new(RefCell::new(Vec::new()));
let muted_id = ar::DefaultResolver::new().create_identifier(&target, None);
let stage = Stage::builder()
.resolver(RecordingResolver::new(opened.clone()))
.mute([muted_id])
.open(&path)?;
let _ = child_names(&stage, "/World")?;
assert!(
!opened.borrow().iter().any(|p| p.contains("_stage.usda")),
"a muted reference target must never be opened"
);
let errors = stage.composition_errors();
let muted = errors
.iter()
.filter(|e| {
matches!(
e,
pcp::CompositionDiagnostic::MutedAssetPath { arc: pcp::ArcType::Reference, asset_path, .. }
if asset_path.contains("_stage.usda")
)
})
.count();
assert_eq!(
muted, 1,
"a muted reference target must surface exactly one MutedAssetPath diagnostic, got {errors:?}"
);
Ok(())
}
#[test]
fn unmute_unloaded_reference_recomposes() -> Result<()> {
let path = composition_path("references/reference_same_folder.usda");
let target = composition_path("references/_stage.usda");
let opened = Rc::new(RefCell::new(Vec::new()));
let muted_id = ar::DefaultResolver::new().create_identifier(&target, None);
let stage = Stage::builder()
.resolver(RecordingResolver::new(opened.clone()))
.mute([muted_id.clone()])
.open(&path)?;
assert_eq!(child_names(&stage, "/World")?, Vec::<String>::new());
assert!(
!opened.borrow().iter().any(|p| p.contains("_stage.usda")),
"a muted reference target must never be opened"
);
stage.unmute_layer(&muted_id);
assert_eq!(
child_names(&stage, "/World")?,
vec!["Cube"],
"unmuting a never-loaded reference target recomposes the referrer"
);
assert!(
opened.borrow().iter().any(|p| p.contains("_stage.usda")),
"unmuting must let the load barrier open the now-unmuted target"
);
Ok(())
}
#[test]
fn load_none() -> Result<()> {
let path = composition_path("payload/payload_same_folder.usda");
let loaded = Stage::open(&path)?;
assert_eq!(loaded.layer_count(), 1);
assert!(loaded.prim("/World")?.is_loaded()?);
assert_eq!(child_names(&loaded, "/World")?, vec!["Cube"]);
assert_eq!(loaded.layer_count(), 2);
let unloaded = Stage::builder().load(InitialLoadSet::LoadNone).open(&path)?;
assert_eq!(unloaded.initial_load_set(), InitialLoadSet::LoadNone);
assert_eq!(unloaded.layer_count(), 1);
assert!(!unloaded.prim("/World")?.is_loaded()?);
assert_eq!(child_names(&unloaded, "/World")?, Vec::<String>::new());
let mut prims = Vec::new();
unloaded.traverse(PrimPredicate::DEFAULT, |p| prims.push(p.as_str().to_string()))?;
assert!(prims.is_empty());
Ok(())
}
#[test]
fn runtime_load_unload() -> Result<()> {
let path = composition_path("payload/payload_same_folder.usda");
let stage = Stage::builder().load(InitialLoadSet::LoadNone).open(&path)?;
assert!(!stage.prim("/World")?.is_loaded()?);
stage.load("/World", LoadPolicy::WithDescendants).unwrap();
assert!(stage.prim("/World")?.is_loaded()?);
assert_eq!(child_names(&stage, "/World")?, vec!["Cube"]);
stage.unload("/World").unwrap();
assert!(!stage.prim("/World")?.is_loaded()?);
assert_eq!(child_names(&stage, "/World")?, Vec::<String>::new());
Ok(())
}
#[test]
fn set_load_rules_round_trips() -> Result<()> {
let path = composition_path("payload/payload_same_folder.usda");
let stage = Stage::open(&path)?;
let mut rules = pcp::LoadRules::all();
rules.unload(sdf::Path::new("/World")?);
stage.set_load_rules(rules.clone());
assert_eq!(stage.load_rules(), rules);
assert!(!stage.prim("/World")?.is_loaded()?);
Ok(())
}
#[test]
fn load_noop_fires_no_notification() -> Result<()> {
let path = composition_path("payload/payload_same_folder.usda");
let stage = Stage::open(&path)?;
let calls = Rc::new(RefCell::new(0));
let _token = {
let calls = calls.clone();
stage.add_sink(RecordingSink {
load_rules: Some(Box::new(move |_stage, _resynced| {
*calls.borrow_mut() += 1;
})),
..Default::default()
})
};
stage.load("/World", LoadPolicy::WithDescendants).unwrap();
assert_eq!(
*calls.borrow(),
0,
"already-loaded path with default rules fires nothing"
);
stage.unload("/World").unwrap();
assert_eq!(*calls.borrow(), 1);
stage.unload("/World").unwrap();
assert_eq!(*calls.borrow(), 1, "repeated unload is a no-op");
Ok(())
}
fn write_nested_payload_scene(dir: &std::path::Path) -> Result<std::path::PathBuf> {
let root = dir.join("root.usda");
let a = dir.join("a.usda");
let deep = dir.join("deep.usda");
fs::write(
&root,
"#usda 1.0\ndef \"World\" {\n def \"A\" (\n payload = @a.usda@\n ) {}\n}\n",
)?;
fs::write(
&a,
"#usda 1.0\n(\n defaultPrim = \"A\"\n)\ndef \"A\" {\n def \"Deep\" (\n payload = @deep.usda@\n ) {}\n}\n",
)?;
fs::write(
&deep,
"#usda 1.0\n(\n defaultPrim = \"Deep\"\n)\ndef \"Deep\" {\n custom double x = 42\n}\n",
)?;
Ok(root)
}
#[test]
fn nested_payload_find_loadable_and_load_set() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_nested_payload_scene(dir.path())?;
let stage = Stage::builder()
.load(InitialLoadSet::LoadNone)
.open(root.to_str().unwrap())?;
assert_eq!(
stage.find_loadable("/World")?,
vec![sdf::path("/World/A")?, sdf::path("/World/A/Deep")?]
);
assert!(stage.load_set()?.is_empty(), "load rules still say LoadNone");
assert!(!stage.prim("/World/A")?.is_loaded()?);
stage.load("/World/A", LoadPolicy::WithDescendants).unwrap();
assert_eq!(
stage.load_set()?,
vec![sdf::path("/World/A")?, sdf::path("/World/A/Deep")?]
);
Ok(())
}
#[test]
fn load_and_unload_same_path_prefers_load() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_nested_payload_scene(dir.path())?;
let stage = Stage::open(root.to_str().unwrap())?;
stage
.load_and_unload(
[(sdf::path("/World/A")?, LoadPolicy::WithDescendants)],
[sdf::path("/World/A")?],
)
.unwrap();
assert!(stage.prim("/World/A")?.is_loaded()?);
Ok(())
}
#[test]
fn load_and_unload_nested_ancestor_descendant() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_nested_payload_scene(dir.path())?;
let stage = Stage::open(root.to_str().unwrap())?;
stage
.load_and_unload(
[(sdf::path("/World/A/Deep")?, LoadPolicy::WithDescendants)],
[sdf::path("/World/A")?],
)
.unwrap();
assert!(stage.prim("/World/A")?.is_loaded()?);
assert!(stage.prim("/World/A/Deep")?.is_loaded()?);
assert_eq!(
stage
.attribute("/World/A/Deep.x")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(42.0))
);
Ok(())
}
#[test]
fn prim_load_unload() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_nested_payload_scene(dir.path())?;
let stage = Stage::builder()
.load(InitialLoadSet::LoadNone)
.open(root.to_str().unwrap())?;
assert!(!stage.prim("/World/A")?.is_loaded()?);
stage.prim("/World/A")?.load(LoadPolicy::WithoutDescendants);
assert!(stage.prim("/World/A")?.is_loaded()?);
assert!(!stage.prim("/World/A/Deep")?.is_loaded()?);
assert!(
stage
.load_rules()
.is_loaded_with_no_descendants(&sdf::path("/World/A")?)
);
stage.prim("/World/A")?.load(LoadPolicy::WithDescendants);
assert!(stage.prim("/World/A/Deep")?.is_loaded()?);
assert!(
stage
.load_rules()
.is_loaded_with_all_descendants(&sdf::path("/World/A")?)
);
assert_eq!(stage.attribute("/World/A/Deep.x")?.get::<f64>()?, Some(42.0));
stage.prim("/World/A")?.unload();
assert!(!stage.prim("/World/A")?.is_loaded()?);
assert_eq!(child_names(&stage, "/World/A")?, Vec::<String>::new());
Ok(())
}
#[test]
fn prototype_load_ignored() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let proto = dir.path().join("proto.usda");
fs::write(
&root,
"#usda 1.0\ndef \"World\" {\n def \"Inst\" (\n instanceable = true\n references = @proto.usda@\n ) {}\n}\n",
)?;
fs::write(
&proto,
"#usda 1.0\n(\n defaultPrim = \"Proto\"\n)\ndef \"Proto\" {\n def \"Child\" {}\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let prototype = stage.prim("/World/Inst")?.prototype()?.expect("Inst is an instance");
let before = stage.load_rules();
stage.prim(&prototype)?.unload();
assert_eq!(stage.load_rules(), before, "the prototype root authors no rule");
stage.prim(&sdf::path(format!("{prototype}/Child"))?)?.unload();
assert_eq!(stage.load_rules(), before, "nor does a prim inside the prototype");
stage.prim("/World/Inst/Child")?.unload();
assert_ne!(stage.load_rules(), before, "the real-namespace path does author one");
Ok(())
}
#[test]
fn instance_descendant_load_rule_splits_prototype() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let proto = dir.path().join("proto.usda");
let heavy = dir.path().join("heavy.usda");
fs::write(
&root,
"#usda 1.0\ndef \"World\" {\n def \"InstA\" (\n instanceable = true\n references = @proto.usda@\n ) {}\n def \"InstB\" (\n instanceable = true\n references = @proto.usda@\n ) {}\n}\n",
)?;
fs::write(
&proto,
"#usda 1.0\n(\n defaultPrim = \"Proto\"\n)\ndef \"Proto\" {\n def \"Heavy\" (\n payload = @heavy.usda@\n ) {}\n}\n",
)?;
fs::write(
&heavy,
"#usda 1.0\n(\n defaultPrim = \"Heavy\"\n)\ndef \"Heavy\" {\n custom double x = 1\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let proto_a = stage.prim("/World/InstA")?.prototype()?.expect("InstA is an instance");
let proto_b = stage.prim("/World/InstB")?.prototype()?.expect("InstB is an instance");
assert_eq!(
proto_a, proto_b,
"identical composition and load state share a prototype"
);
stage.unload("/World/InstA/Heavy").unwrap();
let proto_a = stage.prim("/World/InstA")?.prototype()?.expect("still an instance");
let proto_b = stage.prim("/World/InstB")?.prototype()?.expect("still an instance");
assert_ne!(
proto_a, proto_b,
"differing load rules on a descendant split the prototype"
);
Ok(())
}
#[test]
fn set_load_rules_strips_prototype_path() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let proto = dir.path().join("proto.usda");
fs::write(
&root,
"#usda 1.0\ndef \"World\" {\n def \"Inst\" (\n instanceable = true\n references = @proto.usda@\n ) {}\n}\n",
)?;
fs::write(
&proto,
"#usda 1.0\n(\n defaultPrim = \"Proto\"\n)\ndef \"Proto\" {}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let prototype = stage.prim("/World/Inst")?.prototype()?.expect("Inst is an instance");
let mut rules = pcp::LoadRules::all();
rules.unload(prototype);
stage.set_load_rules(rules);
assert!(
stage.load_rules().is_empty(),
"the prototype-rooted rule must never be stored"
);
Ok(())
}
#[test]
fn is_loaded_through_prototype_path() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let proto = dir.path().join("proto.usda");
let heavy = dir.path().join("heavy.usda");
fs::write(
&root,
"#usda 1.0\ndef \"World\" {\n def \"Inst\" (\n instanceable = true\n references = @proto.usda@\n ) {}\n}\n",
)?;
fs::write(
&proto,
"#usda 1.0\n(\n defaultPrim = \"Proto\"\n)\ndef \"Proto\" {\n def \"Heavy\" (\n payload = @heavy.usda@\n ) {}\n}\n",
)?;
fs::write(
&heavy,
"#usda 1.0\n(\n defaultPrim = \"Heavy\"\n)\ndef \"Heavy\" {\n custom double x = 1\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let prototype = stage.prim("/World/Inst")?.prototype()?.expect("Inst is an instance");
let proto_heavy = prototype.append_path("Heavy")?;
assert!(
stage.prim(&proto_heavy)?.is_loaded()?,
"loaded by default before any unload"
);
stage.unload("/World/Inst/Heavy").unwrap();
assert!(
!stage.prim("/World/Inst/Heavy")?.is_loaded()?,
"unloaded through the instance's own path"
);
assert!(
!stage.prim(&proto_heavy)?.is_loaded()?,
"the prototype's own path must report the same, not the previous unconditional loaded"
);
Ok(())
}
#[test]
fn defined_abstract() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert_eq!(stage.prim("/World/OverOnly")?.specifier()?, Some(sdf::Specifier::Over));
assert!(stage.prim("/World/ActiveParent/Child")?.is_defined()?);
assert!(!stage.prim("/World/OverOnly")?.is_defined()?);
assert!(!stage.prim("/World/OverParent/Child")?.is_defined()?);
assert!(stage.prim("/World/ClassParent/Child")?.is_defined()?);
assert!(stage.prim("/World/ClassParent")?.is_abstract()?);
assert!(stage.prim("/World/ClassParent/Child")?.is_abstract()?);
assert!(!stage.prim("/World/ActiveParent/Child")?.is_abstract()?);
Ok(())
}
#[test]
fn instance_flag() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert!(stage.prim("/World/Instance")?.has_composition_arc()?);
assert!(stage.prim("/World/Instance")?.is_instance()?);
assert!(!stage.prim("/World/InstanceableNoArc")?.has_composition_arc()?);
assert!(!stage.prim("/World/InstanceableNoArc")?.is_instance()?);
Ok(())
}
#[test]
fn instance_children_from_arcs_only() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing.usda"))?;
let mut children = child_names(&stage, "/Instance")?;
children.sort();
assert_eq!(children, vec!["Child".to_string()]);
let mut non_instance = child_names(&stage, "/NonInstance")?;
non_instance.sort();
assert_eq!(non_instance, vec!["Child".to_string(), "LocalOnly".to_string()]);
Ok(())
}
#[test]
fn shared_instances_resolve_identically() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
assert_eq!(
stage
.attribute("/A/Child.size")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(5.0))
);
assert_eq!(
stage
.attribute("/B/Child.size")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(5.0))
);
assert_eq!(
stage
.attribute("/C/Child.size")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(9.0))
);
assert_eq!(child_names(&stage, "/A")?, vec!["Child".to_string()]);
assert_eq!(child_names(&stage, "/B")?, vec!["Child".to_string()]);
Ok(())
}
#[test]
fn class_arcs_share_prototype() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_class_arcs.usda"))?;
let prototype = first_prototype(&stage)?;
for instance in ["/Set/A", "/Set/B", "/C", "/D"] {
assert_eq!(
stage.prim(instance)?.prototype()?.as_ref(),
Some(&prototype),
"{instance}"
);
assert_eq!(
stage
.attribute(format!("{instance}/Geo.radius"))?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(1.0)),
"{instance}"
);
}
assert_eq!(stage.prototypes()?, vec![prototype]);
Ok(())
}
fn geo_radius(stage: &Stage, instance: &str) -> Result<Option<sdf::Value>> {
stage
.attribute(format!("{instance}/Geo.radius"))?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))
}
#[test]
fn class_spec_rekeys_instance() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_class_arcs.usda"))?;
let shared = stage.prim("/C")?.prototype()?.expect("an instance");
assert_eq!(stage.prim("/D")?.prototype()?.as_ref(), Some(&shared));
stage
.override_prim("/__class__/C/Geo")?
.create_attribute("radius", "double")?
.set(sdf::Value::Double(7.0))?;
let own = stage.prim("/C")?.prototype()?.expect("an instance");
assert_eq!(geo_radius(&stage, "/C")?, Some(sdf::Value::Double(7.0)));
for instance in ["/D", "/Set/A", "/Set/B"] {
let prototype = stage.prim(instance)?.prototype()?.expect("an instance");
assert_ne!(prototype, own, "{instance}");
assert_eq!(
geo_radius(&stage, instance)?,
Some(sdf::Value::Double(1.0)),
"{instance}"
);
}
assert_eq!(stage.prim(own.clone())?.instances()?, paths(&["/C"]));
assert_eq!(stage.prototypes()?.len(), 2);
assert!(stage.remove_prim("/__class__/C")?);
let prototype = stage.prim("/D")?.prototype()?.expect("an instance");
assert_eq!(stage.prim("/C")?.prototype()?.as_ref(), Some(&prototype));
assert_eq!(geo_radius(&stage, "/C")?, Some(sdf::Value::Double(1.0)));
assert_eq!(stage.prototypes()?, vec![prototype]);
Ok(())
}
#[test]
fn ancestral_arc_not_instance() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_ancestral_arc.usda"))?;
let prim = stage.prim("/Set/A")?;
assert!(!prim.is_instance()?);
assert_eq!(prim.prototype()?, None);
assert!(stage.prim("/Set/A/Geo")?.is_valid()?);
assert!(stage.prim("/Set/A/Local")?.is_valid()?);
assert_eq!(geo_radius(&stage, "/Set/A")?, Some(sdf::Value::Double(5.0)));
assert!(stage.prototypes()?.is_empty());
Ok(())
}
#[test]
fn prototype_queries() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
let proto = stage.prim("/A")?.prototype()?;
assert!(proto.is_some());
assert_eq!(stage.prim("/B")?.prototype()?, proto); assert_ne!(stage.prim("/C")?.prototype()?, proto); assert_eq!(stage.prim("/Proto")?.prototype()?, None);
let proto = proto.unwrap();
let instances: Vec<String> = stage
.prim(proto.clone())?
.instances()?
.iter()
.map(|p| p.to_string())
.collect();
assert_eq!(instances, vec!["/A".to_string(), "/B".to_string()]);
assert!(stage.prim(proto.clone())?.is_prototype()?);
let child = sdf::path(format!("{proto}/Child"))?;
assert!(stage.prim(child.clone())?.is_in_prototype()?);
assert_eq!(
stage
.attribute(child.append_property("size")?)?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(5.0))
);
Ok(())
}
#[test]
fn prototype_queries_masked() -> Result<()> {
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/A"])?)
.open(&fixture_path("instancing_shared.usda"))?;
assert!(stage.prim("/A")?.is_instance()?);
assert!(!stage.prim("/B")?.is_instance()?);
let proto = stage.prim("/A")?.prototype()?;
assert!(proto.is_some());
assert_eq!(stage.prim("/B")?.prototype()?, None);
let proto = proto.unwrap();
assert_eq!(stage.prim(proto.clone())?.instances()?, vec![sdf::path("/A")?]);
assert_eq!(stage.prototypes()?, vec![proto]);
Ok(())
}
#[test]
fn mask_keys_prototype() -> Result<()> {
let fixture = fixture_path("instancing_mask_split.usda");
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/Instance_1/geom", "/Instance_2/geom"])?)
.open(&fixture)?;
let one = stage.prim("/Instance_1")?.prototype()?.expect("an instance");
assert_eq!(stage.prim("/Instance_2")?.prototype()?, Some(one.clone()));
assert_eq!(child_names(&stage, one)?, vec!["geom".to_string()]);
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/Instance_1/geom", "/Instance_2"])?)
.open(&fixture)?;
let one = stage.prim("/Instance_1")?.prototype()?.expect("an instance");
let two = stage.prim("/Instance_2")?.prototype()?.expect("an instance");
assert_ne!(one, two, "instances the mask reaches differently split");
assert_eq!(child_names(&stage, one)?, vec!["geom".to_string()]);
assert_eq!(
child_names(&stage, two)?,
vec!["geom".to_string(), "shading".to_string()]
);
Ok(())
}
#[test]
fn mask_relative_sharing() -> Result<()> {
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/Instance_1", "/Instance_2"])?)
.open(&fixture_path("instancing_mask_split.usda"))?;
let one = stage.prim("/Instance_1")?.prototype()?.expect("an instance");
assert_eq!(stage.prim("/Instance_2")?.prototype()?, Some(one.clone()));
assert_eq!(stage.prototypes()?, vec![one.clone()]);
assert_eq!(
stage.prim(one)?.instances()?,
paths(&["/Instance_1", "/Instance_2"]),
"both instances registered"
);
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/Instance_1"])?)
.open(&fixture_path("instancing_mask_split.usda"))?;
let one = stage.prim("/Instance_1")?.prototype()?.expect("an instance");
assert!(!stage.prim("/Instance_2")?.is_instance()?, "excluded, so not populated");
assert_eq!(stage.prim("/Instance_2")?.prototype()?, None);
assert_eq!(stage.prim(one)?.instances()?, paths(&["/Instance_1"]));
Ok(())
}
#[test]
fn masked_cold_prototype_query() -> Result<()> {
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/Instance_1/geom"])?)
.open(&fixture_path("instancing_mask_split.usda"))?;
let proto = first_prototype(&stage)?;
assert_eq!(child_names(&stage, proto.clone())?, vec!["geom".to_string()]);
assert_eq!(
stage
.attribute(proto.append_path("geom")?.append_property("size")?)?
.get_at::<f64>(usd::TimeCode::new(0.0))?,
Some(1.0)
);
assert_eq!(
stage
.attribute(proto.append_path("shading")?.append_property("size")?)?
.get_at::<f64>(usd::TimeCode::new(0.0))?,
None,
"the mask does not name shading, so the prototype does not compose it"
);
Ok(())
}
#[test]
fn nested_prototype_cold_query() -> Result<()> {
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/A"])?)
.open(&fixture_path("instancing_nested_in_prototype.usda"))?;
let roots = stage.prototypes()?;
assert_eq!(roots.len(), 2, "an outer and a nested prototype: {roots:?}");
let outer = roots[0].clone();
let nested = roots[1].clone();
assert_eq!(
stage.prim(nested)?.instances()?,
vec![outer.append_path("Nested")?],
"the nested prototype's instance is the prim inside the enclosing one"
);
Ok(())
}
#[test]
fn inactive_instance_not_populated() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
assert!(stage.prim("/A")?.is_instance()?);
stage.prim("/A")?.set_active(false)?;
assert!(!stage.prim("/A")?.is_instance()?, "an inactive prim is not populated");
assert_eq!(stage.prim("/A")?.prototype()?, None);
let a = sdf::path("/A")?;
for root in stage.prototypes()? {
assert!(
!stage.prim(root.clone())?.instances()?.contains(&a),
"{root} still lists the deactivated instance"
);
}
stage.prim("/A")?.set_active(true)?;
assert!(stage.prim("/A")?.is_instance()?, "reactivating restores the instance");
assert!(stage.prim("/A")?.prototype()?.is_some());
Ok(())
}
#[test]
fn deactivating_retires_prototype() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
let solo = stage.prim("/C")?.prototype()?.expect("/C is an instance");
assert!(stage.prototypes()?.contains(&solo));
stage.prim("/C")?.set_active(false)?;
assert!(
!stage.prototypes()?.contains(&solo),
"the prototype went with its only instance"
);
Ok(())
}
#[test]
fn rediscovery_on_known_root() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Proto/Child")?;
let instance = |path: &str| -> Result<()> {
stage.define_prim(path)?.set_metadata(
sdf::FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
prim_path: sdf::path("/Proto")?,
..Default::default()
}])),
)?;
stage
.prim(path)?
.set_metadata(sdf::FieldKey::Instanceable.as_str(), sdf::Value::Bool(true))?;
Ok(())
};
instance("/A")?;
let proto = first_prototype(&stage)?;
assert_eq!(stage.prim(proto.clone())?.instances()?, paths(&["/A"]));
instance("/B")?;
assert_eq!(
stage.prim(proto)?.instances()?,
paths(&["/A", "/B"]),
"the new instance is found without touching it"
);
Ok(())
}
#[test]
fn in_prototype_is_inclusive() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
let proto = stage.prim("/A")?.prototype()?.expect("/A is an instance");
assert!(stage.prim(proto.clone())?.is_in_prototype()?, "the root itself");
assert!(stage.prim(proto.append_path("Child")?)?.is_in_prototype()?);
assert!(
!stage.prim(proto.append_path("Missing")?)?.is_in_prototype()?,
"a path that composes to no prim"
);
assert!(!stage.prim("/A/Child")?.is_in_prototype()?, "an instance proxy");
Ok(())
}
#[test]
fn synthetic_load_target_ignored() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
let before = stage.load_rules();
stage.unload("/__Prototype_99")?;
stage.load("/__Prototype_99/Child", usd::LoadPolicy::WithDescendants)?;
assert_eq!(stage.load_rules(), before, "no rule was recorded either way");
Ok(())
}
#[test]
fn prototype_type_resolves_cold() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
let proto = first_prototype(&stage)?;
let child = stage.prim(proto.append_path("Child")?)?;
let scope = Some(tf::Token::from("Scope"));
assert_eq!(child.type_name()?, scope);
assert_eq!(child.type_name()?, scope, "the memoized identity agrees");
Ok(())
}
#[test]
fn epoch_covers_failure_recovery() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let target = dir.path().join("target.usda");
fs::write(
&root,
"#usda 1.0
def \"P\" (
references = @target.usda@
) {}
",
)?;
fs::write(
&target,
"#usda 1.0
def Broken {{{ not valid
",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert!(
stage.prototypes()?.is_empty(),
"the unreadable target hides the instance"
);
fs::write(
&target,
"#usda 1.0
(
defaultPrim = \"P\"
)
def \"P\" (
instanceable = true
)
{
def \"Child\" {
custom double x = 7
}
}
",
)?;
stage.define_prim("/Trigger")?;
let roots = stage.prototypes()?;
assert_eq!(roots.len(), 1, "the newly revealed prototype is found: {roots:?}");
assert_eq!(stage.prim(roots[0].clone())?.instances()?, paths(&["/P"]));
Ok(())
}
#[test]
fn discovery_skips_inactive_subtree() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0
def \"Live\" (
instanceable = true
references = </Proto>
) {}
def \"Proto\" {
def \"Child\" {}
}
def \"Off\" (
active = false
)
{
def \"Deep\" (
payload = @missing.usda@
) {}
}
",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.prototypes()?.len(), 1, "only the live instance registers");
assert!(
stage.composition_errors().is_empty(),
"the payload under an inactive prim was never demanded: {:?}",
stage.composition_errors()
);
Ok(())
}
const INACTIVE_PARENT: &str = "#usda 1.0
def Xform \"P\" (
active = false
)
{
custom double x = 1
def Sphere \"C\"
{
custom double y = 2
def Scope \"D\" {}
}
}
";
#[test]
fn inactive_prim_no_descendants() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_text(&dir, INACTIVE_PARENT)?;
let p = stage.prim("/P")?;
assert!(p.is_valid()?);
assert!(!p.is_active()?);
assert_eq!(stage.attribute("/P.x")?.get::<f64>()?, Some(1.0));
assert!(p.child_names()?.is_empty());
assert!(p.children()?.is_empty());
for below in ["/P/C", "/P/C/D"] {
let prim = stage.prim(below)?;
assert!(!prim.is_valid()?, "{below}");
assert!(prim.child_names()?.is_empty(), "{below}");
assert!(prim.prim_stack()?.is_empty(), "{below}");
assert_eq!(stage.prim_status(below)?, PrimStatus::empty(), "{below}");
}
assert_eq!(stage.attribute("/P/C.y")?.get::<f64>()?, None);
let mut visited = Vec::new();
stage.traverse(PrimPredicate::ALL, |path| visited.push(path.clone()))?;
assert_eq!(visited, paths(&["/P"]));
let (children, _) = stage.prim_index("/P")?.child_names()?;
assert_eq!(children, vec![tf::Token::new("C")]);
assert_eq!(stage.prim_index("/P/C")?.prim_stack()?.len(), 1);
Ok(())
}
#[test]
fn inactive_subtree_never_loads() -> Result<()> {
let scene = |active: bool| {
format!(
"#usda 1.0
def \"Parent\" (
active = {active}
)
{{
def \"Deep\" (
payload = @missing.usda@
) {{
def \"Leaf\" {{}}
}}
}}
"
)
};
let dir = tempfile::tempdir()?;
let stage = open_text(&dir, &scene(false))?;
assert!(!stage.prim("/Parent/Deep/Leaf")?.is_valid()?);
assert!(child_names(&stage, "/Parent/Deep")?.is_empty());
assert!(
stage.composition_errors().is_empty(),
"the payload under an inactive prim was never demanded: {:?}",
stage.composition_errors()
);
let dir = tempfile::tempdir()?;
let stage = open_text(&dir, &scene(true))?;
assert!(stage.prim("/Parent/Deep/Leaf")?.is_valid()?);
assert!(
!stage.composition_errors().is_empty(),
"the same payload under an active prim is demanded"
);
Ok(())
}
#[test]
fn forwarding_stops_at_inactive() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_text(
&dir,
"#usda 1.0
def \"A\" {
rel r = </Off/Deep.r>
}
def \"X\" {}
def \"Off\" (
active = false
)
{
def \"Deep\" (
payload = @missing.usda@
) {
rel r = </X>
}
}
",
)?;
assert_eq!(fwd_targets(&stage, &sdf::path("/A.r")?)?, paths(&["/Off/Deep.r"]));
assert!(rel_targets(&stage, &sdf::path("/Off/Deep.r")?)?.is_empty());
assert!(
stage.composition_errors().is_empty(),
"the payload under an inactive prim was never demanded: {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn activation_toggles_descendants() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_text(
&dir,
"#usda 1.0\ndef \"P\" {\n def \"C\" {\n def \"D\" {}\n }\n}\n",
)?;
let on_stage = |stage: &Stage| -> Result<(Vec<String>, bool, bool)> {
Ok((
child_names(stage, "/P")?,
stage.prim("/P/C")?.is_valid()?,
stage.prim("/P/C/D")?.is_valid()?,
))
};
assert_eq!(on_stage(&stage)?, (vec!["C".to_string()], true, true));
stage.prim("/P")?.set_active(false)?;
assert_eq!(on_stage(&stage)?, (Vec::new(), false, false));
assert!(stage.prim("/P")?.is_valid()?);
stage.prim("/P")?.set_active(true)?;
assert_eq!(on_stage(&stage)?, (vec!["C".to_string()], true, true));
Ok(())
}
#[test]
fn define_below_inactive() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_text(&dir, INACTIVE_PARENT)?;
let new = stage.define_prim("/P/New")?;
assert!(!new.is_valid()?);
assert_eq!(
stage.prim_index("/P/New")?.prim_stack()?.len(),
1,
"the spec is authored"
);
assert!(matches!(
new.clone().set_kind("component"),
Err(StageAuthoringError::PrimNotValid { .. })
));
stage.prim("/P")?.set_active(true)?;
assert!(new.is_valid()?);
assert_eq!(child_names(&stage, "/P")?, ["C", "New"]);
new.set_kind("component")?;
Ok(())
}
#[test]
fn property_write_below_inactive() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("weak.usda"),
"#usda 1.0
def \"P\" {
def \"C\" {
double radius = 1
rel proxy
}
}
",
)?;
let stage = open_text(
&dir,
"#usda 1.0
(
subLayers = [@weak.usda@]
)
over \"P\" (
active = false
) {}
",
)?;
let not_valid = |result: Result<(), StageAuthoringError>| {
assert!(
matches!(result, Err(StageAuthoringError::PrimNotValid { .. })),
"{result:?}"
);
};
not_valid(stage.create_attribute("/P/C.radius", "float").map(drop));
not_valid(stage.create_attribute("/P/C.fresh", "float").map(drop));
not_valid(stage.create_relationship("/P/C.radius").map(drop));
not_valid(stage.create_relationship("/P/C.proxy").map(drop));
not_valid(stage.attribute("/P/C.radius")?.set(2.0).map(drop));
not_valid(stage.attribute("/P/C.radius")?.clear().map(drop));
not_valid(stage.relationship("/P/C.proxy")?.set_targets(["/P"]).map(drop));
not_valid(stage.relationship("/P/C.proxy")?.clear_targets().map(drop));
assert!(
stage.root_layer().prim("/P/C")?.is_none(),
"a refused write authors nothing"
);
stage.prim("/P")?.set_active(true)?;
let radius = stage.create_attribute("/P/C.radius", "float")?.set(2.0)?;
assert_eq!(radius.get::<f64>()?, Some(2.0), "stamped as the weaker layer's double");
assert!(matches!(
stage.create_relationship("/P/C.radius"),
Err(StageAuthoringError::SpecKindMismatch { .. })
));
stage.relationship("/P/C.proxy")?.set_targets(["/P"])?;
assert_eq!(rel_targets(&stage, &sdf::path("/P/C.proxy")?)?, paths(&["/P"]));
Ok(())
}
#[test]
fn property_write_masked_out() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\ndef \"Vis\" {}\ndef \"Hidden\" {\n double x = 1\n}\n",
)?;
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/Vis"])?)
.open(root.to_str().unwrap())?;
assert!(matches!(
stage.create_attribute("/Hidden.x", "double"),
Err(StageAuthoringError::PrimNotValid { .. })
));
assert!(matches!(
stage.attribute("/Hidden.x")?.set(2.0),
Err(StageAuthoringError::PrimNotValid { .. })
));
stage.create_attribute("/Vis.x", "double")?.set(2.0)?;
Ok(())
}
#[test]
fn property_write_unselected_variant() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/World")?;
let root = stage.edit_target().layer_identifier().to_string();
stage.set_edit_target(EditTarget::for_local_direct_variant(root, "/World{set=sel}")?)?;
let only = stage.define_prim("/World/Only")?;
assert!(!only.is_valid()?, "the variant is not selected");
stage.create_attribute("/World/Only.x", "double")?;
stage.create_relationship("/World/Only.r")?.set_targets(["/World"])?;
stage.define_prim("/World/Only/Child")?;
stage.create_attribute("/World/Only/Child.x", "double")?;
stage
.create_relationship("/World/Only/Child.r")?
.set_targets(["/World"])?;
let layer = stage.root_layer();
assert!(layer.attribute("/World{set=sel}Only.x")?.is_some());
assert!(layer.relationship("/World{set=sel}Only.r")?.is_some());
assert!(layer.attribute("/World{set=sel}Only/Child.x")?.is_some());
assert!(layer.relationship("/World{set=sel}Only/Child.r")?.is_some());
Ok(())
}
#[test]
fn move_below_inactive_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("ref.usda"),
"#usda 1.0\ndef \"M\" {\n def \"C\" {\n double fromRef = 1\n }\n}\n",
)?;
let stage = open_text(
&dir,
"#usda 1.0
def \"A\" {
double local = 2
}
def \"P\" (
active = false
references = @ref.usda@</M>
) {}
",
)?;
assert_eq!(
stage.prim_index("/P/C")?.prim_stack()?.len(),
1,
"the reference composes /P/C"
);
let mut editor = usd::NamespaceEditor::new(&stage);
editor.move_prim("/A", "/P/C").unwrap();
editor.apply()?;
assert!(!stage.prim("/A")?.is_valid()?);
assert!(!stage.prim("/P/C")?.is_valid()?, "/P is still inactive");
stage.prim("/P")?.set_active(true)?;
assert_eq!(stage.attribute("/P/C.local")?.get::<f64>()?, Some(2.0));
assert_eq!(stage.attribute("/P/C.fromRef")?.get::<f64>()?, Some(1.0));
Ok(())
}
#[test]
fn malformed_active_fails_reads() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_text(
&dir,
"#usda 1.0\ndef \"P\" {\n def \"C\" {\n double y = 2\n }\n}\n",
)?;
let root = stage.root_layer().identifier().to_string();
let set_active = |value: sdf::Value| -> Result<()> {
stage.layer_mut(&root).expect("root layer").edit(|e| {
e.data_mut()
.set_field(&sdf::path("/P")?, sdf::FieldKey::Active.as_str(), value);
Ok(())
})?;
Ok(())
};
assert_eq!(stage.attribute("/P/C.y")?.get::<f64>()?, Some(2.0));
set_active(sdf::Value::Int(5))?;
assert!(stage.prim("/P/C")?.is_valid().is_err());
assert!(stage.attribute("/P/C.y")?.get::<f64>().is_err());
assert!(stage.prim("/P")?.child_names().is_err());
set_active(sdf::Value::Bool(true))?;
assert!(stage.prim("/P/C")?.is_valid()?);
assert_eq!(stage.attribute("/P/C.y")?.get::<f64>()?, Some(2.0));
assert_eq!(child_names(&stage, "/P")?, ["C"]);
Ok(())
}
#[test]
fn discovery_spans_branches() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0
(
expressionVariables = {
string WHICH = \"pick\"
}
)
def \"A\" (
instanceable = true
references = @early.usda@
) {}
def \"B\" (
references = @holder.usda@
) {}
",
)?;
fs::write(
dir.path().join("early.usda"),
"#usda 1.0
(
defaultPrim = \"Early\"
)
def \"Early\" {
def \"Child\" {}
}
",
)?;
fs::write(
dir.path().join("holder.usda"),
"#usda 1.0
(
defaultPrim = \"Holder\"
subLayers = [@`\"${WHICH}.usda\"`@]
)
def \"Holder\" {}
",
)?;
fs::write(
dir.path().join("pick.usda"),
"#usda 1.0
over \"Holder\" {}
",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let roots = stage.prototypes()?;
assert_eq!(roots.len(), 1, "the earlier branch's prototype survives: {roots:?}");
assert_eq!(stage.prim(roots[0].clone())?.instances()?, paths(&["/A"]));
assert_eq!(child_names(&stage, roots[0].clone())?, vec!["Child".to_string()]);
Ok(())
}
#[test]
fn masked_instance_payload() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0
def \"Outer\" (
instanceable = true
references = @model.usda@
) {}
",
)?;
fs::write(
dir.path().join("model.usda"),
"#usda 1.0
(
defaultPrim = \"Model\"
)
def \"Model\" {
def \"geom\" {
custom double size = 1
}
def \"shading\" {}
def \"Inner\" (
instanceable = true
payload = @inner.usda@
) {}
}
",
)?;
fs::write(
dir.path().join("inner.usda"),
"#usda 1.0
(
defaultPrim = \"Inner\"
)
def \"Inner\" {
def \"Leaf\" {
custom double size = 2
}
}
",
)?;
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/Outer/geom", "/Outer/Inner"])?)
.load(usd::InitialLoadSet::LoadNone)
.open(root.to_str().unwrap())?;
let outer = first_prototype(&stage)?;
assert_eq!(
child_names(&stage, outer.clone())?,
vec!["geom".to_string(), "Inner".to_string()],
"shading is outside the mask"
);
assert_eq!(
stage
.attribute(outer.append_path("geom")?.append_property("size")?)?
.get_at::<f64>(usd::TimeCode::new(0.0))?,
Some(1.0)
);
stage.load("/Outer/Inner", usd::LoadPolicy::WithDescendants)?;
let outer = stage.prim("/Outer")?.prototype()?.expect("/Outer is still an instance");
let nested = stage
.prim(outer.append_path("Inner")?)?
.prototype()?
.expect("the nested prim is an instance");
assert_eq!(
stage
.attribute(nested.append_path("Leaf")?.append_property("size")?)?
.get_at::<f64>(usd::TimeCode::new(0.0))?,
Some(2.0),
"the nested prototype composes the loaded payload"
);
assert!(
stage.prototypes()?.contains(&nested),
"the nested prototype joins the stage's set"
);
Ok(())
}
#[test]
fn empty_index_sees_child() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let extra = dir.path().join("extra.usda");
fs::write(
&root,
"#usda 1.0
(
subLayers = [@extra.usda@]
)
def \"A\" {}
",
)?;
fs::write(
&extra,
"#usda 1.0
",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert!(child_names(&stage, "/A")?.is_empty(), "nothing below /A yet");
let extra_id = identifier_by_leaf(&stage, "extra.usda");
stage.layer_mut(&extra_id).expect("the sublayer is live").edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/A", sdf::Specifier::Over, "")?;
sdf::PrimSpec::new(e.data_mut(), "/A/B", sdf::Specifier::Def, "")?;
Ok(())
})?;
assert_eq!(
child_names(&stage, "/A")?,
vec!["B".to_string()],
"the first spec authored below /A reaches its cached index"
);
Ok(())
}
#[test]
fn discovery_restarts_on_late_load() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0
def \"A\" (
instanceable = true
references = @early.usda@
) {}
def \"B\" (
references = @late.usda@
) {}
",
)?;
fs::write(
dir.path().join("early.usda"),
"#usda 1.0
(
defaultPrim = \"Early\"
)
def \"Early\" {
def \"Child\" {}
}
",
)?;
fs::write(
dir.path().join("late.usda"),
"#usda 1.0
(
defaultPrim = \"Late\"
relocates = {
</Late/Old>: </Late/New>
}
)
def \"Late\" {
def \"Old\" {}
}
",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let roots = stage.prototypes()?;
assert_eq!(roots.len(), 1, "the earlier branch's prototype survives: {roots:?}");
assert_eq!(stage.prim(roots[0].clone())?.instances()?, paths(&["/A"]));
assert_eq!(child_names(&stage, roots[0].clone())?, vec!["Child".to_string()]);
assert!(stage.prim("/B")?.is_valid()?, "the late branch composed");
Ok(())
}
#[test]
fn prototype_root_not_instance() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_prototype_root_instanceable.usda"))?;
let proto = stage
.prim("/World/Place")?
.prototype()?
.expect("/World/Place is an instance");
assert!(stage.prim(proto.clone())?.is_prototype()?);
assert!(!stage.prim(proto.clone())?.is_instance()?);
assert_eq!(stage.prim(proto)?.prototype()?, None);
Ok(())
}
#[test]
fn nested_prototype_masked() -> Result<()> {
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/A"])?)
.open(&fixture_path("instancing_nested_in_prototype.usda"))?;
let outer = stage.prim("/A")?.prototype()?.expect("/A is an instance");
let nested_instance = outer.append_path("Nested")?;
let nested = stage
.prim(nested_instance.clone())?
.prototype()?
.expect("the nested prim is an instance");
assert!(
stage.prototypes()?.contains(&nested),
"the nested prototype is populated"
);
assert_eq!(stage.prim(nested.clone())?.instances()?, vec![nested_instance]);
assert_eq!(
stage
.attribute(nested.append_path("Leaf")?.append_property("v")?)?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(3.0))
);
Ok(())
}
#[test]
fn nested_instances() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_nested.usda"))?;
assert_eq!(
stage
.attribute("/A/Sub/L.v")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(7.0))
);
assert_eq!(
stage
.attribute("/B/Sub/L.v")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(7.0))
);
assert!(stage.prim("/A/Sub")?.is_instance()?);
assert!(stage.prim("/B/Sub")?.is_instance()?);
let nested = stage.prim("/A/Sub")?.prototype()?;
assert!(nested.is_some());
assert_eq!(stage.prim("/B/Sub")?.prototype()?, nested);
let outer = stage.prim("/A")?.prototype()?;
assert_eq!(stage.prim("/B")?.prototype()?, outer);
assert_ne!(outer, nested);
let outer = outer.unwrap();
assert_eq!(
stage
.attribute(sdf::path(format!("{outer}/Sub/L.v"))?)?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(7.0))
);
Ok(())
}
#[test]
fn instance_connection_remaps_to_instance() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_connections.usda"))?;
assert_eq!(
connections(&stage, &sdf::path("/I1/Dst.inputs:in")?)?,
vec![sdf::path("/I1/Src.outputs:out")?]
);
assert_eq!(
connections(&stage, &sdf::path("/I2/Dst.inputs:in")?)?,
vec![sdf::path("/I2/Src.outputs:out")?]
);
Ok(())
}
#[test]
fn prototype_descendant_target_remap() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_connections.usda"))?;
assert_eq!(
connections(&stage, &sdf::path("/I1/Dst.inputs:in")?)?,
vec![sdf::path("/I1/Src.outputs:out")?]
);
let proto = stage.prim("/I1")?.prototype()?.expect("I1 is an instance");
let dst_in = proto.append_path("Dst")?.append_property("inputs:in")?;
assert_eq!(
connections(&stage, &dst_in)?,
vec![proto.append_path("Src")?.append_property("outputs:out")?]
);
Ok(())
}
#[test]
fn forwarded_targets_through_instance() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("asset.usda"),
r#"#usda 1.0
(
defaultPrim = "Proto"
)
def "Proto"
{
def "Target" {}
rel direct = [</Proto/Target>]
rel chain = [</Proto.direct>]
}
"#,
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "I1" (
instanceable = true
references = @asset.usda@
)
{
}
def "I2" (
instanceable = true
references = @asset.usda@
)
{
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
assert_eq!(
fwd_targets(&stage, &sdf::path("/I1.chain")?)?,
vec![sdf::path("/I1/Target")?]
);
assert_eq!(
fwd_targets(&stage, &sdf::path("/I2.chain")?)?,
vec![sdf::path("/I2/Target")?]
);
Ok(())
}
#[test]
fn readers_index_instanced_content() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_connections.usda"))?;
let graph = usd::ConnectionGraph::from_stage(&stage)?;
assert_eq!(
graph.sources(&sdf::path("/I1/Dst.inputs:in")?),
&[sdf::path("/I1/Src.outputs:out")?]
);
Ok(())
}
#[test]
fn traversal_instance_proxies() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
let mut default = Vec::new();
stage.traverse(PrimPredicate::DEFAULT, |p| default.push(p.to_string()))?;
assert!(default.contains(&"/A".to_string()));
assert!(!default.contains(&"/A/Child".to_string()));
let mut proxies = Vec::new();
stage.traverse(PrimPredicate::DEFAULT.with_instance_proxies(true), |p| {
proxies.push(p.to_string());
})?;
assert!(proxies.contains(&"/A/Child".to_string()));
Ok(())
}
#[test]
fn prototype_root_drops_instance_overrides() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_root_override.usda"))?;
assert_eq!(
stage
.attribute("/A.shared")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(7.0))
);
assert_eq!(
stage
.attribute("/A.rootOnly")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(42.0))
);
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
for name in ["shared", "rootOnly"] {
assert_eq!(
stage
.attribute(proto.append_property(name)?)?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
None,
"{name}"
);
}
Ok(())
}
#[test]
fn prototype_survives_early_query() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_root_override.usda"))?;
let proto = first_prototype(&stage)?;
let root = stage.prim(proto.clone())?;
assert!(root.is_valid()?);
assert!(root.is_prototype()?);
assert_eq!(stage.prim("/A")?.prototype()?, Some(proto));
Ok(())
}
#[test]
fn descendant_survives_early_query() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
let proto = first_prototype(&stage)?;
let child = proto.append_path("Child")?;
assert_eq!(
stage
.attribute(child.append_property("size")?)?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(5.0))
);
assert!(stage.prim(child)?.is_in_prototype()?);
Ok(())
}
#[test]
fn unknown_prototype_is_empty() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
let unknown = sdf::path("/__Prototype_99")?;
assert!(!stage.prim(unknown.clone())?.is_valid()?);
assert!(!stage.prim(unknown.clone())?.is_prototype()?);
assert!(!stage.prim(unknown.clone())?.is_in_prototype()?);
assert_eq!(
stage
.attribute(unknown.append_path("Child")?.append_property("size")?)?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
None
);
Ok(())
}
#[test]
fn query_heals_materialization() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
let proto = first_prototype(&stage)?;
let q = stage.attribute_query(proto.append_path("Child")?.append_property("size")?)?;
assert_eq!(q.get_at::<f64>(usd::TimeCode::new(0.0))?, Some(5.0));
assert_eq!(stage.prim("/A")?.prototype()?, Some(proto));
assert_eq!(q.get_at::<f64>(usd::TimeCode::new(0.0))?, Some(5.0));
Ok(())
}
#[test]
fn prototype_root_keeps_variant_opinions() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_variant_root.usda"))?;
assert_eq!(
stage
.attribute("/A.picked")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(5.0))
);
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
assert_eq!(
stage
.attribute(proto.append_property("picked")?)?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
None
);
Ok(())
}
#[test]
fn prototype_root_target_remap() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_root_target.usda"))?;
assert_eq!(
rel_targets(&stage, &sdf::path("/A.myrel")?)?,
vec![sdf::path("/A/Target")?]
);
assert_eq!(
connections(&stage, &sdf::path("/A.inputs:in")?)?,
vec![sdf::path("/A.outputs:out")?]
);
assert_eq!(
rel_targets(&stage, &sdf::path("/B.myrel")?)?,
vec![sdf::path("/B/Target")?]
);
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
assert!(rel_targets(&stage, &proto.append_property("myrel")?)?.is_empty());
assert!(connections(&stage, &proto.append_property("inputs:in")?)?.is_empty());
Ok(())
}
#[test]
fn prototype_root_empty() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_root_empty.usda"))?;
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
let root = stage.prim(proto.clone())?;
assert!(root.is_valid()?);
assert_eq!(root.type_name()?, None);
assert!(root.api_schemas()?.is_empty());
assert!(root.property_names()?.is_empty());
assert_eq!(root.specifier()?, Some(sdf::Specifier::Def));
assert!(root.is_defined()?);
assert!(root.is_model()?);
assert!(root.is_group()?);
assert!(!root.is_component()?);
let animated = root.attribute("animated");
assert_eq!(animated.type_name()?, None);
assert!(!animated.has_authored_value()?);
assert!(!animated.resolve_info()?.has_authored_value());
assert_eq!(animated.get::<sdf::Value>()?, None);
assert_eq!(animated.time_samples()?, None);
assert!(!animated.value_might_be_time_varying()?);
assert!(animated.property_stack()?.is_empty());
let myrel = root.relationship("myrel");
assert!(!myrel.has_authored_targets()?);
assert!(myrel.targets()?.is_empty());
assert!(myrel.property_stack()?.is_empty());
let input = root.attribute("inputs:in");
assert!(!input.has_authored_connections()?);
assert!(input.connections()?.is_empty());
assert!(root.prim_stack()?.is_empty());
assert!(root.variant_sets().get_all_variant_selections()?.is_empty());
assert!(!root.has_composition_arc()?);
assert!(root.prim_index().graph()?.is_empty());
assert_eq!(root.prim_index().child_names()?, (Vec::new(), Vec::new()));
assert_eq!(child_names(&stage, proto.clone())?, vec!["Child".to_string()]);
let child_path = proto.append_path("Child")?;
let child = stage.prim(child_path.clone())?;
assert_eq!(child.type_name()?.as_deref(), Some("Scope"));
assert_eq!(child.attribute("size").get::<f64>()?, Some(3.0));
assert!(child.is_defined()?);
let to_root = child_path.append_property("toRoot")?;
assert_eq!(fwd_targets(&stage, &to_root)?, vec![proto.append_property("myrel")?]);
assert_eq!(
fwd_targets(&stage, &sdf::path("/A/Child.toRoot")?)?,
vec![sdf::path("/A/Child")?]
);
Ok(())
}
#[test]
fn prototype_root_active() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_inactive_source.usda"))?;
assert!(stage.prim("/A")?.is_active()?);
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
let root = stage.prim(proto.clone())?;
assert!(root.is_active()?);
assert!(root.is_loaded()?);
assert_eq!(child_names(&stage, proto.clone())?, vec!["Child".to_string()]);
let child = stage.prim(proto.append_path("Child")?)?;
assert!(child.is_active()?);
assert_eq!(child.attribute("size").get::<f64>()?, Some(3.0));
Ok(())
}
#[test]
fn traversal_matches_prim_status() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("payload.usda"),
"#usda 1.0\ndef \"P\"\n{\n def \"Inside\"\n {\n def \"More\"\n {\n }\n }\n}\n",
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "World"
{
def "Active"
{
def "Leaf"
{
}
}
def "Off" (
active = false
)
{
def "Leaf"
{
}
}
over "Over"
{
def "Leaf"
{
}
}
class "Class"
{
def "Leaf"
{
}
}
def "Loaded" (
payload = @./payload.usda@</P>
)
{
}
def "Unloaded" (
payload = @./payload.usda@</P>
)
{
}
def "Inst" (
instanceable = true
references = </World/Active>
)
{
}
def "Deep"
{
over "A"
{
def "B"
{
}
}
def "C"
{
class "D"
{
def "E"
{
}
}
}
}
}
"#,
)?;
let root = root.to_str().expect("utf-8 temp path");
for load in [InitialLoadSet::LoadAll, InitialLoadSet::LoadNone] {
let stage = Stage::builder().load(load).open(root)?;
stage.prim("/World/Loaded")?.load(LoadPolicy::WithDescendants);
let mut all = Vec::new();
stage.traverse(PrimPredicate::ALL, |path| all.push(path.clone()))?;
let predicates = [
PrimPredicate::DEFAULT,
PrimPredicate::DEFAULT_PROXIES,
PrimPredicate::new(PrimStatus::ACTIVE, PrimStatus::empty()),
PrimPredicate::new(PrimStatus::LOADED, PrimStatus::empty()),
PrimPredicate::new(PrimStatus::DEFINED, PrimStatus::empty()),
PrimPredicate::new(PrimStatus::empty(), PrimStatus::ABSTRACT),
];
for predicate in predicates {
let mut expected = Vec::new();
let mut instances: Vec<sdf::Path> = Vec::new();
for path in &all {
let below_instance = instances
.iter()
.any(|instance| path.has_prefix(instance) && path != instance);
let status = stage.prim_status(path.clone())?;
if status.contains(PrimStatus::INSTANCE) {
instances.push(path.clone());
}
if predicate.matches(status) && (predicate == PrimPredicate::DEFAULT_PROXIES || !below_instance) {
expected.push(path.clone());
}
}
let mut visited = Vec::new();
stage.traverse(predicate, |path| visited.push(path.clone()))?;
assert_eq!(visited, expected, "{load:?} {predicate:?}");
assert!(visited.len() > 5);
}
}
Ok(())
}
#[test]
fn properties_by_spec_type() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "Source"
{
double size = 1
rel target = </Source>
}
def "A" (
instanceable = true
references = </Source>
)
{
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let source = stage.prim("/Source")?;
let attributes: Vec<String> = source
.attributes()?
.iter()
.map(|attribute| attribute.name().to_owned())
.collect();
assert_eq!(attributes, ["size"]);
let relationships: Vec<String> = source
.relationships()?
.iter()
.map(|rel| rel.name().to_owned())
.collect();
assert_eq!(relationships, ["target"]);
let prototype = stage.prim(stage.prim("/A")?.prototype()?.expect("A is an instance"))?;
assert!(prototype.attributes()?.is_empty());
assert!(prototype.relationships()?.is_empty());
Ok(())
}
#[test]
fn status_bits_match_queries() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
class "Class"
{
def "Child"
{
over "Over"
{
}
}
}
over "Over"
{
def "Child"
{
}
}
def "World"
{
class "Nested"
{
def "Leaf"
{
}
}
}
def "A" (
instanceable = true
references = </Class>
)
{
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let mut paths = Vec::new();
stage.traverse(PrimPredicate::ALL, |path| paths.push(path.clone()))?;
for prototype in stage.prototypes()? {
paths.push(prototype.append_path("Child")?);
paths.push(prototype);
}
assert!(paths.len() > 10);
for path in paths {
let prim = stage.prim(path.clone())?;
let status = stage.prim_status(path.clone())?;
assert_eq!(status.contains(PrimStatus::DEFINED), prim.is_defined()?, "{path}");
assert_eq!(status.contains(PrimStatus::ABSTRACT), prim.is_abstract()?, "{path}");
}
Ok(())
}
#[test]
fn prototype_root_defined() -> Result<()> {
let dir = tempfile::tempdir()?;
for specifier in ["over", "class"] {
let root = dir.path().join(format!("{specifier}.usda"));
fs::write(
&root,
format!(
r#"#usda 1.0
{specifier} "Source"
{{
def Scope "Child"
{{
}}
}}
def "A" (
instanceable = true
references = </Source>
)
{{
}}
"#
),
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
let root = stage.prim(proto.clone())?;
assert_eq!(root.specifier()?, Some(sdf::Specifier::Def), "{specifier}");
assert!(root.is_defined()?, "{specifier}");
assert!(!root.is_abstract()?, "{specifier}");
let child = stage.prim(proto.append_path("Child")?)?;
assert!(child.is_defined()?, "{specifier}");
assert!(!child.is_abstract()?, "{specifier}");
}
Ok(())
}
#[test]
fn prototype_child_model() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def Scope "Source"
{
def Scope "Child" (
kind = "component"
)
{
}
}
def Scope "A" (
instanceable = true
references = </Source>
)
{
}
"#,
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
let child = stage.prim(proto.append_path("Child")?)?;
assert!(child.is_model()?);
assert!(child.is_component()?);
let proxy = stage.prim("/A/Child")?;
assert!(proxy.is_instance_proxy()?);
assert!(proxy.is_model()? && proxy.is_component()?);
assert_eq!(model_flags(&stage, "/A")?, NOT_MODEL);
Ok(())
}
#[test]
fn proxy_model_from_prototype() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def Scope "Inner"
{
def Scope "Leaf" (
kind = "prop"
)
{
def Scope "Part" (
kind = "prop"
)
{
}
}
}
def Scope "Source"
{
def Scope "Squad" (
kind = "chargroup"
)
{
}
def Scope "Nest" (
instanceable = true
references = </Inner>
)
{
}
}
def Scope "A" (
instanceable = true
kind = "site_root"
references = </Source>
)
{
}
"#,
)?;
let stage = Stage::builder()
.schema_registry(custom_kinds_registry()?)
.open(root.to_str().expect("utf-8 temp path"))?;
assert_eq!(model_flags(&stage, "/A")?, NOT_MODEL);
assert_eq!(model_flags(&stage, "/A/Squad")?, GROUP);
assert!(stage.prim("/A/Nest")?.is_instance_proxy()?);
assert_eq!(model_flags(&stage, "/A/Nest")?, NOT_MODEL);
assert_eq!(model_flags(&stage, "/A/Nest/Leaf")?, COMPONENT);
assert_eq!(model_flags(&stage, "/A/Nest/Leaf/Part")?, NOT_MODEL);
assert_eq!(model_flags(&stage, "/Inner/Leaf")?, NOT_MODEL);
let leaf = usd::ModelAPI::from_prim_unchecked(stage.prim("/A/Nest/Leaf")?);
assert!(leaf.is_kind("component", usd::KindValidation::ModelHierarchy)?);
assert!(leaf.is_kind("prop", usd::KindValidation::ModelHierarchy)?);
let part = usd::ModelAPI::from_prim_unchecked(stage.prim("/A/Nest/Leaf/Part")?);
assert!(!part.is_kind("prop", usd::KindValidation::ModelHierarchy)?);
assert!(part.is_kind("prop", usd::KindValidation::None)?);
assert!(stage.prim_status("/A/Nest/Leaf")?.contains(PrimStatus::MODEL));
assert!(!stage.prim_status("/A/Nest")?.contains(PrimStatus::MODEL));
Ok(())
}
#[test]
fn masked_arc_unloaded() -> Result<()> {
let dir = tempfile::tempdir()?;
let referenced = dir.path().join("ref.usda");
fs::write(&referenced, "#usda 1.0\ndef Scope \"Ref\"\n{\n}\n")?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def Scope "Masked" (
references = @./ref.usda@</Ref>
)
{
}
def Scope "Kept"
{
}
"#,
)?;
let root = root.to_str().expect("utf-8 temp path");
let loads_ref = |stage: &Stage| {
stage
.layer_identifiers()
.iter()
.any(|id| FsPath::new(id).ends_with("ref.usda"))
};
let stage = Stage::builder().mask(StagePopulationMask::new(["/Kept"])?).open(root)?;
assert!(!stage.prim("/Masked")?.has_composition_arc()?);
assert!(!loads_ref(&stage), "ref.usda is not loaded");
let stage = Stage::open(root)?;
assert!(stage.prim("/Masked")?.has_composition_arc()?);
assert!(loads_ref(&stage), "ref.usda is loaded");
Ok(())
}
#[test]
fn variant_selection_keys_prototype() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_variant_distinct.usda"))?;
let proto = |p: &str| -> Result<sdf::Path> {
Ok(stage
.prim(p)?
.prototype()?
.unwrap_or_else(|| panic!("{p} is not an instance")))
};
assert_eq!(proto("/A")?, proto("/C")?);
assert_ne!(proto("/A")?, proto("/B")?);
assert_eq!(
stage
.attribute("/A.picked")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(1.0))
);
assert_eq!(
stage
.attribute("/B.picked")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(2.0))
);
assert_eq!(
stage
.attribute("/C.picked")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(1.0))
);
Ok(())
}
#[test]
fn variant_rel_in_prototype() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_variant_rel.usda"))?;
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
let rig = proto.append_path("Rig")?;
assert_eq!(
stage.relationship(rig.append_property("wires")?)?.targets()?,
vec![proto.append_path("Geom")?],
"the variant-authored target lands in the prototype namespace"
);
assert!(
stage.composition_errors().is_empty(),
"no target drops: {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn rel_through_variant_payload() -> Result<()> {
for variant in [false, true] {
for instanceable in [false, true] {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("bundle.usda"),
r#"#usda 1.0
( defaultPrim = "bundle" )
def Xform "bundle"
{
def PointInstancer "instancer"
{
rel prototypes = [ </bundle/instancer/Proto1> ]
def Xform "Proto1"
{
}
}
}
"#,
)?;
let geometry = r#"def Xform "geometry" (
prepend payload = @./bundle.usda@</bundle>
)
{
}"#;
let content = if variant {
format!(
r#"#usda 1.0
( defaultPrim = "C" )
def Xform "C" (
variants = {{
string element = "v1"
}}
prepend variantSets = "element"
)
{{
variantSet "element" = {{
"v1" {{
{geometry}
}}
}}
}}
"#
)
} else {
format!(
r#"#usda 1.0
( defaultPrim = "C" )
def Xform "C"
{{
{geometry}
}}
"#
)
};
fs::write(dir.path().join("content.usda"), content)?;
let inst = if instanceable {
"instanceable = true\n "
} else {
""
};
fs::write(
dir.path().join("outer.usda"),
format!(
r#"#usda 1.0
( defaultPrim = "Root" )
def Xform "Root"
{{
def Xform "Inst" (
{inst}payload = @./content.usda@</C>
)
{{
}}
}}
"#
),
)?;
let stage = Stage::builder()
.load(InitialLoadSet::LoadAll)
.open(dir.path().join("outer.usda").to_str().unwrap())?;
let base = if instanceable {
stage
.prim("/Root/Inst")?
.prototype()?
.expect("instance resolves a prototype")
} else {
sdf::path("/Root/Inst")?
};
let instancer = base.append_path("geometry")?.append_path("instancer")?;
assert_eq!(
stage
.relationship(instancer.append_property("prototypes")?)?
.targets()?,
vec![instancer.append_path("Proto1")?],
"variant={variant} instanceable={instanceable}"
);
assert!(
stage.composition_errors().is_empty(),
"variant={variant} instanceable={instanceable}: {:?}",
stage.composition_errors()
);
}
}
Ok(())
}
#[test]
fn prototype_visible_under_mask() -> Result<()> {
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/A"])?)
.open(&fixture_path("instancing_shared.usda"))?;
assert!(stage.prim("/A")?.is_instance()?);
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
let child = proto.append_path("Child")?;
assert!(stage.prim(child.clone())?.is_valid()?);
assert_eq!(
stage
.attribute(child.append_property("size")?)?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(5.0))
);
assert!(!stage.mask().includes(&sdf::path("/B")?));
Ok(())
}
#[test]
fn nested_instance_in_prototype() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_nested_in_prototype.usda"))?;
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
assert!(stage.prim("/A/Nested")?.is_instance()?);
assert_eq!(
stage
.attribute("/A/Nested/Leaf.v")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(3.0))
);
let nested = stage.prim(proto.append_path("Nested")?)?;
assert!(nested.is_instance()?);
let nested_proto = nested.prototype()?.expect("nested prim is an instance");
assert_ne!(nested_proto, proto);
let leaf = stage.prim(proto.append_path("Nested")?.append_path("Leaf")?)?;
assert!(leaf.is_instance_proxy()?);
let in_proto = leaf.prim_in_prototype()?.expect("Leaf is an instance proxy");
assert_eq!(in_proto.path(), &nested_proto.append_path("Leaf")?);
Ok(())
}
#[test]
fn instance_proxy_api() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_shared.usda"))?;
assert!(!stage.prim("/A")?.is_instance_proxy()?);
assert!(stage.prim("/A/Child")?.is_instance_proxy()?);
let proto = stage.prim("/A")?.prototype()?.expect("A is an instance");
let in_proto = stage
.prim("/A/Child")?
.prim_in_prototype()?
.expect("Child is an instance proxy");
assert_eq!(in_proto.path(), &proto.append_path("Child")?);
assert!(in_proto.is_in_prototype()?);
assert!(!in_proto.is_instance_proxy()?);
assert!(!stage.prim("/A/Missing")?.is_instance_proxy()?);
assert!(stage.prim("/A/Missing")?.prim_in_prototype()?.is_none());
Ok(())
}
#[test]
fn instance_descendant_ignores_local_override() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing.usda"))?;
assert_eq!(
stage
.attribute("/Instance/Child.size")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(1.0))
);
assert_eq!(
stage
.attribute("/NonInstance/Child.size")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(999.0))
);
Ok(())
}
#[test]
fn instance_descendant_ignores_local_arc() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_local_arc.usda"))?;
assert_eq!(
stage
.attribute("/A/Child.v")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(1.0))
);
Ok(())
}
#[test]
fn model_hierarchy() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert_eq!(stage.prim("/World")?.kind()?.as_deref(), Some("assembly"));
assert!(stage.prim("/World")?.is_model()?);
assert!(stage.prim("/World")?.is_group()?);
assert!(stage.prim("/World/Group")?.is_model()?);
assert!(stage.prim("/World/Group")?.is_group()?);
assert!(stage.prim("/World/Group/Component")?.is_model()?);
assert!(stage.prim("/World/Group/Component")?.is_component()?);
assert!(!stage.prim("/World/Group/Subcomponent")?.is_model()?);
assert!(stage.prim("/World/Group/Subcomponent")?.is_subcomponent()?);
assert_eq!(
stage
.prim("/World/InvalidComponentParent/Component")?
.kind()?
.as_deref(),
Some("component")
);
assert!(!stage.prim("/World/InvalidComponentParent/Component")?.is_model()?);
assert!(!stage.prim("/World/InvalidComponentParent/Component")?.is_component()?);
Ok(())
}
fn model_flags(stage: &Stage, path: &str) -> Result<(bool, bool, bool)> {
let prim = stage.prim(path)?;
Ok((prim.is_model()?, prim.is_group()?, prim.is_component()?))
}
fn custom_kinds_registry() -> Result<Arc<usd::SchemaRegistry>> {
static SITE: &usd::SchemaFamily<'_> = &usd::SchemaFamily::new("site", &[]).kinds(&[
kind::Decl::new("chargroup").base("assembly"),
kind::Decl::new("prop").base("component"),
kind::Decl::new("rig").base("model"),
kind::Decl::new("rivet").base("subcomponent"),
kind::Decl::new("site_root"),
]);
Ok(usd::SchemaRegistry::builder().register(SITE).build()?)
}
fn open_custom_kinds() -> Result<Stage> {
Stage::builder()
.schema_registry(custom_kinds_registry()?)
.open("fixtures/custom_kinds.usda")
}
const MODEL: (bool, bool, bool) = (true, false, false);
const GROUP: (bool, bool, bool) = (true, true, false);
const COMPONENT: (bool, bool, bool) = (true, false, true);
const NOT_MODEL: (bool, bool, bool) = (false, false, false);
#[test]
fn model_root_and_base() -> Result<()> {
let stage = Stage::open("fixtures/custom_kinds.usda")?;
assert_eq!(model_flags(&stage, "/")?, GROUP);
assert_eq!(model_flags(&stage, "/BuiltIn")?, GROUP);
assert_eq!(model_flags(&stage, "/BuiltIn/Leaf")?, COMPONENT);
assert_eq!(model_flags(&stage, "/BuiltIn/Abstract")?, MODEL);
Ok(())
}
#[test]
fn model_custom_kinds() -> Result<()> {
let stage = open_custom_kinds()?;
assert_eq!(model_flags(&stage, "/Show")?, GROUP);
assert_eq!(model_flags(&stage, "/Show/Hero")?, COMPONENT);
assert_eq!(model_flags(&stage, "/Show/Squad")?, GROUP);
assert_eq!(model_flags(&stage, "/Show/Squad/Unit")?, COMPONENT);
assert_eq!(model_flags(&stage, "/Show/Rig")?, MODEL);
assert_eq!(model_flags(&stage, "/Show/Marker")?, NOT_MODEL);
assert_eq!(model_flags(&stage, "/Show/Marker/Prop")?, NOT_MODEL);
let rivet = stage.prim("/Show/Hero/Rivet")?;
assert!(rivet.is_subcomponent()? && !rivet.is_model()?);
assert!(!stage.prim("/Show/Hero")?.is_subcomponent()?);
Ok(())
}
#[test]
fn model_custom_kind_instance() -> Result<()> {
let stage = open_custom_kinds()?;
let proto = stage.prim("/Show/Crowd")?.prototype()?.expect("Crowd is an instance");
assert_eq!(model_flags(&stage, proto.as_str())?, GROUP);
let member = proto.append_path("Member")?;
assert_eq!(model_flags(&stage, member.as_str())?, COMPONENT);
assert_eq!(model_flags(&stage, "/Show/Crowd")?, GROUP);
assert_eq!(model_flags(&stage, "/Show/Crowd/Member")?, COMPONENT);
Ok(())
}
#[test]
fn model_kinds_undeclared() -> Result<()> {
let stage = Stage::open("fixtures/custom_kinds.usda")?;
for path in [
"/Show",
"/Show/Hero",
"/Show/Rig",
"/Show/Marker",
"/Show/Marker/Prop",
"/Show/Squad",
"/Show/Squad/Unit",
"/Show/Crowd",
"/Show/Crowd/Member",
] {
assert_eq!(model_flags(&stage, path)?, NOT_MODEL, "{path}");
}
assert!(!stage.prim("/Show/Hero/Rivet")?.is_subcomponent()?);
assert_eq!(model_flags(&stage, "/")?, GROUP);
assert_eq!(model_flags(&stage, "/BuiltIn/Leaf")?, COMPONENT);
let proto = stage.prim("/Show/Crowd")?.prototype()?.expect("Crowd is an instance");
assert_eq!(model_flags(&stage, proto.as_str())?, GROUP);
let member = proto.append_path("Member")?;
assert_eq!(model_flags(&stage, member.as_str())?, NOT_MODEL);
Ok(())
}
#[test]
fn model_follows_kind_edit() -> Result<()> {
let stage = open_custom_kinds()?;
assert_eq!(model_flags(&stage, "/Show/Hero")?, COMPONENT);
assert_eq!(model_flags(&stage, "/Show/Squad")?, GROUP);
stage.prim("/Show")?.set_kind("site_root")?;
assert_eq!(model_flags(&stage, "/Show")?, NOT_MODEL);
assert_eq!(model_flags(&stage, "/Show/Hero")?, NOT_MODEL);
assert_eq!(model_flags(&stage, "/Show/Squad")?, NOT_MODEL);
assert_eq!(model_flags(&stage, "/Show/Squad/Unit")?, NOT_MODEL);
stage.prim("/Show")?.set_kind("chargroup")?;
assert_eq!(model_flags(&stage, "/Show")?, GROUP);
assert_eq!(model_flags(&stage, "/Show/Hero")?, COMPONENT);
assert_eq!(model_flags(&stage, "/Show/Squad")?, GROUP);
assert_eq!(model_flags(&stage, "/Show/Squad/Unit")?, COMPONENT);
Ok(())
}
#[test]
fn model_kinds_per_stage() -> Result<()> {
let declared = open_custom_kinds()?;
let plain = Stage::open("fixtures/custom_kinds.usda")?;
for _ in 0..2 {
assert_eq!(model_flags(&declared, "/Show/Hero")?, COMPONENT);
assert_eq!(model_flags(&plain, "/Show/Hero")?, NOT_MODEL);
assert_eq!(model_flags(&declared, "/Show")?, GROUP);
assert_eq!(model_flags(&plain, "/Show")?, NOT_MODEL);
}
Ok(())
}
#[test]
fn prim_status_bits() -> Result<()> {
let stage = open_stage_queries_fixture()?;
assert_eq!(
stage.prim_status("/World/ClassParent/Child")?,
PrimStatus::ACTIVE | PrimStatus::LOADED | PrimStatus::DEFINED | PrimStatus::ABSTRACT
);
assert_eq!(
stage.prim_status("/World/Instance")?,
PrimStatus::ACTIVE | PrimStatus::LOADED | PrimStatus::DEFINED | PrimStatus::INSTANCE
);
Ok(())
}
#[test]
fn traverse_default() -> Result<()> {
let stage = open_stage_queries_fixture()?;
let mut prims = Vec::new();
stage.traverse(PrimPredicate::DEFAULT, |p| prims.push(p.as_str().to_string()))?;
assert!(prims.contains(&"/World".to_string()));
assert!(prims.contains(&"/World/ActiveParent".to_string()));
assert!(prims.contains(&"/World/ActiveParent/Child".to_string()));
assert!(prims.contains(&"/World/Instance".to_string()));
assert!(!prims.contains(&"/World/InactiveParent".to_string()));
assert!(!prims.contains(&"/World/InactiveParent/Child".to_string()));
assert!(!prims.contains(&"/World/OverOnly".to_string()));
assert!(!prims.contains(&"/World/OverParent".to_string()));
assert!(!prims.contains(&"/World/OverParent/Child".to_string()));
assert!(!prims.contains(&"/World/ClassParent".to_string()));
assert!(!prims.contains(&"/World/ClassParent/Child".to_string()));
Ok(())
}
#[test]
fn traverse_all_predicate() -> Result<()> {
let stage = open_stage_queries_fixture()?;
let mut prims = Vec::new();
stage.traverse(PrimPredicate::ALL, |p| prims.push(p.as_str().to_string()))?;
assert!(prims.contains(&"/World/InactiveParent".to_string()));
assert!(!prims.contains(&"/World/InactiveParent/Child".to_string()));
assert!(prims.contains(&"/World/OverOnly".to_string()));
assert!(prims.contains(&"/World/OverParent/Child".to_string()));
assert!(prims.contains(&"/World/ClassParent".to_string()));
assert!(prims.contains(&"/World/ClassParent/Child".to_string()));
Ok(())
}
#[test]
fn custom_predicate() -> Result<()> {
let stage = open_stage_queries_fixture()?;
let predicate = PrimPredicate::new(PrimStatus::ACTIVE | PrimStatus::DEFINED, PrimStatus::empty());
let mut prims = Vec::new();
stage.traverse(predicate, |p| prims.push(p.as_str().to_string()))?;
assert!(prims.contains(&"/World/ClassParent".to_string()));
assert!(prims.contains(&"/World/ClassParent/Child".to_string()));
assert!(!prims.contains(&"/World/InactiveParent".to_string()));
assert!(!prims.contains(&"/World/OverOnly".to_string()));
Ok(())
}
fn author_raw_field(attr: &usd::Attribute, field: &str, value: sdf::Value) -> Result<()> {
let stage = attr.stage();
let root_id = stage.root_layer().identifier().to_string();
stage.layer_mut(&root_id).expect("root layer is live").edit(|e| {
e.attribute_mut(attr.path())?
.expect("the attribute spec is on the root layer")
.set(field, value);
Ok(())
})?;
Ok(())
}
fn in_memory_stage() -> Result<Stage> {
Stage::builder().in_memory("anon.usda")
}
#[test]
fn author_default_prim() -> Result<()> {
let stage = in_memory_stage()?;
stage.set_default_prim("World")?;
stage.define_prim("/World")?.set_type_name("Xform")?;
assert_eq!(stage.default_prim().as_deref(), Some("World"));
Ok(())
}
#[test]
fn default_prim_rejects_non_prim() -> Result<()> {
let stage = in_memory_stage()?;
for name in ["World.attr", "/", ""] {
let err = stage.set_default_prim(name).unwrap_err();
assert!(
matches!(err, StageAuthoringError::Layer(sdf::AuthoringError::InvalidPath { .. })),
"{name} names no prim"
);
}
stage.set_default_prim("/World/Char")?;
assert_eq!(stage.default_prim().as_deref(), Some("/World/Char"));
Ok(())
}
#[test]
fn default_prim_accepts_nested() -> Result<()> {
let stage = in_memory_stage()?;
stage.set_default_prim("World/Mesh")?;
assert_eq!(stage.default_prim().as_deref(), Some("World/Mesh"));
Ok(())
}
#[test]
fn file_loaded_stage_is_editable() -> Result<()> {
let stage = Stage::open(&composition_path("subLayer/sublayer_same_folder.usda"))?;
stage.define_prim("/X")?;
stage.set_default_prim("World")?;
assert_eq!(stage.default_prim().as_deref(), Some("World"));
Ok(())
}
#[test]
fn edit_target_out_of_range() -> Result<()> {
let stage = in_memory_stage()?;
let err = stage
.set_edit_target(EditTarget::for_layer("missing-layer"))
.unwrap_err();
assert!(matches!(err, StageAuthoringError::LayerNotFound { .. }));
Ok(())
}
#[test]
fn edit_target_local_is_identity() -> Result<()> {
let target = EditTarget::for_layer("test");
let path = sdf::path("/A/B")?;
assert_eq!(target.map_to_spec_path(&path), Some(path));
Ok(())
}
#[test]
fn variant_target_maps_selection() -> Result<()> {
let target = EditTarget::for_local_direct_variant("test", sdf::path("/Prim{set=sel}")?)?;
assert_eq!(
target.map_to_spec_path(&sdf::path("/Prim/child")?),
Some(sdf::path("/Prim{set=sel}child")?)
);
assert_eq!(
target.map_to_spec_path(&sdf::path("/Prim.attr")?),
Some(sdf::path("/Prim{set=sel}.attr")?)
);
assert_eq!(
target.map_to_spec_path(&sdf::path("/Other")?),
Some(sdf::path("/Other")?)
);
Ok(())
}
#[test]
fn edit_context_rejects_bad_target() -> Result<()> {
let stage = in_memory_stage()?;
let before = stage.edit_target().layer_identifier().to_string();
let result = stage.edit_context(EditTarget::for_layer("missing-layer"));
assert!(matches!(result, Err(StageAuthoringError::LayerNotFound { .. })));
assert_eq!(stage.edit_target().layer_identifier(), before);
Ok(())
}
#[test]
fn define_prim_at_variant_leaf_errors() -> Result<()> {
let stage = in_memory_stage()?;
let root = stage.edit_target().layer_identifier().to_string();
stage.define_prim("/Prim")?;
stage.set_edit_target(EditTarget::for_local_direct_variant(
root,
sdf::path("/Prim{set=sel}")?,
)?)?;
assert!(matches!(
stage.define_prim("/Prim"),
Err(StageAuthoringError::Layer(sdf::AuthoringError::InvalidPath { .. }))
));
Ok(())
}
fn inherit_stage() -> Result<Stage> {
let stage = in_memory_stage()?;
stage.define_prim("/_Class")?;
stage.define_prim("/Prim")?.set_metadata(
sdf::FieldKey::InheritPaths.as_str(),
sdf::Value::PathListOp(sdf::PathListOp::prepended([sdf::path("/_Class")?])),
)?;
Ok(stage)
}
#[test]
fn edit_target_for_reference_node() -> Result<()> {
let stage = Stage::open(&fixture_path("ref_external.usda"))?;
let target = stage.edit_target_for_node(&sdf::path("/World/MyPrim")?, EditTargetArc::Reference)?;
assert!(target.layer_identifier().ends_with("ref_target.usda"));
assert_eq!(
target.map_to_spec_path(&sdf::path("/World/MyPrim/Child")?),
Some(sdf::path("/Source/Child")?)
);
Ok(())
}
#[test]
fn edit_target_for_inherit_node() -> Result<()> {
let stage = inherit_stage()?;
let target = stage.edit_target_for_node(&sdf::path("/Prim")?, EditTargetArc::Inherit)?;
assert_eq!(target.layer_identifier(), stage.root_layer().identifier());
assert_eq!(
target.map_to_spec_path(&sdf::path("/Prim/Child")?),
Some(sdf::path("/_Class/Child")?)
);
Ok(())
}
#[test]
fn edit_target_no_matching_arc() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Prim")?;
assert!(matches!(
stage.edit_target_for_node(&sdf::path("/Prim")?, EditTargetArc::Reference),
Err(StageAuthoringError::NoArcNode { .. })
));
Ok(())
}
#[test]
fn arc_target_in_variant() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def Scope "P" (
variants = {
string v = "x"
}
prepend variantSets = "v"
)
{
variantSet "v" = {
"x" {
def Scope "_C"
{
double val = 1
}
def Scope "Child" (
inherits = </P/_C>
)
{
}
}
}
}
"#,
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P/Child.val")?.get::<f64>()?,
Some(1.0),
"the in-variant class composes onto the inheritor"
);
let target = stage.edit_target_for_node(&sdf::path("/P/Child")?, EditTargetArc::Inherit)?;
assert_eq!(
target.map_to_spec_path(&sdf::path("/P/Child")?),
Some(sdf::path("/P{v=x}_C")?),
"the class spec lives inside the variant, so the target maps there"
);
assert_eq!(
target.map_to_spec_path(&sdf::path("/Unrelated")?),
Some(sdf::path("/Unrelated")?),
"the class map's root identity survives the qualifier composition"
);
{
let _ctx = stage.edit_context(target)?;
stage
.create_attribute("/P/Child.extra", "double")?
.set(sdf::Value::Double(4.0))?;
}
assert_eq!(
stage.attribute("/P/Child.extra")?.get::<f64>()?,
Some(4.0),
"the opinion authored at the variant-qualified class path composes back"
);
Ok(())
}
#[test]
fn variant_class_rel() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def Scope "P" (
variants = {
string v = "x"
}
prepend variantSets = "v"
)
{
variantSet "v" = {
"x" {
def Scope "_C"
{
double val = 1
rel self_rel = </P/_C>
}
def Scope "Child" (
inherits = <../_C>
)
{
}
}
}
}
"#,
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P/Child.val")?.get::<f64>()?,
Some(1.0),
"the relatively-inherited in-variant class composes"
);
assert_eq!(
stage.relationship("/P/Child.self_rel")?.targets()?,
vec![sdf::path("/P/Child")?],
"the within-class target translates to the inheritor's image"
);
assert!(
stage.composition_errors().is_empty(),
"no spurious target diagnostics: {:?}",
stage.composition_errors()
);
let target = stage.edit_target_for_node(&sdf::path("/P/Child")?, EditTargetArc::Inherit)?;
assert_eq!(
target.map_to_spec_path(&sdf::path("/P/Child")?),
Some(sdf::path("/P{v=x}_C")?),
"the relative form maps like the absolute one"
);
Ok(())
}
#[test]
fn variant_ref_path_error() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Target")?;
stage.define_prim("/P")?.set_metadata(
sdf::FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
asset_path: String::new(),
prim_path: sdf::path("/Target{v=a}")?,
..Default::default()
}])),
)?;
assert!(stage.prim("/P")?.is_valid()?);
assert!(
stage
.composition_errors()
.iter()
.any(|e| matches!(e, pcp::CompositionDiagnostic::InvalidPrimPath { .. })),
"the selection-bearing prim path is rejected, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn edit_target_authors_into_class() -> Result<()> {
let stage = inherit_stage()?;
let target = stage.edit_target_for_node(&sdf::path("/Prim")?, EditTargetArc::Inherit)?;
{
let _ctx = stage.edit_context(target)?;
stage.define_prim("/Prim/Child")?;
}
assert!(stage.prim("/Prim/Child")?.is_valid()?);
assert!(stage.root_layer().data().has_spec(&sdf::path("/_Class/Child")?));
assert!(!stage.root_layer().data().has_spec(&sdf::path("/Prim/Child")?));
Ok(())
}
#[test]
fn map_spec_path_remaps_embedded_target() -> Result<()> {
let stage = Stage::open(&fixture_path("ref_external.usda"))?;
let target = stage.edit_target_for_node(&sdf::path("/World/MyPrim")?, EditTargetArc::Reference)?;
assert_eq!(
target.map_to_spec_path(&sdf::path("/World/MyPrim.rel[/World/MyPrim/Child]")?),
Some(sdf::path("/Source.rel[/Source/Child]")?)
);
Ok(())
}
#[test]
fn map_spec_path_rejects_outside_target() -> Result<()> {
let stage = Stage::open(&fixture_path("ref_external.usda"))?;
let target = stage.edit_target_for_node(&sdf::path("/World/MyPrim")?, EditTargetArc::Reference)?;
assert_eq!(
target.map_to_spec_path(&sdf::path("/World/MyPrim.rel[/Elsewhere]")?),
None
);
Ok(())
}
#[test]
fn map_spec_path_local_keeps_target() -> Result<()> {
let target = EditTarget::for_layer("test");
let path = sdf::path("/A.rel[/B].attr")?;
assert_eq!(target.map_to_spec_path(&path), Some(path));
Ok(())
}
#[test]
fn map_spec_path_variant_strips_target() -> Result<()> {
let target = EditTarget::for_local_direct_variant("test", sdf::path("/Prim{set=sel}")?)?;
assert_eq!(
target.map_to_spec_path(&sdf::path("/Prim.rel[/Prim/T]")?),
Some(sdf::path("/Prim{set=sel}.rel[/Prim/T]")?)
);
Ok(())
}
#[test]
fn edit_target_root_matches_default() -> Result<()> {
let stage = in_memory_stage()?;
let target = stage.edit_target_root();
assert_eq!(target.layer_identifier(), stage.root_layer().identifier());
assert_eq!(target.map_to_spec_path(&sdf::path("/A/B")?), Some(sdf::path("/A/B")?));
assert_eq!(stage.edit_target(), target);
Ok(())
}
#[test]
fn edit_target_session() -> Result<()> {
let stage = open_with_session()?;
let target = stage.edit_target_session().expect("session layer");
assert_eq!(
target.layer_identifier(),
stage.session_layer().expect("session layer").identifier()
);
assert!(in_memory_stage()?.edit_target_session().is_none());
Ok(())
}
#[test]
fn layer_stack_id_distinguishes_stages() -> Result<()> {
let stage_a = Stage::builder().in_memory("anon_a.usda")?;
let stage_b = Stage::builder().in_memory("anon_b.usda")?;
let bound = stage_a.edit_target_root();
assert!(matches!(
stage_b.set_edit_target(bound.clone()),
Err(StageAuthoringError::EditTargetWrongStage)
));
let root_b = stage_b.root_layer().identifier().to_string();
assert!(stage_b.set_edit_target(EditTarget::for_layer(root_b)).is_ok());
let stage_a_clone = stage_a.clone();
assert!(stage_a_clone.set_edit_target(bound).is_ok());
Ok(())
}
#[test]
fn anonymous_stages_are_distinct() -> Result<()> {
let stage_a = Stage::builder().in_memory("same.usda")?;
let stage_b = Stage::builder().in_memory("same.usda")?;
assert_ne!(stage_a.root_layer().identifier(), stage_b.root_layer().identifier());
assert!(matches!(
stage_b.set_edit_target(stage_a.edit_target_root()),
Err(StageAuthoringError::EditTargetWrongStage)
));
Ok(())
}
#[test]
fn edit_target_null_and_valid() -> Result<()> {
let valid = EditTarget::for_layer("layer");
assert!(!valid.is_null());
assert!(valid.is_valid());
let null = EditTarget::for_layer("");
assert!(null.is_null());
assert!(!null.is_valid());
Ok(())
}
#[test]
fn edit_target_compose_over() -> Result<()> {
let stage = Stage::open(&fixture_path("ref_external.usda"))?;
let weaker = stage.edit_target_for_node(&sdf::path("/World/MyPrim")?, EditTargetArc::Reference)?;
let stronger = EditTarget::for_local_direct_variant(weaker.layer_identifier(), sdf::path("/Source{set=sel}")?)?;
let composed = stronger.compose_over(&weaker);
assert_eq!(composed.layer_identifier(), weaker.layer_identifier());
assert_eq!(
composed.map_to_spec_path(&sdf::path("/World/MyPrim/Child")?),
Some(sdf::path("/Source{set=sel}Child")?)
);
assert_eq!(EditTarget::for_layer("").compose_over(&weaker), weaker);
Ok(())
}
#[test]
fn compose_over_cross_stack_null() -> Result<()> {
let stage_a = Stage::builder().in_memory("anon_a.usda")?;
let stage_b = Stage::builder().in_memory("anon_b.usda")?;
let composed = stage_a.edit_target_root().compose_over(&stage_b.edit_target_root());
assert!(composed.is_null());
Ok(())
}
#[test]
fn layer_stack_id_same_inputs() -> Result<()> {
let path = fixture_path("ref_external.usda");
let stage_a = Stage::open(&path)?;
let stage_b = Stage::open(&path)?;
assert!(stage_b.set_edit_target(stage_a.edit_target_root()).is_ok());
Ok(())
}
fn reference_with_offset(stage: &Stage, offset: sdf::LayerOffset) -> Result<()> {
stage.define_prim("/Prim")?.set_metadata(
sdf::FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
prim_path: sdf::path("/Source")?,
layer_offset: offset,
..Default::default()
}])),
)?;
Ok(())
}
#[test]
fn arc_target_retimes_time_sample() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Source")?.create_attribute("x", "double")?;
reference_with_offset(&stage, sdf::LayerOffset::new(10.0, 1.0))?;
let target = stage.edit_target_for_node(&sdf::path("/Prim")?, EditTargetArc::Reference)?;
assert_eq!(target.map_to_spec_time(15.0), 5.0);
{
let _ctx = stage.edit_context(target)?;
stage
.attribute("/Prim.x")?
.set_at(sdf::Value::Double(42.0), usd::TimeCode::new(15.0))?;
}
let samples = stage.attribute("/Source.x")?.time_samples()?.expect("samples");
assert_eq!(samples, vec![(5.0, sdf::Value::Double(42.0))]);
assert_eq!(
stage.attribute("/Prim.x")?.get_at::<f64>(usd::TimeCode::new(15.0))?,
Some(42.0)
);
Ok(())
}
#[test]
fn time_sample_times_retimed() -> Result<()> {
let stage = in_memory_stage()?;
stage
.define_prim("/Source")?
.create_attribute("x", "double")?
.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?
.set_at(sdf::Value::Double(3.0), usd::TimeCode::new(10.0))?;
reference_with_offset(&stage, sdf::LayerOffset::new(10.0, 1.0))?;
let attr = stage.attribute("/Prim.x")?;
let map = attr.time_samples()?.expect("samples");
let retimed_keys: Vec<f64> = map.iter().map(|(t, _)| *t).collect();
assert_eq!(retimed_keys, vec![10.0, 20.0]);
assert_eq!(attr.time_sample_times()?, retimed_keys);
assert_eq!(attr.num_time_samples()?, 2);
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(10.0))?, Some(1.0));
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(20.0))?, Some(3.0));
Ok(())
}
#[test]
fn timecode_values_retimed() -> Result<()> {
let stage = in_memory_stage()?;
let source = stage.define_prim("/Source")?;
source
.create_attribute("t", "timecode")?
.set(sdf::Value::TimeCode(sdf::TimeCode(5.0)))?;
source
.create_attribute("many", "timecode[]")?
.set(sdf::Value::TimeCodeVec(vec![sdf::TimeCode(5.0), sdf::TimeCode(10.0)]))?;
source
.create_attribute("animated", "timecode")?
.set_at(sdf::Value::TimeCode(sdf::TimeCode(5.0)), usd::TimeCode::new(0.0))?;
reference_with_offset(&stage, sdf::LayerOffset::new(10.0, 2.0))?;
let scalar = Some(sdf::Value::TimeCode(sdf::TimeCode(20.0)));
let array = Some(sdf::Value::TimeCodeVec(vec![sdf::TimeCode(20.0), sdf::TimeCode(30.0)]));
assert_eq!(stage.attribute("/Prim.t")?.get::<sdf::Value>()?, scalar);
assert_eq!(stage.attribute("/Prim.many")?.get::<sdf::Value>()?, array);
let at = usd::TimeCode::new(0.0);
assert_eq!(stage.attribute("/Prim.t")?.get_at::<sdf::Value>(at)?, scalar);
assert_eq!(stage.attribute("/Prim.many")?.get_at::<sdf::Value>(at)?, array);
assert_eq!(stage.attribute_query("/Prim.t")?.get_at::<sdf::Value>(at)?, scalar);
let animated = stage.attribute("/Prim.animated")?;
let sampled = Some(sdf::Value::TimeCode(sdf::TimeCode(20.0)));
assert_eq!(animated.time_sample_times()?, vec![10.0]);
assert_eq!(animated.get_at::<sdf::Value>(usd::TimeCode::new(10.0))?, sampled);
assert_eq!(
animated.time_samples()?,
Some(vec![(10.0, sdf::Value::TimeCode(sdf::TimeCode(20.0)))])
);
assert_eq!(
stage
.attribute_query("/Prim.animated")?
.get_at::<sdf::Value>(usd::TimeCode::new(10.0))?,
sampled
);
Ok(())
}
#[test]
fn timecode_authoring_retimed() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Source")?.create_attribute("t", "timecode")?;
reference_with_offset(&stage, sdf::LayerOffset::new(10.0, 2.0))?;
let target = stage.edit_target_for_node(&sdf::path("/Prim")?, EditTargetArc::Reference)?;
{
let _ctx = stage.edit_context(target)?;
stage
.attribute("/Prim.t")?
.set(sdf::Value::TimeCode(sdf::TimeCode(20.0)))?;
}
assert_eq!(
stage.attribute("/Source.t")?.get::<sdf::Value>()?,
Some(sdf::Value::TimeCode(sdf::TimeCode(5.0)))
);
assert_eq!(
stage.attribute("/Prim.t")?.get::<sdf::Value>()?,
Some(sdf::Value::TimeCode(sdf::TimeCode(20.0)))
);
Ok(())
}
#[test]
fn default_hides_weaker_samples() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
dir.path().join("sub.usda"),
"#usda 1.0\ndef \"A\"\n{\n double x.timeSamples = { 0: 2.0 }\n}\n",
)?;
fs::write(
&root,
"#usda 1.0\n(\n subLayers = [@./sub.usda@]\n)\ndef \"A\"\n{\n double x = 1.0\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let attr = stage.attribute("/A.x")?;
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(0.0))?, Some(1.0));
assert_eq!(attr.get::<f64>()?, Some(1.0));
assert!(!attr.value_might_be_time_varying()?);
assert_eq!(attr.num_time_samples()?, 0);
assert_eq!(attr.time_samples()?, None, "the hidden samples are not the source");
Ok(())
}
#[test]
fn default_hides_across_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
dir.path().join("refsub.usda"),
"#usda 1.0\ndef \"S\"\n{\n double x.timeSamples = { 0: 2.0 }\n}\n",
)?;
fs::write(
dir.path().join("ref.usda"),
"#usda 1.0\n(\n subLayers = [@./refsub.usda@]\n)\ndef \"S\"\n{\n double x = 1.0\n}\n",
)?;
fs::write(
&root,
"#usda 1.0\ndef \"A\" (\n references = @./ref.usda@</S>\n)\n{\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/A.x")?.get_at::<f64>(usd::TimeCode::new(0.0))?,
Some(1.0)
);
Ok(())
}
#[test]
fn default_hides_inherited_samples() -> Result<()> {
let stage = in_memory_stage()?;
stage
.override_prim("/C")?
.create_attribute("x", "double")?
.set_at(sdf::Value::Double(2.0), usd::TimeCode::new(0.0))?;
stage
.define_prim("/A")?
.set_metadata(
sdf::FieldKey::InheritPaths.as_str(),
sdf::Value::PathListOp(sdf::PathListOp::prepended([sdf::path("/C")?])),
)?
.create_attribute("x", "double")?
.set(sdf::Value::Double(1.0))?;
assert_eq!(
stage.attribute("/A.x")?.get_at::<f64>(usd::TimeCode::new(0.0))?,
Some(1.0)
);
Ok(())
}
#[test]
fn proximal_differs_from_default() -> Result<()> {
let stage = in_memory_stage()?;
stage
.define_prim("/A")?
.create_attribute("x", "double")?
.set(sdf::Value::Double(1.0))?
.set_at(sdf::Value::Double(2.0), usd::TimeCode::new(0.0))?;
let attr = stage.attribute("/A.x")?;
assert_eq!(attr.resolve_info()?.source(), usd::ResolveInfoSource::TimeSamples);
assert_eq!(attr.resolve_info_at(None)?.source(), usd::ResolveInfoSource::Default);
assert!(attr.resolve_info()?.value_source_might_be_time_varying());
assert!(!attr.resolve_info_at(None)?.value_source_might_be_time_varying());
Ok(())
}
#[test]
fn resolve_info_reports_block() -> Result<()> {
let stage = in_memory_stage()?;
let attr = stage.define_prim("/A")?.create_attribute("x", "double")?;
attr.clone().set(sdf::Value::Double(1.0))?;
assert!(stage.attribute("/A.x")?.resolve_info()?.has_authored_value());
attr.block()?;
let info = stage.attribute("/A.x")?.resolve_info()?;
assert!(info.value_is_blocked());
assert!(!info.has_authored_value());
assert!(info.has_authored_value_opinion());
assert_eq!(info.source(), usd::ResolveInfoSource::None);
assert_eq!(stage.attribute("/A.x")?.get::<f64>()?, None);
Ok(())
}
#[test]
fn spec_site_carries_offset() -> Result<()> {
let stage = in_memory_stage()?;
stage
.define_prim("/Source")?
.create_attribute("x", "double")?
.set(sdf::Value::Double(1.0))?;
reference_with_offset(&stage, sdf::LayerOffset::new(10.0, 2.0))?;
let attr = stage.attribute("/Prim.x")?;
let stack = attr.property_stack()?;
let site = stack.first().expect("one contributing spec");
assert_eq!(site.path, sdf::path("/Source.x")?);
assert_eq!(site.offset, sdf::LayerOffset::new(10.0, 2.0));
assert_eq!(attr.resolve_info()?.spec_site(), Some(site));
Ok(())
}
#[test]
fn resolve_info_names_node() -> Result<()> {
let stage = in_memory_stage()?;
stage
.define_prim("/Source")?
.create_attribute("x", "double")?
.set(sdf::Value::Double(1.0))?;
reference_with_offset(&stage, sdf::LayerOffset::IDENTITY)?;
let info = stage.attribute("/Prim.x")?.resolve_info()?;
let node = info.node().expect("an authored opinion names its node");
assert_eq!(node.arc(), pcp::ArcType::Reference);
assert_eq!(node.path(), &sdf::path("/Source")?);
Ok(())
}
#[test]
fn sublayer_contributors_differ() -> Result<()> {
let dir = tempfile::tempdir()?;
for (name, value) in [("strong.usda", 1.0), ("weak.usda", 2.0)] {
fs::write(
dir.path().join(name),
format!("#usda 1.0\ndef \"A\"\n{{\n double x = {value}\n}}\n"),
)?;
}
fs::write(
dir.path().join("root.usda"),
"#usda 1.0\n(\n subLayers = [\n @./strong.usda@,\n @./weak.usda@\n ]\n)\n",
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().expect("utf-8 path"))?;
let attr = stage.attribute("/A.x")?;
let info = attr.resolve_info()?;
let site = info.spec_site().expect("an authored opinion names its spec");
assert!(
site.layer.ends_with("strong.usda"),
"the stronger sublayer answered: {site:?}"
);
let stack = attr.property_stack()?;
assert_eq!(stack.len(), 2);
assert_eq!(site, &stack[0]);
assert_eq!(attr.get::<f64>()?, Some(1.0));
Ok(())
}
#[test]
fn clip_names_its_layer() -> Result<()> {
let stage = Stage::open(&clip_asset("clip_basic"))?;
let attr = stage.attribute("/Model.size")?;
let at = usd::TimeCode::new(10.0);
let info = attr.resolve_info_at(at)?;
assert_eq!(info.source(), usd::ResolveInfoSource::ValueClips);
let site = info.spec_site().expect("a clip names the layer it was read from");
assert!(site.layer.ends_with("clip.usd"), "the clip layer answered: {site:?}");
assert_eq!(site, attr.property_stack_at(at)?.first().expect("a clip site"));
assert!(attr.resolve_info()?.spec_site().is_none());
assert!(info.weaker_sources().is_empty());
Ok(())
}
#[test]
fn clip_interpolated_gap_site() -> Result<()> {
let stage = Stage::open(&fixture_path("clip_missing_interp/root.usda"))?;
let attr = stage.attribute("/Model.size")?;
let at = usd::TimeCode::new(15.0);
assert_eq!(value_f64(&stage, "/Model.size", 15.0), Some(75.0));
let info = attr.resolve_info_at(at)?;
let site = info.spec_site().expect("the stack's site");
assert!(
site.layer.ends_with("manifest.usda"),
"the stack names the manifest, not the clips the samples came from: {site:?}"
);
assert!(attr.property_stack_at(at)?.contains(site), "the stack lists it too");
Ok(())
}
fn expression_stack(dir: &FsPath, opinions: &[(&str, &str)]) -> Result<Stage> {
let bodies: Vec<_> = opinions
.iter()
.map(|(kind, literal)| format!(" uniform {kind} expr = {literal}\n"))
.collect();
sublayer_stack(dir, &bodies)
}
fn sublayer_stack(dir: &FsPath, bodies: &[impl AsRef<str>]) -> Result<Stage> {
for (i, body) in bodies.iter().enumerate() {
let prim = format!("def \"A\"\n{{\n{}}}\n", body.as_ref());
let text = match i + 1 < bodies.len() {
true => format!("#usda 1.0\n(\n subLayers = [@./layer{}.usda@]\n)\n{prim}", i + 1),
false => format!("#usda 1.0\n{prim}"),
};
fs::write(dir.join(format!("layer{i}.usda")), text)?;
}
Stage::open(dir.join("layer0.usda").to_str().expect("utf-8 path"))
}
#[test]
fn chain_names_contributors() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = expression_stack(
dir.path(),
&[("pathExpression", "\"/a// %_\""), ("pathExpression", "\"/b//\"")],
)?;
let attr = stage.attribute("/A.expr")?;
assert_eq!(
attr.get::<sdf::PathExpression>()?,
Some(sdf::PathExpression::parse("/a// /b//"))
);
let info = attr.resolve_info_at(None)?;
let links = info.weaker_sources();
assert_eq!(links.len(), 1);
let site = links[0].spec_site().expect("a link names its spec");
assert!(site.layer.ends_with("layer1.usda"), "{site:?}");
assert_eq!(
links[0].node().expect("a link names its node").arc(),
pcp::ArcType::Root
);
Ok(())
}
#[test]
fn chain_stops_unreferenced() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = expression_stack(
dir.path(),
&[
("pathExpression", "\"/a// %_\""),
("pathExpression", "\"/b// %_\""),
("pathExpression", "\"/c//\""),
("pathExpression", "\"/d//\""),
],
)?;
let attr = stage.attribute("/A.expr")?;
assert_eq!(
attr.get::<sdf::PathExpression>()?.map(|expr| expr.to_string()),
Some("/a// (/b// /c//)".to_string())
);
let info = attr.resolve_info_at(None)?;
let layers = stack_layers(
info.weaker_sources()
.iter()
.map(|link| link.spec_site().expect("a link names its spec").clone())
.collect(),
);
assert_eq!(layers.len(), 2, "the opinion after the last `%_` is not a contributor");
assert!(layers[0].ends_with("layer1.usda"), "{layers:?}");
assert!(layers[1].ends_with("layer2.usda"), "{layers:?}");
Ok(())
}
#[test]
fn chain_unions_sample_times() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e.timeSamples = {
0: \"/s// %_\",
}
",
" pathExpression e.timeSamples = {
1: \"/w//\",
2: \"/x//\",
}
",
],
)?;
let attr = stage.attribute("/A.e")?;
assert_eq!(attr.time_sample_times()?, vec![0.0, 1.0, 2.0]);
assert!(attr.value_might_be_time_varying()?);
assert_eq!(attr.time_samples()?, None);
assert_eq!(
attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(2.0))?,
Some(sdf::PathExpression::parse("/s// /x//"))
);
Ok(())
}
#[test]
fn closed_clip_expression_masks_weaker() -> Result<()> {
let dir = tempfile::tempdir()?;
write_clip_scene(
dir.path(),
r#"#usda 1.0
(
subLayers = [@./weak.usda@]
)
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
string primPath = "/Model"
asset manifestAssetPath = @./manifest.usda@
double2[] active = [(0, 0)]
}
}
)
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
pathExpression e.timeSamples = {
0: "/clip//",
}
}
"#,
)?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def "Model"
{
pathExpression e.timeSamples = {
1: "/y//",
2: "/z//",
}
}
"#,
)?;
let strong = dir.path().join("strong.usda");
fs::write(
&strong,
r#"#usda 1.0
(
subLayers = [@./root.usda@]
)
over "Model"
{
pathExpression e = "/s// %_"
}
"#,
)?;
let stage = Stage::open(strong.to_str().expect("utf-8 path"))?;
let attr = stage.attribute("/Model.e")?;
assert_eq!(
attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(2.0))?,
Some(sdf::PathExpression::parse("/s// /clip//"))
);
assert_eq!(attr.time_sample_times()?, vec![0.0]);
Ok(())
}
#[test]
fn non_finite_sample_time_survives() -> Result<()> {
let stage = in_memory_stage()?;
let attr = stage.define_prim("/A")?.create_attribute("x", "double")?;
attr.set_at(1.0_f64, usd::TimeCode::new(f64::INFINITY))?;
let attr = stage.attribute("/A.x")?;
assert_eq!(attr.time_sample_times()?, vec![f64::INFINITY]);
assert_eq!(attr.num_time_samples()?, 1);
Ok(())
}
#[test]
fn text_clip_masks_weaker_times() -> Result<()> {
let dir = tempfile::tempdir()?;
write_clip_scene(
dir.path(),
r#"#usda 1.0
(
subLayers = [@./weak.usda@]
)
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
string primPath = "/Model"
asset manifestAssetPath = @./manifest.usda@
double2[] active = [(0, 0)]
}
}
)
{
string s
}
"#,
r#"#usda 1.0
def "Model"
{
string s
}
"#,
r#"#usda 1.0
def "Model"
{
string s.timeSamples = {
0: "fixed",
}
}
"#,
)?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def "Model"
{
string s.timeSamples = {
1: "one",
2: "two",
}
}
"#,
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().expect("utf-8 path"))?;
let attr = stage.attribute("/Model.s")?;
for time in [0.0, 1.0, 2.0] {
assert_eq!(
attr.get_at::<String>(usd::TimeCode::new(time))?,
Some("fixed".into()),
"at {time}"
);
}
assert_eq!(attr.time_sample_times()?, vec![0.0]);
Ok(())
}
#[test]
fn text_samples_keep_their_map() -> Result<()> {
let stage = in_memory_stage()?;
let attr = stage.define_prim("/A")?.create_attribute("s", "string")?;
attr.clone().set_at("first", usd::TimeCode::new(0.0))?;
attr.set_at("second", usd::TimeCode::new(1.0))?;
let samples = stage.attribute("/A.s")?.time_samples()?.expect("an authored map");
assert_eq!(samples.len(), 2);
assert_eq!(
stage.attribute("/A.s")?.get_at::<String>(usd::TimeCode::new(1.0))?,
Some("second".into())
);
Ok(())
}
#[test]
fn blocked_sample_closes_chain() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/s// %_\"
",
" pathExpression e.timeSamples = {
0: None,
1: \"/w//\",
}
",
" pathExpression e.timeSamples = {
5: \"/never//\",
}
",
],
)?;
let attr = stage.attribute("/A.e")?;
let read = attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(5.0))?;
assert_eq!(read, Some(sdf::PathExpression::parse("/s// /w//")));
let times = attr.time_sample_times()?;
assert!(!times.contains(&5.0), "the block ended the composition: {times:?}");
assert_eq!(attr.num_time_samples()?, times.len());
Ok(())
}
#[test]
fn blocked_sample_keeps_chain_open() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/s// %_\"
",
" int e.timeSamples = {
0: None,
1: 7,
}
",
" pathExpression e.timeSamples = {
2: \"/y//\",
3: \"/z//\",
}
",
],
)?;
let attr = stage.attribute("/A.e")?;
assert_eq!(
attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(3.0))?,
Some(sdf::PathExpression::parse("/s// /z//"))
);
let times = attr.time_sample_times()?;
assert!(times.contains(&2.0) && times.contains(&3.0), "{times:?}");
assert_eq!(attr.num_time_samples()?, times.len());
Ok(())
}
#[test]
fn ignored_clip_keeps_chain_open() -> Result<()> {
let dir = tempfile::tempdir()?;
write_clip_scene(
dir.path(),
r#"#usda 1.0
(
subLayers = [@./weak.usda@]
)
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
string primPath = "/Model"
asset manifestAssetPath = @./manifest.usda@
double2[] active = [(0, 0)]
}
}
)
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
int e
}
"#,
r#"#usda 1.0
def "Model"
{
int e.timeSamples = {
0: 7,
}
}
"#,
)?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def "Model"
{
pathExpression e.timeSamples = {
1: "/y//",
2: "/z//",
}
}
"#,
)?;
let strong = dir.path().join("strong.usda");
fs::write(
&strong,
r#"#usda 1.0
(
subLayers = [@./root.usda@]
)
over "Model"
{
pathExpression e = "/s// %_"
}
"#,
)?;
let stage = Stage::open(strong.to_str().expect("utf-8 path"))?;
let times = stage.attribute("/Model.e")?.time_sample_times()?;
assert!(times.contains(&1.0) && times.contains(&2.0), "{times:?}");
Ok(())
}
#[test]
fn sample_count_reads_past_ignored() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/s// %_\"
",
" int e.timeSamples = {
0: 3,
}
",
" pathExpression e.timeSamples = {
1: \"/y//\",
2: \"/z//\",
}
",
],
)?;
let attr = stage.attribute("/A.e")?;
assert_eq!(
attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(2.0))?,
Some(sdf::PathExpression::parse("/s// /z//"))
);
let times = attr.time_sample_times()?;
assert_eq!(attr.num_time_samples()?, times.len(), "{times:?}");
assert!(attr.value_might_be_time_varying()?);
Ok(())
}
#[test]
fn text_clip_samples_keep_chain_open() -> Result<()> {
let dir = tempfile::tempdir()?;
write_clip_scene(
dir.path(),
r#"#usda 1.0
(
subLayers = [@./weak.usda@]
)
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
string primPath = "/Model"
asset manifestAssetPath = @./manifest.usda@
double2[] active = [(0, 0)]
}
}
)
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
token e.timeSamples = {
0: "/clip// %_",
}
}
"#,
)?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def "Model"
{
pathExpression e.timeSamples = {
1: "/y//",
2: "/z//",
}
}
"#,
)?;
let strong = dir.path().join("strong.usda");
fs::write(
&strong,
r#"#usda 1.0
(
subLayers = [@./root.usda@]
)
over "Model"
{
pathExpression e = "/s// %_"
}
"#,
)?;
let stage = Stage::open(strong.to_str().expect("utf-8 path"))?;
let attr = stage.attribute("/Model.e")?;
assert_eq!(
attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(2.0))?,
Some(sdf::PathExpression::parse("/s// (/clip// /z//)"))
);
assert_eq!(attr.time_sample_times()?, vec![0.0, 1.0, 2.0]);
Ok(())
}
#[test]
fn manifest_default_chains_sample_times() -> Result<()> {
let dir = tempfile::tempdir()?;
write_clip_scene(
dir.path(),
r#"#usda 1.0
(
subLayers = [@./weak.usda@]
)
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
string primPath = "/Model"
asset manifestAssetPath = @./manifest.usda@
double2[] active = [(0, 0)]
}
}
)
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
pathExpression e = "/manifest// %_"
}
"#,
r#"#usda 1.0
def "Model"
{
}
"#,
)?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def "Model"
{
pathExpression e.timeSamples = {
1: "/y//",
2: "/z//",
}
}
"#,
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().expect("utf-8 path"))?;
let attr = stage.attribute("/Model.e")?;
assert_eq!(
attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(2.0))?,
Some(sdf::PathExpression::parse("/manifest// /z//"))
);
assert_eq!(attr.time_sample_times()?, vec![0.0, 1.0, 2.0]);
Ok(())
}
#[test]
fn text_samples_keep_chain_open() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/s// %_\"
",
" token e.timeSamples = {
0: \"/t// %_\",
}
",
" pathExpression e.timeSamples = {
1: \"/y//\",
2: \"/z//\",
}
",
],
)?;
let attr = stage.attribute("/A.e")?;
assert_eq!(attr.time_sample_times()?, vec![0.0, 1.0, 2.0]);
assert_eq!(attr.num_time_samples()?, 3);
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/s// %_\"
",
" token e.timeSamples = {
0: \"/a//\",
5: \"/b// %_\",
}
",
" pathExpression e.timeSamples = {
7: \"/y//\",
}
",
],
)?;
assert_eq!(stage.attribute("/A.e")?.time_sample_times()?, vec![0.0, 5.0, 7.0]);
Ok(())
}
#[test]
fn clip_chain_unions_sample_times() -> Result<()> {
let dir = tempfile::tempdir()?;
write_clip_scene(
dir.path(),
r#"#usda 1.0
(
subLayers = [@./weak.usda@]
)
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
string primPath = "/Model"
asset manifestAssetPath = @./manifest.usda@
double2[] active = [(0, 0)]
}
}
)
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
pathExpression e.timeSamples = {
0: "/clip// %_",
}
}
"#,
)?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def "Model"
{
pathExpression e.timeSamples = {
1: "/y//",
2: "/z//",
}
}
"#,
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().expect("utf-8 path"))?;
let attr = stage.attribute("/Model.e")?;
assert_eq!(
attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(2.0))?,
Some(sdf::PathExpression::parse("/clip// /z//"))
);
assert_eq!(attr.time_sample_times()?, vec![0.0, 1.0, 2.0]);
Ok(())
}
#[test]
fn sampled_chain_reads_past_ignored() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e.timeSamples = {
0: \"/s// %_\",
}
",
" int e = 3
",
" pathExpression e.timeSamples = {
1: \"/y//\",
2: \"/z//\",
}
",
],
)?;
let attr = stage.attribute("/A.e")?;
assert_eq!(
attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(2.0))?,
Some(sdf::PathExpression::parse("/s// /z//"))
);
assert_eq!(attr.time_sample_times()?, vec![0.0, 1.0, 2.0]);
Ok(())
}
#[test]
fn query_maps_sampled_expression() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("ref.usda"),
"#usda 1.0
def \"Original\"
{
pathExpression e.timeSamples = {
0: \"/Original/child//\",
}
}
",
)?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0
def \"A\" (
references = @./ref.usda@</Original>
)
{
}
",
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().expect("utf-8 path"))?;
let attr = stage.attribute("/A.e")?;
let at = usd::TimeCode::new(0.0);
assert_eq!(
usd::AttributeQuery::new(&attr).get_at::<sdf::PathExpression>(at)?,
attr.get_at::<sdf::PathExpression>(at)?
);
assert_eq!(
attr.get_at::<sdf::PathExpression>(at)?,
Some(sdf::PathExpression::parse("/A/child//"))
);
Ok(())
}
#[test]
fn chain_reads_past_ignored_opinion() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/s// %_\"
",
" int e = 3
",
" pathExpression e.timeSamples = {
1: \"/y//\",
2: \"/z//\",
}
",
],
)?;
let attr = stage.attribute("/A.e")?;
let at = usd::TimeCode::new(2.0);
assert_eq!(
attr.get_at::<sdf::PathExpression>(at)?,
Some(sdf::PathExpression::parse("/s// /z//"))
);
assert_eq!(
usd::AttributeQuery::new(&attr).get_at::<sdf::PathExpression>(at)?,
attr.get_at::<sdf::PathExpression>(at)?
);
assert_eq!(attr.time_sample_times()?, vec![1.0, 2.0]);
Ok(())
}
#[test]
fn text_opinion_maps_across_arc() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("ref.usda"),
"#usda 1.0
def \"Original\"
{
token e = \"child//\"
}
",
)?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0
def \"A\" (
references = @./ref.usda@</Original>
)
{
pathExpression e = \"/A/strong// %_\"
}
",
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().expect("utf-8 path"))?;
assert_eq!(
stage.attribute("/A.e")?.get::<sdf::PathExpression>()?,
Some(sdf::PathExpression::parse("/A/strong// /A/child//"))
);
Ok(())
}
#[test]
fn chain_parses_text() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = expression_stack(
dir.path(),
&[
("pathExpression", "\"/a// %_\""),
("token", "\"/b//\""),
("pathExpression", "\"/c//\""),
],
)?;
let attr = stage.attribute("/A.expr")?;
assert_eq!(
attr.get::<sdf::PathExpression>()?,
Some(sdf::PathExpression::parse("/a// /b//"))
);
let info = attr.resolve_info_at(None)?;
assert_eq!(info.weaker_sources().len(), 1);
assert!(
info.weaker_sources()[0]
.spec_site()
.expect("a link names its spec")
.layer
.ends_with("layer1.usda"),
"the text opinion is the contributor"
);
Ok(())
}
#[test]
fn chain_ends_at_block() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = expression_stack(
dir.path(),
&[
("pathExpression", "\"/a// %_\""),
("pathExpression", "None"),
("pathExpression", "\"/c//\""),
],
)?;
let attr = stage.attribute("/A.expr")?;
assert_eq!(
attr.get::<sdf::PathExpression>()?,
Some(sdf::PathExpression::parse("/a//")),
"the surviving `%_` resolves to nothing"
);
assert!(attr.resolve_info_at(None)?.weaker_sources().is_empty());
Ok(())
}
#[test]
fn chain_follows_tier() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = expression_stack(
dir.path(),
&[("pathExpression", "\"/a// %_\""), ("pathExpression", "\"/b//\"")],
)?;
let attr = stage.attribute("/A.expr")?;
let info = attr.resolve_info_at(usd::TimeCode::new(5.0))?;
assert_eq!(info.source(), usd::ResolveInfoSource::Default);
assert_eq!(info.weaker_sources().len(), 1);
assert!(attr.resolve_info()?.weaker_sources().is_empty());
Ok(())
}
#[test]
fn chain_matches_property_stack() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = expression_stack(
dir.path(),
&[
("pathExpression", "\"/a// %_\""),
("pathExpression", "\"/b// %_\""),
("pathExpression", "\"/c//\""),
],
)?;
let attr = stage.attribute("/A.expr")?;
let info = attr.resolve_info_at(None)?;
let stack = attr.property_stack()?;
let mut composed = vec![info.spec_site().expect("the head names its spec").clone()];
composed.extend(
info.weaker_sources()
.iter()
.map(|link| link.spec_site().expect("a link names its spec").clone()),
);
let positions: Vec<usize> = composed
.iter()
.map(|site| {
stack
.iter()
.position(|listed| listed == site)
.expect("listed in the stack")
})
.collect();
assert!(positions.windows(2).all(|pair| pair[0] < pair[1]), "{positions:?}");
Ok(())
}
#[test]
fn samples_have_no_chain() -> Result<()> {
let stage = in_memory_stage()?;
let samples = stage.define_prim("/S")?.create_attribute("x", "double")?;
samples.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?;
let info = stage.attribute("/S.x")?.resolve_info_at(usd::TimeCode::new(0.0))?;
assert_eq!(info.source(), usd::ResolveInfoSource::TimeSamples);
assert!(info.weaker_sources().is_empty());
Ok(())
}
#[test]
fn sampled_expression_closes() -> Result<()> {
let stage = in_memory_stage()?;
let expr = stage.define_prim("/S")?.create_attribute("e", "pathExpression")?;
expr.set_at(sdf::PathExpression::parse("/s// %_"), usd::TimeCode::new(0.0))?;
assert_eq!(
stage
.attribute("/S.e")?
.get_at::<sdf::PathExpression>(usd::TimeCode::new(0.0))?,
Some(sdf::PathExpression::parse("/s//"))
);
Ok(())
}
#[test]
fn chain_reaches_samples() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/strong// %_\"\n",
" pathExpression e.timeSamples = {\n 0: \"/sampled//\",\n }\n",
],
)?;
let attr = stage.attribute("/A.e")?;
let at = usd::TimeCode::new(0.0);
assert_eq!(
attr.get_at::<sdf::PathExpression>(at)?,
Some(sdf::PathExpression::parse("/strong// /sampled//"))
);
let info = attr.resolve_info_at(at)?;
assert_eq!(info.source(), usd::ResolveInfoSource::Default);
let links = info.weaker_sources();
assert_eq!(links.len(), 1);
assert_eq!(links[0].source(), usd::ResolveInfoSource::TimeSamples);
assert!(
links[0]
.spec_site()
.is_some_and(|site| site.layer.ends_with("layer1.usda")),
"{links:?}"
);
Ok(())
}
#[test]
fn chain_reaches_clip() -> Result<()> {
let dir = tempfile::tempdir()?;
write_clip_scene(
dir.path(),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
string primPath = "/Model"
asset manifestAssetPath = @./manifest.usda@
double2[] active = [(0, 0)]
}
}
)
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
pathExpression e
}
"#,
r#"#usda 1.0
def "Model"
{
pathExpression e.timeSamples = {
0: "/clipped//",
}
}
"#,
)?;
let strong = dir.path().join("strong.usda");
fs::write(
&strong,
r#"#usda 1.0
(
subLayers = [@./root.usda@]
)
over "Model"
{
pathExpression e = "/strong// %_"
}
"#,
)?;
let stage = Stage::open(strong.to_str().expect("utf-8 path"))?;
let attr = stage.attribute("/Model.e")?;
let at = usd::TimeCode::new(0.0);
assert_eq!(
attr.get_at::<sdf::PathExpression>(at)?,
Some(sdf::PathExpression::parse("/strong// /clipped//"))
);
let info = attr.resolve_info_at(at)?;
assert_eq!(info.source(), usd::ResolveInfoSource::Default);
let links = info.weaker_sources();
assert_eq!(links.len(), 1);
assert_eq!(links[0].source(), usd::ResolveInfoSource::ValueClips);
assert!(
links[0]
.spec_site()
.is_some_and(|site| site.layer.ends_with("clip.usda")),
"the link names the clip the value came from: {links:?}"
);
Ok(())
}
#[test]
fn chain_skips_answered_site() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/beside//\"\n pathExpression e.timeSamples = {\n 0: \"/sampled// %_\",\n }\n",
" pathExpression e = \"/weak//\"\n",
],
)?;
let attr = stage.attribute("/A.e")?;
let at = usd::TimeCode::new(0.0);
assert_eq!(
attr.get_at::<sdf::PathExpression>(at)?,
Some(sdf::PathExpression::parse("/sampled// /weak//"))
);
let links = attr.resolve_info_at(at)?.weaker_sources().to_vec();
assert_eq!(links.len(), 1);
assert!(
links[0]
.spec_site()
.is_some_and(|site| site.layer.ends_with("layer1.usda")),
"the `default` beside the samples is not a source: {links:?}"
);
Ok(())
}
#[test]
fn query_matches_composed_read() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/strong// %_\"\n",
" pathExpression e.timeSamples = {\n 0: \"/sampled//\",\n }\n",
],
)?;
let attr = stage.attribute("/A.e")?;
let at = usd::TimeCode::new(0.0);
let query = usd::AttributeQuery::new(&attr);
assert_eq!(
query.get_at::<sdf::PathExpression>(at)?,
attr.get_at::<sdf::PathExpression>(at)?
);
assert_eq!(
query.get_at::<sdf::PathExpression>(at)?,
Some(sdf::PathExpression::parse("/strong// /sampled//"))
);
Ok(())
}
#[test]
fn sampled_expression_query_varies() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e.timeSamples = {\n 0: \"/a// %_\",\n 10: \"/b// %_\",\n }\n",
" pathExpression e = \"/weak//\"\n",
],
)?;
let attr = stage.attribute("/A.e")?;
let query = usd::AttributeQuery::new(&attr);
for time in [0.0, 10.0] {
let at = usd::TimeCode::new(time);
assert_eq!(
query.get_at::<sdf::PathExpression>(at)?,
attr.get_at::<sdf::PathExpression>(at)?,
"at {time}"
);
}
assert_eq!(
attr.get_at::<sdf::PathExpression>(usd::TimeCode::new(10.0))?,
Some(sdf::PathExpression::parse("/b// /weak//"))
);
Ok(())
}
#[test]
fn proximal_reports_varying() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/strong// %_\"\n",
" pathExpression e.timeSamples = {\n 0: \"/sampled//\",\n }\n",
],
)?;
let info = stage.attribute("/A.e")?.resolve_info()?;
assert_eq!(info.source(), usd::ResolveInfoSource::Default);
assert!(info.value_source_might_be_time_varying());
assert!(info.weaker_sources().is_empty(), "a proximal query builds no chain");
Ok(())
}
#[test]
fn chain_omits_foreign_kind() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/strong// %_\"\n",
" int e = 3\n",
" pathExpression e = \"/weak//\"\n",
],
)?;
let attr = stage.attribute("/A.e")?;
assert_eq!(
attr.get::<sdf::PathExpression>()?,
Some(sdf::PathExpression::parse("/strong// /weak//"))
);
let links = attr.resolve_info_at(None)?.weaker_sources().to_vec();
assert_eq!(links.len(), 1, "the int composed nothing: {links:?}");
assert!(
links[0]
.spec_site()
.is_some_and(|site| site.layer.ends_with("layer2.usda")),
"{links:?}"
);
Ok(())
}
#[test]
fn chain_keeps_value_past_block() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = sublayer_stack(
dir.path(),
&[
" pathExpression e = \"/strong// %_\"\n",
" pathExpression e = None\n",
],
)?;
let attr = stage.attribute("/A.e")?;
let info = attr.resolve_info_at(None)?;
assert_eq!(
attr.get::<sdf::PathExpression>()?,
Some(sdf::PathExpression::parse("/strong//"))
);
assert_eq!(info.source(), usd::ResolveInfoSource::Default);
assert!(!info.value_is_blocked(), "the block answered nothing");
let sampled = tempfile::tempdir()?;
let stage = sublayer_stack(
sampled.path(),
&[
" pathExpression e = \"/strong// %_\"\n",
" pathExpression e.timeSamples = {\n 0: None,\n }\n",
],
)?;
let attr = stage.attribute("/A.e")?;
let at = usd::TimeCode::new(0.0);
let info = attr.resolve_info_at(at)?;
assert_eq!(
attr.get_at::<sdf::PathExpression>(at)?,
Some(sdf::PathExpression::parse("/strong//"))
);
assert_eq!(info.source(), usd::ResolveInfoSource::Default);
assert!(!info.value_is_blocked(), "the blocked samples answered nothing");
Ok(())
}
#[test]
fn resolve_info_strength_order() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("ref.usda"),
"#usda 1.0
def \"S\"
{
double x = 111.0
}
",
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
concat!(
"#usda 1.0
",
"class \"C\"
{
double x = 999.0
}
",
"def \"A\" (
references = @./ref.usda@</S>
specializes = </C>
)
{
}
",
),
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let attr = stage.attribute("/A.x")?;
assert_eq!(attr.get::<f64>()?, Some(111.0));
assert_eq!(
attr.resolve_info()?.node().map(pcp::ResolveNode::arc),
Some(pcp::ArcType::Reference)
);
let stack: Vec<_> = attr.property_stack()?.into_iter().map(|site| site.path).collect();
assert_eq!(stack, vec![sdf::path("/S.x")?, sdf::path("/C.x")?]);
Ok(())
}
#[test]
fn blocked_field_is_authored() -> Result<()> {
let stage = in_memory_stage()?;
let attr = stage.define_prim("/A")?.create_attribute("x", "double")?;
author_raw_field(&attr, sdf::FieldKey::TimeSamples.as_str(), sdf::Value::ValueBlock)?;
let info = stage.attribute("/A.x")?.resolve_info()?;
assert!(info.has_authored_value_opinion());
assert!(!info.has_authored_value());
assert!(!info.value_is_blocked());
Ok(())
}
#[test]
fn blocked_samples_fall_through() -> Result<()> {
let stage = in_memory_stage()?;
let attr = stage
.define_prim("/A")?
.create_attribute("x", "double")?
.set(sdf::Value::Double(5.0))?;
author_raw_field(&attr, sdf::FieldKey::TimeSamples.as_str(), sdf::Value::ValueBlock)?;
let attr = stage.attribute("/A.x")?;
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(0.0))?, Some(5.0));
assert_eq!(attr.get::<f64>()?, Some(5.0));
Ok(())
}
#[test]
fn shadowed_clip_stays_uncomposed() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
concat!(
"#usda 1.0
",
"def \"Model\" (
",
" clips = {
",
" dictionary default = {
",
" asset[] assetPaths = [@`bad(`@]
",
" string primPath = \"/Model\"
",
" double2[] active = [(0, 0)]
",
" }
",
" }
",
")
{
double size = 42.0
}
",
),
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/Model.size")?.get_at::<f64>(usd::TimeCode::new(0.0))?,
Some(42.0)
);
assert!(stage.composition_errors().is_empty());
Ok(())
}
#[test]
fn default_time_sees_samples() -> Result<()> {
let stage = in_memory_stage()?;
stage
.define_prim("/A")?
.create_attribute("x", "double")?
.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?;
let info = stage.attribute("/A.x")?.resolve_info_at(None)?;
assert_eq!(info.source(), usd::ResolveInfoSource::None);
assert!(!info.has_authored_value(), "no default answers at the default time");
assert!(info.has_authored_value_opinion(), "the samples are authored");
Ok(())
}
#[test]
fn mixed_blocks_track_time() -> Result<()> {
let stage = in_memory_stage()?;
stage
.define_prim("/A")?
.create_attribute("x", "double")?
.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?
.set_at(sdf::Value::ValueBlock, usd::TimeCode::new(10.0))?;
let attr = stage.attribute("/A.x")?;
let live = attr.resolve_info_at(usd::TimeCode::new(0.0))?;
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(0.0))?, Some(1.0));
assert_eq!(live.source(), usd::ResolveInfoSource::TimeSamples);
assert!(live.has_authored_value());
assert!(!live.value_is_blocked());
let blocked = attr.resolve_info_at(usd::TimeCode::new(10.0))?;
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(10.0))?, None);
assert_eq!(blocked.source(), usd::ResolveInfoSource::None);
assert!(!blocked.has_authored_value());
assert!(blocked.value_is_blocked());
assert_eq!(attr.resolve_info()?.source(), usd::ResolveInfoSource::TimeSamples);
Ok(())
}
#[test]
fn blocked_values_report_block() -> Result<()> {
let stage = in_memory_stage()?;
stage
.define_prim("/A")?
.create_attribute("x", "double")?
.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?
.set_at(sdf::Value::Double(3.0), usd::TimeCode::new(10.0))?
.block()?;
let attr = stage.attribute("/A.x")?;
let at = usd::TimeCode::new(5.0);
assert_eq!(attr.get_at::<f64>(at)?, None);
let info = attr.resolve_info_at(at)?;
assert_eq!(info.source(), usd::ResolveInfoSource::None);
assert!(!info.has_authored_value(), "no value survives, matching get_at");
assert!(info.value_is_blocked());
assert!(info.has_authored_value_opinion());
Ok(())
}
#[test]
fn blocked_samples_hide_weaker() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("sub.usda"),
"#usda 1.0
def \"A\"
{
double x.timeSamples = { 0: 9.0 }
}
",
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0
(
subLayers = [@./sub.usda@]
)
def \"A\"
{
}
",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let attr = stage.create_attribute("/A.x", "double")?;
author_raw_field(&attr, sdf::FieldKey::TimeSamples.as_str(), sdf::Value::ValueBlock)?;
let attr = stage.attribute("/A.x")?;
assert_eq!(attr.time_samples()?, None);
assert!(attr.time_sample_times()?.is_empty());
assert_eq!(attr.num_time_samples()?, 0);
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(0.0))?, None);
Ok(())
}
#[test]
fn empty_samples_no_source() -> Result<()> {
let stage = in_memory_stage()?;
let attr = stage
.define_prim("/B")?
.create_attribute("y", "double")?
.set(sdf::Value::Double(7.0))?;
author_raw_field(
&attr,
sdf::FieldKey::TimeSamples.as_str(),
sdf::Value::TimeSamples(vec![]),
)?;
let attr = stage.attribute("/B.y")?;
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(0.0))?, Some(7.0));
assert_eq!(attr.num_time_samples()?, 0);
assert_eq!(attr.resolve_info()?.source(), usd::ResolveInfoSource::Default);
Ok(())
}
#[test]
fn time_sample_times_masked() -> Result<()> {
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/B"])?)
.in_memory("anon.usda")?;
let root_id = stage.root_layer().identifier().to_string();
stage.layer_mut(&root_id).expect("root layer is live").edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/A", sdf::Specifier::Def, "")?;
let mut x = sdf::AttributeSpec::new(e.data_mut(), "/A.x", "double", sdf::Variability::Varying, true)?;
x.set_time_sample(0.0, sdf::Value::Double(1.0))?;
x.set_time_sample(10.0, sdf::Value::Double(3.0))?;
Ok(())
})?;
stage.define_prim("/B")?.create_attribute("y", "double")?;
let masked = stage.attribute("/A.x")?;
assert!(masked.time_sample_times()?.is_empty());
assert_eq!(masked.num_time_samples()?, 0);
Ok(())
}
#[test]
fn edit_target_for_instance_proxy() -> Result<()> {
let stage = Stage::open(&fixture_path("instancing_nested_reference.usda"))?;
let proxy = sdf::path("/World/Inst/OtherChild")?;
let target = stage.edit_target_for_node(&proxy, EditTargetArc::Reference)?;
assert!(target.layer_identifier().ends_with("instancing_nested_reference.usda"));
let proto = stage
.prim("/World/Inst")?
.prototype()?
.expect("instance has a prototype");
let proto_child = proto.append_path(sdf::path("OtherChild")?)?;
let source = target.map_to_spec_path(&proto_child).expect("prototype path maps");
assert_ne!(source, proto_child, "prototype path remaps to the arc source");
assert_ne!(target.map_to_spec_path(&proxy), Some(source));
Ok(())
}
fn clip_asset(name: &str) -> String {
format!(
"{}vendor/core-spec-supplemental-release_dec2025/value_resolution/tests/assets/{name}/entry.usd",
env!("CARGO_WORKSPACE_DIR")
)
}
#[test]
fn clip_reads_needed_assets() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("manifest.usda"),
"#usda 1.0\ndef \"Model\" {\n float size\n}\n",
)?;
fs::write(
dir.path().join("a.usda"),
"#usda 1.0\ndef \"Model\" {\n float size.timeSamples = {0: 1, 20: 3}\n}\n",
)?;
fs::write(dir.path().join("bad.usda"), "#usda 1.0\ndef !!!")?;
for active in ["[(0, 0), (10, 0)]", "[(0, 0), (100, 1)]"] {
fs::write(
dir.path().join("root.usda"),
format!(
r#"#usda 1.0
def "Model" (
clips = {{
dictionary default = {{
asset[] assetPaths = [@a.usda@, @bad.usda@]
asset manifestAssetPath = @manifest.usda@
string primPath = "/Model"
double2[] active = {active}
}}
}}
) {{
float size
}}
"#
),
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().unwrap())?;
for (time, expected) in [(0.0, 1.0), (5.0, 1.5), (10.0, 2.0)] {
assert_eq!(value_f64(&stage, "/Model.size", time), Some(expected), "{active}");
}
}
Ok(())
}
fn value_f64(stage: &Stage, attr: &str, time: f64) -> Option<f64> {
match stage
.attribute(attr)
.unwrap()
.get_at::<sdf::Value>(usd::TimeCode::new(time))
.expect("value_at")
{
Some(sdf::Value::Float(v)) => Some(v as f64),
Some(sdf::Value::Double(v)) => Some(v),
Some(sdf::Value::Int64(v)) => Some(v as f64),
_ => None,
}
}
fn write_clip_scene(dir: &std::path::Path, root_body: &str, manifest_body: &str, clip_body: &str) -> Result<String> {
fs::write(dir.join("root.usda"), root_body)?;
fs::write(dir.join("manifest.usda"), manifest_body)?;
fs::write(dir.join("clip.usda"), clip_body)?;
Ok(dir.join("root.usda").to_string_lossy().into_owned())
}
#[test]
fn clip_missing_asset_recovers() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("clip0.usda"),
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = { 0: 1 }\n}\n",
)?;
fs::write(
dir.path().join("root.usda"),
r#"#usda 1.0
(
defaultPrim = "Model"
)
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip0.usda@, @./clip1.usda@]
string primPath = "/Model"
double2[] active = [(0, 0), (10, 1)]
}
}
)
{
float size
float extra
}
"#,
)?;
let root = dir.path().join("root.usda").to_string_lossy().into_owned();
let stage = Stage::open(&root)?;
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(1.0));
assert_eq!(value_f64(&stage, "/Model.size", 10.0), None);
assert_eq!(value_f64(&stage, "/Model.extra", 10.0), None);
assert!(stage.composition_errors().iter().any(
|error| matches!(error, openusd::pcp::CompositionDiagnostic::UnreadableClip { asset_path, .. }
if asset_path.contains("clip1.usda"))
));
fs::write(
dir.path().join("clip1.usda"),
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = { 10: 2 }\n float extra.timeSamples = { 10: 7 }\n}\n",
)?;
assert_eq!(value_f64(&stage, "/Model.size", 10.0), Some(2.0));
assert_eq!(value_f64(&stage, "/Model.extra", 10.0), Some(7.0));
Ok(())
}
#[test]
fn clip_manifest_block_skips_clip() -> Result<()> {
let dir = tempfile::tempdir()?;
for (name, body) in [
("clipA.usda", "float size.timeSamples = { 0: 0.0 }"),
("clipB.usda", ""),
("clipC.usda", "float size.timeSamples = { 20: 100.0 }"),
("clipD.usda", "float size.timeSamples = { 30: 300.0 }"),
] {
fs::write(
dir.path().join(name),
format!("#usda 1.0\ndef \"Model\"\n{{\n {body}\n}}\n"),
)?;
}
fs::write(
dir.path().join("root.usda"),
r#"#usda 1.0
(
defaultPrim = "Model"
)
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clipA.usda@, @./clipB.usda@, @./clipC.usda@, @./clipD.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0), (10, 1), (20, 2), (30, 3)]
bool interpolateMissingClipValues = true
}
}
)
{
float size
}
"#,
)?;
let root = dir.path().join("root.usda").to_string_lossy().into_owned();
fs::write(
dir.path().join("manifest.usda"),
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
)?;
let stage = Stage::open(&root)?;
assert_eq!(value_f64(&stage, "/Model.size", 15.0), Some(75.0));
assert_eq!(value_f64(&stage, "/Model.size", 20.0), Some(100.0));
let stack = stack_layers(
stage
.attribute("/Model.size")?
.property_stack_at(usd::TimeCode::new(20.0))?,
);
assert!(stack.iter().any(|layer| layer.ends_with("clipC.usda")), "{stack:?}");
fs::write(
dir.path().join("manifest.usda"),
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = { 20: None }\n}\n",
)?;
fs::write(dir.path().join("clipC.usda"), "#usda 1.0\ndef !!!")?;
let stage = Stage::open(&root)?;
assert_eq!(value_f64(&stage, "/Model.size", 15.0), Some(150.0));
assert_eq!(value_f64(&stage, "/Model.size", 20.0), Some(200.0));
let stack = stack_layers(
stage
.attribute("/Model.size")?
.property_stack_at(usd::TimeCode::new(20.0))?,
);
assert!(stack.iter().any(|layer| layer.ends_with("manifest.usda")), "{stack:?}");
assert!(!stack.iter().any(|layer| layer.ends_with("clipC.usda")), "{stack:?}");
Ok(())
}
#[test]
fn clip_time_samples_gathered() -> Result<()> {
let stage = Stage::open(&fixture_path("clip_template/root.usda"))?;
let size = stage.attribute("/Model.size")?;
assert_eq!(size.time_sample_times()?, vec![1.0, 2.0]);
assert_eq!(size.num_time_samples()?, 2);
assert!(size.value_might_be_time_varying()?);
assert_eq!(size.time_samples_in_interval(1.5..=3.0)?, vec![2.0]);
Ok(())
}
#[test]
fn clip_interpolate_missing_boundary_is_a_sample() -> Result<()> {
let stage = Stage::open(&fixture_path("clip_missing_interp/root.usda"))?;
let size = stage.attribute("/Model.size")?;
assert_eq!(size.time_sample_times()?, vec![0.0, 10.0, 20.0]);
let before = value_f64(&stage, "/Model.size", 9.999).unwrap();
assert!((before - 49.995).abs() < 1e-4, "{before}");
assert_eq!(value_f64(&stage, "/Model.size", 10.0), Some(50.0));
Ok(())
}
#[test]
fn clip_basic_overrides_reference() -> Result<()> {
let stage = Stage::open(&clip_asset("clip_basic"))?;
assert_eq!(value_f64(&stage, "/Model.size", 10.0), Some(10.0));
assert_eq!(value_f64(&stage, "/Model.size", 7.0), Some(7.0)); Ok(())
}
#[test]
fn query_clip_fallback() -> Result<()> {
let stage = Stage::open(&clip_asset("clip_basic"))?;
let attr = stage.attribute("/Model.size")?;
let q = attr.query();
for t in [0.0, 7.0, 10.0, 15.0] {
assert_eq!(
q.get_at::<sdf::Value>(usd::TimeCode::new(t))?,
attr.get_at(usd::TimeCode::new(t))?
);
}
assert_eq!(q.get_at::<f32>(usd::TimeCode::new(7.0))?, Some(7.0));
Ok(())
}
#[test]
fn clip_strength_local_vs_reference() -> Result<()> {
let stage = Stage::open(&clip_asset("clip_advanced"))?;
assert_eq!(value_f64(&stage, "/Model.local", 10.0), Some(10.0));
assert_eq!(value_f64(&stage, "/Model.ref", 10.0), Some(-10.0));
Ok(())
}
#[test]
fn clip_local_default_wins() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
)
{
float localDefault = 3
}
"#,
r#"#usda 1.0
def "Model"
{
float localDefault
}
"#,
r#"#usda 1.0
def "Model"
{
float localDefault.timeSamples = {
0: 7
}
}
"#,
)?;
let stage = Stage::open(&root)?;
assert_eq!(value_f64(&stage, "/Model.localDefault", 0.0), Some(3.0));
Ok(())
}
#[test]
fn clip_local_default_no_time_samples() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
)
{
float localDefault = 3
}
"#,
r#"#usda 1.0
def "Model"
{
float localDefault
}
"#,
r#"#usda 1.0
def "Model"
{
float localDefault.timeSamples = {
0: 7,
5: 9,
}
}
"#,
)?;
let stage = Stage::open(&root)?;
let attr = stage.attribute("/Model.localDefault")?;
assert_eq!(value_f64(&stage, "/Model.localDefault", 0.0), Some(3.0));
assert!(attr.time_sample_times()?.is_empty());
assert_eq!(attr.num_time_samples()?, 0);
assert!(!attr.value_might_be_time_varying()?);
Ok(())
}
#[test]
fn clip_shadowed_default_not_varying() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@, @./clip.usda@]
string primPath = "/Model"
double2[] active = [(0, 0), (10, 1)]
}
}
)
{
float size = 3
}
"#,
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = { 0: 7, 5: 9 }\n}\n",
)?;
let stage = Stage::open(&root)?;
let attr = stage.attribute("/Model.size")?;
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(3.0));
assert_eq!(value_f64(&stage, "/Model.size", 12.0), Some(3.0));
assert!(attr.time_sample_times()?.is_empty());
assert!(!attr.value_might_be_time_varying()?);
Ok(())
}
#[test]
fn clip_manifestless_unauthored_no_times() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("clip.usda"),
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = { 0: 10, 4: 20 }\n}\n",
)?;
fs::write(
dir.path().join("root.usda"),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
)
{
float size
float other
}
"#,
)?;
let stage = Stage::open(&dir.path().join("root.usda").to_string_lossy())?;
assert_eq!(stage.attribute("/Model.size")?.time_sample_times()?, vec![0.0, 4.0]);
let other = stage.attribute("/Model.other")?;
assert!(other.time_sample_times()?.is_empty());
assert_eq!(other.num_time_samples()?, 0);
Ok(())
}
#[test]
fn clip_manifestless_unscheduled_clip() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("sampled.usda"),
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = { 0: 1, 4: 2 }\n}\n",
)?;
fs::write(dir.path().join("empty.usda"), "#usda 1.0\ndef \"Model\"\n{\n}\n")?;
fs::write(
dir.path().join("ref.usda"),
"#usda 1.0\n(\n defaultPrim = \"Model\"\n)\ndef \"Model\"\n{\n float size.timeSamples = { 5: 50, 8: 80 }\n}\n",
)?;
fs::write(
dir.path().join("root.usda"),
r#"#usda 1.0
(
defaultPrim = "Model"
)
def "Model" (
references = @./ref.usda@
clips = {
dictionary default = {
asset[] assetPaths = [@./sampled.usda@, @./empty.usda@]
string primPath = "/Model"
double2[] active = [(0, 1)]
}
}
)
{
float size
}
"#,
)?;
let stage = Stage::open(&dir.path().join("root.usda").to_string_lossy())?;
let size = stage.attribute("/Model.size")?;
assert_eq!(size.time_sample_times()?, vec![5.0, 8.0]);
assert_eq!(value_f64(&stage, "/Model.size", 5.0), Some(50.0));
assert_eq!(value_f64(&stage, "/Model.size", 8.0), Some(80.0));
Ok(())
}
#[test]
fn clip_manifestless_held_boundary() -> Result<()> {
let stage = Stage::open(&fixture_path("clip_manifestless_held/root.usda"))?;
let size = stage.attribute("/Model.size")?;
assert_eq!(size.time_sample_times()?, vec![0.0, 10.0]);
assert_eq!(size.num_time_samples()?, 2);
assert!(size.value_might_be_time_varying()?);
assert_eq!(value_f64(&stage, "/Model.size", 5.0), Some(50.0));
assert_eq!(value_f64(&stage, "/Model.size", 9.999), Some(50.0));
assert_eq!(value_f64(&stage, "/Model.size", 10.0), None);
Ok(())
}
#[test]
fn clip_manifestless_interior_empty() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("clip0.usda"),
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = { 0: 0, 2: 2 }\n}\n",
)?;
fs::write(dir.path().join("clip1.usda"), "#usda 1.0\ndef \"Model\"\n{\n}\n")?;
fs::write(
dir.path().join("clip2.usda"),
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = { 20: 20, 22: 22 }\n}\n",
)?;
fs::write(
dir.path().join("root.usda"),
r#"#usda 1.0
(
defaultPrim = "Model"
)
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip0.usda@, @./clip1.usda@, @./clip2.usda@]
string primPath = "/Model"
double2[] active = [(0, 0), (10, 1), (20, 2)]
}
}
)
{
float size
}
"#,
)?;
let stage = Stage::open(&dir.path().join("root.usda").to_string_lossy())?;
let size = stage.attribute("/Model.size")?;
assert_eq!(size.time_sample_times()?, vec![0.0, 2.0, 10.0, 20.0, 22.0]);
assert_eq!(value_f64(&stage, "/Model.size", 5.0), Some(2.0));
assert_eq!(value_f64(&stage, "/Model.size", 9.999), Some(2.0));
assert_eq!(value_f64(&stage, "/Model.size", 10.0), None);
assert_eq!(value_f64(&stage, "/Model.size", 20.0), Some(20.0));
Ok(())
}
#[test]
fn clip_local_timesamples_shadow_clips() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
)
{
float size.timeSamples = {
0: 1,
10: 3,
}
}
"#,
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = { 1: 100, 5: 500 }\n}\n",
)?;
let stage = Stage::open(&root)?;
let size = stage.attribute("/Model.size")?;
assert_eq!(size.time_sample_times()?, vec![0.0, 10.0]);
assert_eq!(size.num_time_samples()?, 2);
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(1.0));
assert_eq!(value_f64(&stage, "/Model.size", 10.0), Some(3.0));
Ok(())
}
#[test]
fn clip_anchor_sublayer() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir(dir.path().join("sub"))?;
fs::write(
dir.path().join("root.usda"),
r#"#usda 1.0
(
subLayers = [@./sub/weak.usda@]
)
over "Model" (
clips = {
dictionary default = {
double2[] times = [(0, 0)]
}
}
)
{
}
"#,
)?;
fs::write(
dir.path().join("sub").join("weak.usda"),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
)
{
float size
}
"#,
)?;
fs::write(
dir.path().join("sub").join("manifest.usda"),
r#"#usda 1.0
def "Model"
{
float size
}
"#,
)?;
fs::write(
dir.path().join("sub").join("clip.usda"),
r#"#usda 1.0
def "Model"
{
float size.timeSamples = {
0: 7
}
}
"#,
)?;
let stage = Stage::open(dir.path().join("root.usda").to_string_lossy().as_ref())?;
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(7.0));
Ok(())
}
#[test]
fn clip_anchor_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::create_dir(dir.path().join("asset"))?;
fs::write(
dir.path().join("root.usda"),
r#"#usda 1.0
def "ShotModel" (
references = @./asset/model.usda@</Model>
)
{
}
"#,
)?;
fs::write(
dir.path().join("asset").join("model.usda"),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
)
{
float size
}
"#,
)?;
fs::write(
dir.path().join("asset").join("manifest.usda"),
r#"#usda 1.0
def "Model"
{
float size
}
"#,
)?;
fs::write(
dir.path().join("asset").join("clip.usda"),
r#"#usda 1.0
def "Model"
{
float size.timeSamples = {
0: 7
}
}
"#,
)?;
let stage = Stage::open(dir.path().join("root.usda").to_string_lossy().as_ref())?;
assert_eq!(value_f64(&stage, "/ShotModel.size", 0.0), Some(7.0));
Ok(())
}
#[test]
fn clip_loses_to_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_split_clip_scene(dir.path(), STRONG_PLAIN, WEAK_CLIPPED)?;
let stage = Stage::open(&root)?;
assert_eq!(value_f64(&stage, "/A.size", 0.0), Some(3.0));
Ok(())
}
#[test]
fn clip_beats_weaker_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_split_clip_scene(dir.path(), STRONG_CLIPPED, WEAK_PLAIN)?;
let stage = Stage::open(&root)?;
assert_eq!(value_f64(&stage, "/A.size", 0.0), Some(7.0));
let info = stage.attribute("/A.size")?.resolve_info_at(usd::TimeCode::new(0.0))?;
assert_eq!(info.source(), usd::ResolveInfoSource::ValueClips);
assert_eq!(
info.node().map(pcp::ResolveNode::arc),
Some(pcp::ArcType::Reference),
"a clip-sourced value names the node that introduced the set"
);
Ok(())
}
#[test]
fn value_queries_agree() -> Result<()> {
let dir = tempfile::tempdir()?;
let clipped = write_clip_scene(
dir.path(),
&clipped_model_body(" float size\n"),
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = {\n 0: 7.0\n }\n}\n",
)?;
let layered = tempfile::tempdir()?;
fs::write(
layered.path().join("sub.usda"),
"#usda 1.0\ndef \"A\"\n{\n double x.timeSamples = { 0: 2.0 }\n}\n",
)?;
fs::write(
layered.path().join("root.usda"),
"#usda 1.0\n(\n subLayers = [@./sub.usda@]\n)\ndef \"A\"\n{\n double x = 1.0\n}\n",
)?;
let cases: Vec<(&str, Stage, &str)> = vec![
("declared but unauthored", authored_shape(|_| Ok(()))?, "/A.x"),
(
"plain default",
authored_shape(|attr| {
attr.set(sdf::Value::Double(1.0))?;
Ok(())
})?,
"/A.x",
),
(
"plain samples",
authored_shape(|attr| {
attr.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?
.set_at(sdf::Value::Double(3.0), usd::TimeCode::new(10.0))?;
Ok(())
})?,
"/A.x",
),
(
"blocked default",
authored_shape(|attr| {
attr.set(sdf::Value::Double(1.0))?.set(sdf::Value::ValueBlock)?;
Ok(())
})?,
"/A.x",
),
(
"blocked timeSamples field",
authored_shape(|attr| {
let attr = attr.set(sdf::Value::Double(5.0))?;
author_raw_field(&attr, sdf::FieldKey::TimeSamples.as_str(), sdf::Value::ValueBlock)?;
Ok(())
})?,
"/A.x",
),
(
"every sample blocked",
authored_shape(|attr| {
attr.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?
.set_at(sdf::Value::Double(3.0), usd::TimeCode::new(10.0))?
.block()?;
Ok(())
})?,
"/A.x",
),
(
"mixed blocked samples",
authored_shape(|attr| {
attr.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?
.set_at(sdf::Value::ValueBlock, usd::TimeCode::new(10.0))?;
Ok(())
})?,
"/A.x",
),
(
"empty sample map",
authored_shape(|attr| {
let attr = attr.set(sdf::Value::Double(1.0))?;
author_raw_field(
&attr,
sdf::FieldKey::TimeSamples.as_str(),
sdf::Value::TimeSamples(Vec::new()),
)?;
Ok(())
})?,
"/A.x",
),
("clip sourced", Stage::open(&clipped)?, "/Model.size"),
(
"default hides weaker samples",
Stage::open(layered.path().join("root.usda").to_str().unwrap())?,
"/A.x",
),
];
for (name, stage, path) in cases {
for time in [-5.0, 0.0, 5.0, 10.0, 20.0] {
assert_queries_agree(&stage, path, time, name);
}
}
Ok(())
}
fn authored_shape(author: impl FnOnce(usd::Attribute) -> Result<()>) -> Result<Stage> {
let stage = in_memory_stage()?;
author(stage.define_prim("/A")?.create_attribute("x", "double")?)?;
Ok(stage)
}
fn assert_queries_agree(stage: &Stage, path: &str, time: f64, case: &str) {
let at = usd::TimeCode::new(time);
let attr = stage.attribute(path).expect("attribute");
let where_ = format!("{case} at t={time}");
let proximal = attr.resolve_info().expect("resolve_info");
let count = attr.num_time_samples().expect("num_time_samples");
let times = attr.time_sample_times().expect("time_sample_times");
assert_eq!(times.len(), count, "{where_}: sample times and count disagree");
if count > 0 {
assert!(
matches!(
proximal.source(),
usd::ResolveInfoSource::TimeSamples | usd::ResolveInfoSource::ValueClips
),
"{where_}: {count} samples reported but the source is {:?}",
proximal.source()
);
}
if attr.value_might_be_time_varying().expect("time varying") {
assert!(
proximal.value_source_might_be_time_varying(),
"{where_}: the attribute varies but its source cannot"
);
}
let info = attr.resolve_info_at(at).expect("resolve_info_at");
let value = attr.get_at::<sdf::Value>(at).expect("get_at");
assert_eq!(
value.is_some(),
info.source() != usd::ResolveInfoSource::None,
"{where_}: the read and the reported source disagree ({:?})",
info.source()
);
if info.has_authored_value() {
assert!(value.is_some(), "{where_}: an authored value that reads back as none");
assert!(
!info.value_is_blocked(),
"{where_}: an authored value cannot be blocked"
);
}
if info.has_authored_value_opinion() {
assert!(
!attr.property_stack_at(at).expect("property_stack_at").is_empty(),
"{where_}: an authored opinion with no spec in the stack"
);
}
}
#[test]
fn unusable_samples_unauthored() -> Result<()> {
for field in [
sdf::Value::TimeSamples(Vec::new()),
sdf::Value::String("not a sample map".into()),
] {
let stage = in_memory_stage()?;
let attr = stage.define_prim("/A")?.create_attribute("x", "double")?;
author_raw_field(&attr, sdf::FieldKey::TimeSamples.as_str(), field.clone())?;
let attr = stage.attribute("/A.x")?;
let modes = [
("proximal", attr.resolve_info()?),
("default time", attr.resolve_info_at(None)?),
("numeric time", attr.resolve_info_at(usd::TimeCode::new(0.0))?),
];
for (mode, info) in modes {
assert!(
!info.has_authored_value_opinion(),
"{field:?} reported as authored by the {mode} query"
);
assert_eq!(info.source(), usd::ResolveInfoSource::None, "{field:?} at {mode}");
assert!(!info.value_is_blocked(), "{field:?} at {mode}");
}
assert_eq!(attr.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?, None, "{field:?}");
assert_eq!(attr.num_time_samples()?, 0, "{field:?}");
}
Ok(())
}
#[test]
fn blocked_samples_every_mode() -> Result<()> {
let stage = in_memory_stage()?;
let attr = stage.define_prim("/A")?.create_attribute("x", "double")?;
author_raw_field(&attr, sdf::FieldKey::TimeSamples.as_str(), sdf::Value::ValueBlock)?;
let attr = stage.attribute("/A.x")?;
for (mode, info) in [
("proximal", attr.resolve_info()?),
("default time", attr.resolve_info_at(None)?),
("numeric time", attr.resolve_info_at(usd::TimeCode::new(0.0))?),
] {
assert!(info.has_authored_value_opinion(), "the block is authored at {mode}");
assert!(!info.has_authored_value(), "a block is not a value at {mode}");
}
Ok(())
}
#[test]
fn clip_contributes_one_site() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
r#"#usda 1.0
def "Model" (
variantSets = "v"
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
variants = {
string v = "a"
}
)
{
float size
variantSet "v" = {
"a" {
}
}
}
"#,
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = {\n 0: 7.0\n }\n}\n",
)?;
let stage = Stage::open(&root)?;
let attr = stage.attribute("/Model.size")?;
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(7.0));
let stack = stack_layers(attr.property_stack_at(usd::TimeCode::new(0.0))?);
let clips = stack.iter().filter(|layer| layer.ends_with("clip.usda")).count();
assert_eq!(clips, 1, "the set contributes one site: {stack:?}");
Ok(())
}
#[test]
fn untimed_clip_reports_no_value() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
&clipped_model_body(
" float size
",
),
"#usda 1.0
def \"Model\"
{
float size
}
",
"#usda 1.0
def \"Model\"
{
float other.timeSamples = {
0: 1.0
}
}
",
)?;
let stage = Stage::open(&root)?;
let attr = stage.attribute("/Model.size")?;
assert_eq!(attr.get_at::<sdf::Value>(usd::TimeCode::new(5.0))?, None);
let info = attr.resolve_info()?;
assert_eq!(info.source(), usd::ResolveInfoSource::None);
assert!(!info.has_authored_value());
assert!(info.value_is_blocked(), "the set owns the property and blocks it");
assert_eq!(attr.resolve_info_at(usd::TimeCode::new(5.0))?.source(), info.source());
Ok(())
}
struct Conformance {
case: &'static str,
reads: bool,
source: usd::ResolveInfoSource,
blocked: bool,
authored: bool,
}
#[test]
fn source_answers_conform() -> Result<()> {
let inactive = tempfile::tempdir()?;
let sourced = tempfile::tempdir()?;
let blocked = tempfile::tempdir()?;
write_clip_scene(
inactive.path(),
&clipped_model_body(" float size\n"),
"#usda 1.0\ndef \"Model\"\n{\n float other\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float other.timeSamples = {\n 0: 1.0\n }\n}\n",
)?;
write_clip_scene(
sourced.path(),
&clipped_model_body(" float size\n"),
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = {\n 0: 7.0\n }\n}\n",
)?;
write_clip_scene(
blocked.path(),
&clipped_model_body(" float size\n"),
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float other.timeSamples = {\n 0: 1.0\n }\n}\n",
)?;
let rows: Vec<(Conformance, Stage, &str)> = vec![
(
Conformance {
case: "missing",
reads: false,
source: usd::ResolveInfoSource::None,
blocked: false,
authored: false,
},
authored_shape(|_| Ok(()))?,
"/A.x",
),
(
Conformance {
case: "default value",
reads: true,
source: usd::ResolveInfoSource::Default,
blocked: false,
authored: true,
},
authored_shape(|attr| {
attr.set(sdf::Value::Double(1.0))?;
Ok(())
})?,
"/A.x",
),
(
Conformance {
case: "default block",
reads: false,
source: usd::ResolveInfoSource::None,
blocked: true,
authored: true,
},
authored_shape(|attr| {
attr.set(sdf::Value::Double(1.0))?.set(sdf::Value::ValueBlock)?;
Ok(())
})?,
"/A.x",
),
(
Conformance {
case: "sample value",
reads: true,
source: usd::ResolveInfoSource::TimeSamples,
blocked: false,
authored: true,
},
authored_shape(|attr| {
attr.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?
.set_at(sdf::Value::Double(3.0), usd::TimeCode::new(10.0))?;
Ok(())
})?,
"/A.x",
),
(
Conformance {
case: "sample block",
reads: false,
source: usd::ResolveInfoSource::None,
blocked: true,
authored: true,
},
authored_shape(|attr| {
attr.set_at(sdf::Value::Double(1.0), usd::TimeCode::new(0.0))?
.set_at(sdf::Value::Double(3.0), usd::TimeCode::new(10.0))?
.block()?;
Ok(())
})?,
"/A.x",
),
(
Conformance {
case: "clip does not own the property",
reads: false,
source: usd::ResolveInfoSource::None,
blocked: false,
authored: false,
},
Stage::open(inactive.path().join("root.usda").to_str().unwrap())?,
"/Model.size",
),
(
Conformance {
case: "clip value",
reads: true,
source: usd::ResolveInfoSource::ValueClips,
blocked: false,
authored: true,
},
Stage::open(sourced.path().join("root.usda").to_str().unwrap())?,
"/Model.size",
),
(
Conformance {
case: "clip block",
reads: false,
source: usd::ResolveInfoSource::None,
blocked: true,
authored: true,
},
Stage::open(blocked.path().join("root.usda").to_str().unwrap())?,
"/Model.size",
),
];
let at = usd::TimeCode::new(5.0);
for (row, stage, path) in rows {
let attr = stage.attribute(path)?;
let info = attr.resolve_info_at(at)?;
assert_eq!(
attr.get_at::<sdf::Value>(at)?.is_some(),
row.reads,
"{}: the value read",
row.case
);
assert_eq!(info.source(), row.source, "{}: the reported source", row.case);
assert_eq!(info.value_is_blocked(), row.blocked, "{}: value_is_blocked", row.case);
assert_eq!(
info.has_authored_value_opinion(),
row.authored,
"{}: has_authored_value_opinion",
row.case
);
assert_eq!(
info.has_authored_value(),
row.reads && row.source != usd::ResolveInfoSource::Fallback,
"{}: has_authored_value",
row.case
);
assert!(!(info.has_authored_value() && info.value_is_blocked()), "{}", row.case);
}
Ok(())
}
#[test]
fn clip_joins_timed_stack() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
&clipped_model_body(" float size\n"),
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = {\n 0: 7.0\n }\n}\n",
)?;
let stage = Stage::open(&root)?;
let attr = stage.attribute("/Model.size")?;
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(7.0));
let timed = stack_layers(attr.property_stack_at(usd::TimeCode::new(0.0))?);
assert!(
timed.iter().any(|layer| layer.ends_with("clip.usda")),
"the clip supplying the value joins the stack: {timed:?}"
);
let plain = stack_layers(attr.property_stack()?);
assert!(
!plain.iter().any(|layer| layer.ends_with("clip.usda")),
"no clip contributes at the default time: {plain:?}"
);
Ok(())
}
#[test]
fn manifest_joins_timed_stack() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
&clipped_model_body(" float size\n"),
"#usda 1.0\ndef \"Model\"\n{\n float size = 3\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float other.timeSamples = {\n 0: 1\n }\n}\n",
)?;
let stage = Stage::open(&root)?;
let attr = stage.attribute("/Model.size")?;
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(3.0));
let timed = stack_layers(attr.property_stack_at(usd::TimeCode::new(0.0))?);
assert!(
timed.iter().any(|layer| layer.ends_with("manifest.usda")),
"the manifest fills the gap, so it is the reported spec: {timed:?}"
);
assert!(!timed.iter().any(|layer| layer.ends_with("clip.usda")), "{timed:?}");
let info = attr.resolve_info_at(usd::TimeCode::new(0.0))?;
assert_eq!(info.source(), usd::ResolveInfoSource::ValueClips);
assert!(
info.spec_site()
.expect("the manifest answered")
.layer
.ends_with("manifest.usda"),
"{:?}",
info.spec_site()
);
Ok(())
}
#[test]
fn timed_stack_keeps_clip() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
&clipped_model_body(" float size = 5\n"),
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
"#usda 1.0\ndef \"Model\"\n{\n float size.timeSamples = {\n 0: 7.0\n }\n}\n",
)?;
let stage = Stage::open(&root)?;
let attr = stage.attribute("/Model.size")?;
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(5.0));
assert_eq!(
attr.resolve_info_at(usd::TimeCode::new(0.0))?.source(),
usd::ResolveInfoSource::Default
);
let timed = stack_layers(attr.property_stack_at(usd::TimeCode::new(0.0))?);
assert!(timed.iter().any(|layer| layer.ends_with("root.usda")), "{timed:?}");
assert!(timed.iter().any(|layer| layer.ends_with("clip.usda")), "{timed:?}");
Ok(())
}
#[test]
fn timed_stack_lists_sets() -> Result<()> {
let dir = tempfile::tempdir()?;
for name in ["a", "b"] {
fs::write(
dir.path().join(format!("manifest_{name}.usda")),
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
)?;
fs::write(
dir.path().join(format!("clip_{name}.usda")),
"#usda 1.0
def \"Model\"
{
float size.timeSamples = {
0: 1.0
}
}
",
)?;
}
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "Model" (
clips = {
dictionary a = {
asset[] assetPaths = [@./clip_a.usda@]
asset manifestAssetPath = @./manifest_a.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
dictionary b = {
asset[] assetPaths = [@./clip_b.usda@]
asset manifestAssetPath = @./manifest_b.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
)
{
float size
}
"#,
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let attr = stage.attribute("/Model.size")?;
let timed = stack_layers(attr.property_stack_at(usd::TimeCode::new(0.0))?);
assert!(timed.iter().any(|layer| layer.ends_with("clip_a.usda")), "{timed:?}");
assert!(timed.iter().any(|layer| layer.ends_with("clip_b.usda")), "{timed:?}");
Ok(())
}
fn clipped_model_body(body: &str) -> String {
format!(
r#"#usda 1.0
def "Model" (
clips = {{
dictionary default = {{
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}}
}}
)
{{
{body}}}
"#
)
}
fn stack_layers(stack: Vec<usd::SpecSite>) -> Vec<String> {
stack.into_iter().map(|site| site.layer).collect()
}
const STRONG_PLAIN: &str = r#"#usda 1.0
def "S"
{
float size = 3
}
"#;
const WEAK_PLAIN: &str = r#"#usda 1.0
def "S"
{
float size = 3
}
"#;
const WEAK_CLIPPED: &str = r#"#usda 1.0
def "S" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/S"
double2[] active = [(0, 0)]
}
}
)
{
float size
}
"#;
const STRONG_CLIPPED: &str = WEAK_CLIPPED;
fn write_split_clip_scene(dir: &FsPath, strong: &str, weak: &str) -> Result<String> {
fs::write(
dir.join("root.usda"),
"#usda 1.0\ndef \"A\" (\n references = [@./strong.usda@</S>, @./weak.usda@</S>]\n)\n{\n}\n",
)?;
fs::write(dir.join("strong.usda"), strong)?;
fs::write(dir.join("weak.usda"), weak)?;
fs::write(
dir.join("manifest.usda"),
"#usda 1.0\ndef \"S\"\n{\n float size\n}\n",
)?;
fs::write(
dir.join("clip.usda"),
"#usda 1.0\ndef \"S\"\n{\n float size.timeSamples = {\n 0: 7\n }\n}\n",
)?;
Ok(dir.join("root.usda").to_string_lossy().into_owned())
}
#[test]
fn clip_applies_without_spec() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("sub.usda"),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
)
{
}
"#,
)?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0\n(\n subLayers = [@./sub.usda@]\n)\ndef \"Model\"\n{\n def \"Child\"\n {\n float size\n }\n}\n",
)?;
fs::write(
dir.path().join("manifest.usda"),
"#usda 1.0\ndef \"Model\"\n{\n def \"Child\"\n {\n float size\n }\n}\n",
)?;
fs::write(
dir.path().join("clip.usda"),
"#usda 1.0\ndef \"Model\"\n{\n def \"Child\"\n {\n float size.timeSamples = {\n 0: 7\n }\n }\n}\n",
)?;
let stage = Stage::open(dir.path().join("root.usda").to_string_lossy().as_ref())?;
assert_eq!(value_f64(&stage, "/Model/Child.size", 0.0), Some(7.0));
Ok(())
}
#[test]
fn clip_added_after_read() -> Result<()> {
let dir = tempfile::tempdir()?;
write_child_clip_files(dir.path(), "Model")?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0\ndef \"Model\"\n{\n def \"Child\"\n {\n float size\n }\n}\n",
)?;
let stage = Stage::open(dir.path().join("root.usda").to_string_lossy().as_ref())?;
assert_eq!(value_f64(&stage, "/Model/Child.size", 0.0), None);
stage
.prim("/Model")?
.set_metadata(sdf::FieldKey::Clips.as_str(), clip_metadata("/Model"))?;
assert_eq!(value_f64(&stage, "/Model/Child.size", 0.0), Some(7.0));
stage.prim("/Model")?.clear_metadata(sdf::FieldKey::Clips.as_str())?;
assert_eq!(value_f64(&stage, "/Model/Child.size", 0.0), None);
Ok(())
}
#[test]
fn clip_added_across_reference() -> Result<()> {
let dir = tempfile::tempdir()?;
write_child_clip_files(dir.path(), "Src")?;
fs::write(
dir.path().join("root.usda"),
concat!(
"#usda 1.0\n",
"def \"Src\"\n{\n def \"Child\"\n {\n float size\n }\n}\n",
"def \"Model\" (\n references = </Src>\n)\n{\n}\n",
),
)?;
let stage = Stage::open(dir.path().join("root.usda").to_string_lossy().as_ref())?;
assert_eq!(value_f64(&stage, "/Model/Child.size", 0.0), None);
stage
.prim("/Src")?
.set_metadata(sdf::FieldKey::Clips.as_str(), clip_metadata("/Src"))?;
assert_eq!(value_f64(&stage, "/Model/Child.size", 0.0), Some(7.0));
Ok(())
}
fn clip_metadata(prim_path: &str) -> sdf::Value {
let set = HashMap::from([
(
"assetPaths".to_string(),
sdf::Value::AssetPathVec(vec!["./clip.usda".into()]),
),
(
"manifestAssetPath".to_string(),
sdf::Value::AssetPath("./manifest.usda".into()),
),
("primPath".to_string(), sdf::Value::String(prim_path.to_string())),
(
"active".to_string(),
sdf::Value::Vec2dVec(vec![openusd::gf::vec2d(0.0, 0.0)]),
),
]);
sdf::Value::Dictionary(HashMap::from([("default".to_string(), sdf::Value::Dictionary(set))]))
}
fn write_child_clip_files(dir: &FsPath, anchor: &str) -> Result<()> {
fs::write(
dir.join("manifest.usda"),
format!("#usda 1.0\ndef \"{anchor}\"\n{{\n def \"Child\"\n {{\n float size\n }}\n}}\n"),
)?;
fs::write(
dir.join("clip.usda"),
format!(
"#usda 1.0\ndef \"{anchor}\"\n{{\n def \"Child\"\n {{\n float size.timeSamples = {{\n 0: 7\n }}\n }}\n}}\n"
),
)?;
Ok(())
}
#[test]
fn clip_metadata_retimed() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("root.usda"),
r#"#usda 1.0
(
subLayers = [@./weak.usda@ (offset = 10)]
)
"#,
)?;
fs::write(
dir.path().join("weak.usda"),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
double2[] times = [(0, 0), (5, 5)]
}
}
)
{
float size
}
"#,
)?;
fs::write(
dir.path().join("manifest.usda"),
r#"#usda 1.0
def "Model"
{
float size
}
"#,
)?;
fs::write(
dir.path().join("clip.usda"),
r#"#usda 1.0
def "Model"
{
float size.timeSamples = {
0: 0,
5: 5
}
}
"#,
)?;
let stage = Stage::open(dir.path().join("root.usda").to_string_lossy().as_ref())?;
assert_eq!(value_f64(&stage, "/Model.size", 10.0), Some(0.0));
assert_eq!(value_f64(&stage, "/Model.size", 15.0), Some(5.0));
Ok(())
}
#[test]
fn clip_initial_jump() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
double2[] times = [(0, 0), (0, 25), (10, 35)]
}
}
)
{
float size
}
"#,
r#"#usda 1.0
def "Model"
{
float size
}
"#,
r#"#usda 1.0
def "Model"
{
float size.timeSamples = {
0: 0.0,
25: 25.0,
35: 35.0
}
}
"#,
)?;
let stage = Stage::open(&root)?;
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(25.0));
assert_eq!(value_f64(&stage, "/Model.size", 5.0), Some(30.0));
Ok(())
}
#[test]
fn clip_multi_active_switch() -> Result<()> {
let stage = Stage::open(&clip_asset("clip_multi"))?;
assert_eq!(value_f64(&stage, "/Model_1.size", 10.0), Some(-10.0));
assert_eq!(value_f64(&stage, "/Model_1.size", 22.0), Some(-26.0));
Ok(())
}
#[test]
fn clip_sets_default_order() -> Result<()> {
let stage = Stage::open(&clip_asset("clip_sets"))?;
assert_eq!(value_f64(&stage, "/DefaultOrderTest.attr", 0.0), Some(10.0));
assert_eq!(value_f64(&stage, "/DefaultOrderTest.attr", 1.0), Some(20.0));
Ok(())
}
#[test]
fn clip_timings_curve() -> Result<()> {
let stage = Stage::open(&clip_asset("clip_timings"))?;
assert_eq!(value_f64(&stage, "/Model.size", 0.0), Some(10.0));
assert_eq!(value_f64(&stage, "/Model.size", 10.0), Some(15.0));
assert_eq!(value_f64(&stage, "/Model.size", 20.0), Some(10.0)); assert_eq!(value_f64(&stage, "/Model.size", 30.0), Some(15.0));
Ok(())
}
fn opinion_layer(identifier: &str, value: f64) -> Result<sdf::Layer> {
let mut layer = sdf::Layer::new_anonymous(identifier);
layer.edit(|e| {
sdf::AttributeSpec::new(e.data_mut(), "/A.x", "double", sdf::Variability::Varying, true)?
.set_default(sdf::Value::Double(value))?;
Ok(())
})?;
Ok(layer)
}
fn authored_sublayers(stage: &Stage) -> Vec<String> {
let root = stage.root_layer();
root.pseudo_root().and_then(|pr| pr.sublayers()).unwrap_or_default()
}
#[test]
fn insert_layer_authors_metadata() -> Result<()> {
let stage = Stage::builder().in_memory("root.usda")?;
let root_id = stage.root_layer().identifier().to_string();
let weak = opinion_layer("weak.usda", 5.0)?;
let weak_id = weak.identifier().to_string();
stage.insert_layer(&root_id, 0, weak, sdf::LayerOffset::IDENTITY)?;
assert_eq!(
stage.attribute("/A.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(5.0))
);
assert_eq!(authored_sublayers(&stage), vec![weak_id]);
Ok(())
}
#[test]
fn remove_layer_clears_metadata() -> Result<()> {
let stage = Stage::builder().in_memory("root.usda")?;
let root_id = stage.root_layer().identifier().to_string();
let weak = opinion_layer("weak.usda", 5.0)?;
let weak_id = weak.identifier().to_string();
stage.insert_layer(&root_id, 0, weak, sdf::LayerOffset::IDENTITY)?;
assert_eq!(
stage.attribute("/A.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
Some(sdf::Value::Double(5.0))
);
assert!(stage.remove_layer(&root_id, &weak_id)?, "a sublayer was removed");
assert_eq!(
stage.attribute("/A.x")?.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
None,
"the removed sublayer's opinion is gone"
);
assert!(
authored_sublayers(&stage).is_empty(),
"the removed sublayer's subLayers entry is gone"
);
Ok(())
}
#[test]
fn insert_layer_into_file_loaded_parent() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(&root, "#usda 1.0\n")?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
let root_id = stage.root_layer().identifier().to_string();
let before = stage.layer_count();
stage.insert_layer(
&root_id,
0,
opinion_layer("weak.usda", 5.0)?,
sdf::LayerOffset::IDENTITY,
)?;
assert_eq!(
stage.layer_count(),
before + 1,
"the inserted sublayer adds exactly one node"
);
Ok(())
}
#[test]
fn insert_layer_missing_parent() -> Result<()> {
let stage = Stage::builder().in_memory("root.usda")?;
let err = stage
.insert_layer(
"nope.usda",
0,
opinion_layer("weak.usda", 5.0)?,
sdf::LayerOffset::IDENTITY,
)
.unwrap_err();
assert!(matches!(err, StageAuthoringError::LayerNotFound { .. }));
assert_eq!(stage.layer_count(), 1, "no node added for a missing parent");
Ok(())
}
#[test]
fn from_layers_dedups_order() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("shared.usda"), "#usda 1.0\n")?;
fs::write(
dir.path().join("session.usda"),
"#usda 1.0\n(\n subLayers = [@shared.usda@]\n)\n",
)?;
let root = dir.path().join("root.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@shared.usda@]\n)\n")?;
let stage = Stage::builder()
.session_layer(dir.path().join("session.usda").to_string_lossy().into_owned())
.open(root.to_str().expect("utf-8 temp path"))?;
assert_eq!(
stage.layer_count(),
3,
"the duplicate shared layer collapses to one node"
);
let ids = stage.layer_identifiers();
let unique: std::collections::HashSet<_> = ids.iter().collect();
assert_eq!(ids.len(), unique.len(), "no duplicate id survives");
assert!(
stage.root_layer().identifier().ends_with("root.usda"),
"the root stays the first non-session layer after dedup"
);
Ok(())
}
#[test]
fn from_layers_root_shared_with_session() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(dir.path().join("dep.usda"), "#usda 1.0\n")?;
fs::write(
dir.path().join("shared.usda"),
"#usda 1.0\n(\n subLayers = [@dep.usda@]\n)\n",
)?;
fs::write(
dir.path().join("session.usda"),
"#usda 1.0\n(\n subLayers = [@shared.usda@]\n)\n",
)?;
let shared = dir.path().join("shared.usda");
let stage = Stage::builder()
.session_layer(dir.path().join("session.usda").to_string_lossy().into_owned())
.open(shared.to_str().expect("utf-8 temp path"))?;
assert_eq!(stage.layer_count(), 3, "the shared root/session layer is one node");
assert!(
stage.root_layer().identifier().ends_with("shared.usda"),
"the root resolves to the shared layer, not the next dependency"
);
Ok(())
}
#[test]
fn define_prim() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/World")?.set_type_name("Xform")?;
stage.define_prim("/World/Mesh")?.set_type_name("Mesh")?;
assert!(stage.prim("/World")?.is_defined()?);
assert!(stage.prim("/World/Mesh")?.is_defined()?);
assert_eq!(stage.prim("/World")?.type_name()?.as_deref(), Some("Xform"));
assert_eq!(stage.prim("/World/Mesh")?.type_name()?.as_deref(), Some("Mesh"));
Ok(())
}
#[test]
fn define_typed_prim() -> Result<()> {
let stage = Stage::builder()
.mask(usd::StagePopulationMask::new(["/Keep"])?)
.in_memory("anon.usda")?;
stage.override_prim("/Keep")?;
assert_eq!(
stage.define_typed_prim("/Keep", "Xform")?.type_name()?.as_deref(),
Some("Xform")
);
assert!(!stage.define_typed_prim("/Drop", "Scope")?.is_valid()?);
let layer = stage.root_layer();
assert_eq!(
layer.prim("/Keep")?.expect("upgraded").specifier(),
Some(sdf::Specifier::Def)
);
let drop = layer.prim("/Drop")?.expect("authored outside the mask");
assert_eq!(drop.specifier(), Some(sdf::Specifier::Def));
assert_eq!(drop.type_name().as_deref(), Some("Scope"));
Ok(())
}
#[test]
fn authoring_invalidates_cached_miss() -> Result<()> {
let stage = in_memory_stage()?;
assert!(!stage.prim("/World")?.is_valid()?);
stage.define_prim("/World")?.set_type_name("Xform")?;
assert!(stage.prim("/World")?.is_valid()?);
assert_eq!(stage.prim("/World")?.type_name()?.as_deref(), Some("Xform"));
Ok(())
}
#[test]
fn override_prim() -> Result<()> {
let stage = in_memory_stage()?;
stage.override_prim("/A/B")?;
assert_eq!(stage.prim("/A")?.specifier()?, Some(sdf::Specifier::Over));
assert_eq!(stage.prim("/A/B")?.specifier()?, Some(sdf::Specifier::Over));
Ok(())
}
#[test]
fn permission_edit_does_not_inert_opinion() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n\ndef \"Class\"\n{\n custom double attr = 5\n}\n\ndef \"Inst\" (\n inherits = </Class>\n)\n{\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/Inst.attr")?.get::<sdf::Value>()?,
Some(sdf::Value::Double(5.0)),
"the inherited opinion contributes before the permission edit",
);
stage.prim("/Class")?.set_metadata(
sdf::FieldKey::Permission.as_str(),
sdf::Value::Permission(sdf::Permission::Private),
)?;
assert_eq!(
stage.attribute("/Inst.attr")?.get::<sdf::Value>()?,
Some(sdf::Value::Double(5.0)),
"permission is inert metadata; the inherited opinion still resolves",
);
Ok(())
}
#[test]
fn clips_edit_resolves_live() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
let clip = dir.path().join("clip.usda");
let manifest = dir.path().join("manifest.usda");
let referenced = dir.path().join("ref.usda");
fs::write(
&clip,
"#usda 1.0\n\ndef \"Model\"\n{\n double size.timeSamples = {\n 0: 0,\n 10: 10,\n }\n}\n",
)?;
fs::write(&manifest, "#usda 1.0\n\ndef \"Model\"\n{\n double size\n}\n")?;
fs::write(
&referenced,
"#usda 1.0\n\ndef \"Model\"\n{\n double size.timeSamples = {\n 0: -1,\n 10: -10,\n }\n}\n",
)?;
fs::write(
&root,
format!(
"#usda 1.0\n\ndef \"Model\" (\n references = @{}@</Model>\n)\n{{\n}}\n",
referenced.display()
),
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(
value_f64(&stage, "/Model.size", 10.0),
Some(-10.0),
"the reference time sample resolves before any clips are authored",
);
let api = usd::ClipsAPI::from_prim_unchecked(stage.prim("/Model")?);
api.set_clip_asset_paths("default", vec![clip.display().to_string()])?;
api.set_clip_prim_path("default", "/Model")?;
api.set_clip_manifest_asset_path("default", manifest.display().to_string())?;
api.set_clip_active("default", vec![gf::vec2d(0.0, 0.0)])?;
assert_eq!(
value_f64(&stage, "/Model.size", 10.0),
Some(10.0),
"the authored clip set overrides the reference time sample",
);
Ok(())
}
#[test]
fn target_edit_drops_memo() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A")?;
stage.define_prim("/B")?;
stage.define_prim("/C")?;
stage.prim("/A")?.create_relationship("r")?.set_targets(["/B"])?;
assert_eq!(stage.relationship("/A.r")?.targets()?, vec![sdf::path("/B")?]);
stage.relationship("/A.r")?.set_targets(["/C"])?;
assert_eq!(
stage.relationship("/A.r")?.targets()?,
vec![sdf::path("/C")?],
"the re-authored targets must be visible, not the memoized list",
);
Ok(())
}
#[test]
fn target_edit_fans_out_to_dependent() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Class")?;
stage.define_prim("/Class/Local")?;
stage.define_prim("/Class/Other")?;
stage
.prim("/Class")?
.create_relationship("r")?
.set_targets(["/Class/Local"])?;
stage.define_prim("/Inst")?.set_metadata(
sdf::FieldKey::InheritPaths.as_str(),
sdf::Value::PathListOp(sdf::PathListOp::prepended([sdf::path("/Class")?])),
)?;
stage.define_prim("/Inst/Local")?;
stage.define_prim("/Inst/Other")?;
assert_eq!(
stage.relationship("/Inst.r")?.targets()?,
vec![sdf::path("/Inst/Local")?],
"the inherited target translates into the instance namespace",
);
stage.relationship("/Class.r")?.set_targets(["/Class/Other"])?;
assert_eq!(
stage.relationship("/Inst.r")?.targets()?,
vec![sdf::path("/Inst/Other")?],
"editing the class relationship restales the inheriting prim's memo",
);
Ok(())
}
#[test]
fn target_spec_removal_drops_memo() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A")?;
stage.define_prim("/B")?;
stage.prim("/A")?.create_relationship("r")?.set_targets(["/B"])?;
assert_eq!(stage.relationship("/A.r")?.targets()?, vec![sdf::path("/B")?]);
assert!(stage.remove_property("/A.r")?);
assert_eq!(
stage.relationship("/A.r")?.targets()?,
Vec::<sdf::Path>::new(),
"the removed relationship's memoized targets must not persist",
);
Ok(())
}
#[test]
fn target_edit_is_info_only_not_resync() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A")?;
stage.define_prim("/B")?;
stage.define_prim("/C")?;
stage.prim("/A")?.create_relationship("r")?.set_targets(["/B"])?;
let resynced: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let info: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let _token = {
let (resynced, info) = (resynced.clone(), info.clone());
stage.add_sink(move |_stage: &Stage, oc: &CommittedChange<'_>| {
resynced.borrow_mut().extend(oc.resynced.iter().cloned());
info.borrow_mut().extend(oc.changed_info_only.iter().cloned());
})
};
stage.relationship("/A.r")?.set_targets(["/C"])?;
assert!(
!resynced.borrow().contains(&sdf::path("/A")?),
"a target value edit must not resync the owning prim"
);
assert!(
info.borrow().contains(&sdf::path("/A.r")?),
"the edited relationship is reported as changed-info"
);
Ok(())
}
#[test]
fn attr_removal_not_info_only() -> Result<()> {
let stage = in_memory_stage()?;
stage.create_attribute("/P.size", "double")?;
let info: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let resynced: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let _token = {
let (info, resynced) = (info.clone(), resynced.clone());
stage.add_sink(move |_stage: &Stage, oc: &CommittedChange<'_>| {
info.borrow_mut().extend(oc.changed_info_only.iter().cloned());
resynced.borrow_mut().extend(oc.resynced.iter().cloned());
})
};
assert!(stage.remove_property("/P.size")?);
assert!(
!info.borrow().contains(&sdf::path("/P.size")?),
"a removed attribute must not be reported as a changed-info edit"
);
assert!(
resynced.borrow().contains(&sdf::path("/P.size")?),
"the removed attribute is reported as a resync, got {:?}",
resynced.borrow()
);
Ok(())
}
#[test]
fn rel_removal_not_info_only() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A")?;
stage.define_prim("/B")?;
stage.prim("/A")?.create_relationship("r")?.set_targets(["/B"])?;
let info: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let _token = {
let info = info.clone();
stage.add_sink(move |_stage: &Stage, oc: &CommittedChange<'_>| {
info.borrow_mut().extend(oc.changed_info_only.iter().cloned());
})
};
assert!(stage.remove_property("/A.r")?);
assert!(
!info.borrow().contains(&sdf::path("/A.r")?),
"a removed relationship with targets must not be reported as a changed-info edit"
);
Ok(())
}
#[test]
fn instance_target_memo_not_stale() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\n\nclass \"Class\"\n{\n double x\n add double x.connect = [</Target.y>]\n double y\n}\n\n\
def \"Owner\" (\n inherits = </Class>\n)\n{\n}\n\ndef \"Target\" (\n inherits = </Class>\n)\n{\n}\n",
)?;
let attr = "/Owner.x";
let target = sdf::path("/Target")?;
let drop_inherit = || sdf::Value::PathListOp(sdf::PathListOp::explicit(Vec::<sdf::Path>::new()));
let a = Stage::open(root.to_str().unwrap())?;
let before = a.attribute(attr)?.connections()?;
a.prim(target.clone())?
.set_metadata(sdf::FieldKey::InheritPaths.as_str(), drop_inherit())?;
let after_cached = a.attribute(attr)?.connections()?;
let b = Stage::open(root.to_str().unwrap())?;
b.prim(target)?
.set_metadata(sdf::FieldKey::InheritPaths.as_str(), drop_inherit())?;
let fresh = b.attribute(attr)?.connections()?;
assert_eq!(after_cached, fresh, "the cached path must agree with a fresh compose");
assert_ne!(
before, after_cached,
"the edit must change the result (guards against a vacuous test)"
);
Ok(())
}
#[test]
fn permission_private_inherit_composes_normally() -> Result<()> {
let path = format!(
"{}vendor/core-spec-supplemental-release_dec2025/composition/tests/assets/\
ErrorPermissionDenied_root/usda/root.usd",
env!("CARGO_WORKSPACE_DIR")
);
let stage = Stage::builder().open(&path)?;
assert!(
stage
.prim("/Model")?
.property_names()?
.iter()
.any(|n| n.as_str() == "attr"),
"private inherit must stay visible"
);
assert!(
stage.composition_errors().is_empty(),
"permission = private must not raise a composition error"
);
Ok(())
}
#[test]
fn field_single_layer() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
assert!(!stage.prim("/World/CubeInactive")?.is_active()?);
assert!(stage.prim("/World/CubeActive")?.is_active()?);
Ok(())
}
#[test]
fn sublayer_stronger_opinion_wins() -> Result<()> {
let path = fixture_path("sublayer_override.usda");
let stage = Stage::open(&path)?;
assert_eq!(stage.layer_count(), 2);
let prop_path = sdf::Path::new("/World/Cube")?.append_property("primvars:displayColor")?;
let value = stage.attribute(&prop_path)?.get::<sdf::Value>()?;
assert!(value.is_some(), "displayColor should have a composed value");
let value = value.unwrap();
let base_red = sdf::Value::Vec3fVec(vec![gf::vec3f(1.0, 0.0, 0.0)]);
assert_ne!(value, base_red, "stronger layer opinion should win over weaker");
Ok(())
}
#[test]
fn field_active_metadata() -> Result<()> {
let path = composition_path("active.usda");
let stage = Stage::open(&path)?;
assert!(!stage.prim("/World/CubeInactive")?.is_active()?);
assert!(stage.prim("/World/CubeActive")?.is_active()?);
Ok(())
}
#[test]
fn reference_external_default_prim() -> Result<()> {
let path = fixture_path("ref_external.usda");
let stage = Stage::open(&path)?;
assert!(stage.prim("/World/MyPrim")?.is_valid()?);
let children = child_names(&stage, "/World/MyPrim")?;
assert!(
children.contains(&"Child".to_string()),
"referenced children should be visible"
);
Ok(())
}
#[test]
fn inherit_local_opinion_wins() -> Result<()> {
let path = composition_path("class_inherit.usda");
let stage = Stage::open(&path)?;
let prop = sdf::Path::new("/World/cubeWithSetColor")?.append_property("primvars:displayColor")?;
let value = stage.attribute(&prop)?.get::<sdf::Value>()?;
assert!(value.is_some());
let green = sdf::Value::Vec3fVec(vec![gf::vec3f(0.0, 0.8, 0.0)]);
assert_ne!(value.unwrap(), green, "local opinion should win over inherited");
Ok(())
}
#[test]
fn variant_local_opinion_wins() -> Result<()> {
let path = format!(
"{}vendor/usd-wg-assets/docs/CompositionPuzzles/VariantSetAndLocal1/puzzle_1.usda",
env!("CARGO_WORKSPACE_DIR")
);
let stage = Stage::open(&path)?;
let prop = sdf::Path::new("/World/Sphere")?.append_property("radius")?;
let value = stage.attribute(&prop)?.get::<f64>()?;
assert_eq!(value, Some(1.0), "local opinion (1) should win over variant (2)");
Ok(())
}
#[test]
fn specialize_local_opinion_wins() -> Result<()> {
let path = composition_path("inherit_and_specialize.usda");
let stage = Stage::open(&path)?;
let prop = sdf::Path::new("/World/cubeScene/specializes")?.append_property("primvars:displayColor")?;
let value = stage.attribute(&prop)?.get::<sdf::Value>()?;
assert!(value.is_some());
let red = sdf::Value::Vec3fVec(vec![gf::vec3f(0.8, 0.0, 0.0)]);
assert_ne!(value.unwrap(), red, "local opinion should win over specialized");
Ok(())
}
#[test]
fn instanceable_true_parses_and_is_readable() -> Result<()> {
let path = fixture_path("instanceable_metadata.usda");
let stage = Stage::open(&path)?;
assert!(stage.prim("/Root/InstancePrototype")?.is_instanceable()?);
Ok(())
}
#[test]
fn instanceable_false_parses_and_is_readable() -> Result<()> {
let path = fixture_path("instanceable_metadata.usda");
let stage = Stage::open(&path)?;
assert!(!stage.prim("/Root/NotInstanceable")?.is_instanceable()?);
Ok(())
}
#[test]
fn instanceable_absent_defaults_false() -> Result<()> {
let path = fixture_path("instanceable_metadata.usda");
let stage = Stage::open(&path)?;
assert!(!stage.prim("/Root")?.is_instanceable()?);
Ok(())
}
#[test]
fn variant_fallback_selects_preferred() -> Result<()> {
let path = fixture_path("variant_fallback.usda");
let fallbacks = pcp::VariantFallbackMap::new().add("shadingComplexity", ["simple"]);
let stage = Stage::builder().variant_fallbacks(fallbacks).open(&path)?;
let prop = sdf::Path::new("/NoSelection")?.append_property("complexity")?;
let value = stage.attribute(&prop)?.get::<f64>()?;
assert_eq!(value, Some(0.5), "fallback 'simple' should give complexity=0.5");
Ok(())
}
#[test]
fn variant_fallback_does_not_override_authored() -> Result<()> {
let path = fixture_path("variant_fallback.usda");
let fallbacks = pcp::VariantFallbackMap::new().add("shadingComplexity", ["none"]);
let stage = Stage::builder().variant_fallbacks(fallbacks).open(&path)?;
let prop = sdf::Path::new("/Root")?.append_property("complexity")?;
let value = stage.attribute(&prop)?.get::<f64>()?;
assert_eq!(value, Some(1.0), "authored 'full' should win over fallback 'none'");
Ok(())
}
#[test]
fn inherit_child_exists_without_local_override() -> Result<()> {
let path = fixture_path("inherit_child_propagation.usda");
let stage = Stage::open(&path)?;
let children = child_names(&stage, "/Instance")?;
assert!(
children.contains(&"Child".to_string()),
"inherited child should appear: got {children:?}"
);
assert!(
stage
.prim("/Instance/Child")?
.property_names()?
.iter()
.any(|n| n.as_str() == "name"),
"property from inherited child should be visible"
);
Ok(())
}
#[test]
fn inherit_nested_child_propagation() -> Result<()> {
let path = fixture_path("inherit_nested_child.usda");
let stage = Stage::open(&path)?;
let a_children = child_names(&stage, "/Prim")?;
assert!(
a_children.contains(&"A".to_string()),
"first-level child: got {a_children:?}"
);
let b_children = child_names(&stage, "/Prim/A")?;
assert!(
b_children.contains(&"B".to_string()),
"second-level child: got {b_children:?}"
);
assert!(
stage
.prim("/Prim/A/B")?
.property_names()?
.iter()
.any(|n| n.as_str() == "val"),
"deeply nested inherited property should be visible"
);
Ok(())
}
#[test]
fn inherit_chain_child_propagation() -> Result<()> {
let path = fixture_path("inherit_chain_child.usda");
let stage = Stage::open(&path)?;
let children = child_names(&stage, "/Leaf")?;
assert!(
children.contains(&"Deep".to_string()),
"chain-inherited child: got {children:?}"
);
assert!(
stage
.prim("/Leaf/Deep")?
.property_names()?
.iter()
.any(|n| n.as_str() == "x"),
"property from chain-inherited child should be visible"
);
Ok(())
}
#[test]
fn session_layer_opinion_wins() -> Result<()> {
let stage = open_with_session()?;
assert!(stage.has_session_layer());
assert_eq!(stage.layer_count(), 2);
assert!(
stage
.session_layer()
.expect("configured session layer")
.identifier()
.ends_with("session_layer.usda")
);
let prop = sdf::Path::new("/World")?.append_property("radius")?;
let value = stage.attribute(&prop)?.get::<f64>()?;
assert_eq!(value, Some(99.0), "session layer opinion should win");
Ok(())
}
#[test]
fn session_layer_adds_properties() -> Result<()> {
let stage = open_with_session()?;
let prop = sdf::Path::new("/World")?.append_property("visibility")?;
let value = stage.attribute(&prop)?.get::<String>()?;
assert_eq!(value, Some("hidden".to_string()));
Ok(())
}
#[test]
fn session_layer_preserves_root_opinions() -> Result<()> {
let stage = open_with_session()?;
let prop = sdf::Path::new("/World")?.append_property("name")?;
let value = stage.attribute(&prop)?.get::<String>()?;
assert_eq!(value, Some("root".to_string()));
Ok(())
}
#[test]
fn mask_traverse() -> Result<()> {
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/World/ActiveParent/Child"])?)
.open("fixtures/stage_queries.usda")?;
assert_eq!(
stage.root_prims()?.iter().map(|t| t.as_str()).collect::<Vec<_>>(),
["World"]
);
assert_eq!(child_names(&stage, "/World")?, vec!["ActiveParent"]);
assert_eq!(child_names(&stage, "/World/ActiveParent")?, vec!["Child"]);
assert!(stage.prim("/World")?.is_valid()?);
assert!(stage.prim("/World/ActiveParent/Child")?.is_valid()?);
assert!(!stage.prim("/World/Group")?.is_valid()?);
assert_eq!(stage.prim("/World/Group")?.kind()?, None);
let mut prims = Vec::new();
stage.traverse(PrimPredicate::ALL, |p| prims.push(p.as_str().to_string()))?;
assert_eq!(
prims,
vec!["/World", "/World/ActiveParent", "/World/ActiveParent/Child"]
);
Ok(())
}
#[test]
fn mask_skips_dependency() -> Result<()> {
let path = composition_path("references/reference_invalid.usda");
let stage = Stage::builder()
.mask(StagePopulationMask::new(["/World/cube"])?)
.open(&path)?;
assert_eq!(
stage.root_prims()?.iter().map(|t| t.as_str()).collect::<Vec<_>>(),
["World"]
);
assert_eq!(child_names(&stage, "/World")?, vec!["cube"]);
assert!(!stage.prim("/World/invalid_reference")?.is_valid()?);
Ok(())
}
#[test]
fn custom_layer_data() -> Result<()> {
let stage = in_memory_stage()?;
assert!(stage.custom_layer_data()?.is_none());
let dict = sdf::Value::Dictionary([("tool".to_string(), sdf::Value::String("rs".into()))].into());
stage.set_custom_layer_data(dict)?;
let Some(sdf::Value::Dictionary(read)) = stage.custom_layer_data()? else {
panic!("customLayerData should resolve to a dictionary");
};
assert_eq!(read.get("tool"), Some(&sdf::Value::String("rs".into())));
Ok(())
}
#[test]
fn create_attribute() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Sphere")?.set_type_name("Sphere")?;
stage.create_attribute("/Sphere.radius", "double")?;
let attr = stage.attribute("/Sphere.radius")?;
assert_eq!(attr.type_name()?, Some(sdf::ValueTypeName::DOUBLE));
assert!(attr.is_custom()?, "generic attributes are authored custom");
let radius = sdf::Path::new("/Sphere.radius")?;
let attrs = stage.prim("/Sphere")?.attributes()?;
assert!(attrs.iter().any(|a| a.path() == &radius));
Ok(())
}
#[test]
fn create_relationship() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Mesh")?.set_type_name("Mesh")?;
let rel = stage.create_relationship("/Mesh.material:binding")?;
assert!(rel.is_custom()?, "generic relationships are authored custom");
let binding = sdf::Path::new("/Mesh.material:binding")?;
let rels = stage.prim("/Mesh")?.relationships()?;
assert!(rels.iter().any(|r| r.path() == &binding));
Ok(())
}
#[test]
fn default_prim_targets_root() -> Result<()> {
let session = fixture_path("session_layer.usda");
let stage = Stage::builder().session_layer(&session).in_memory("anon.usda")?;
let session_id = stage.session_layer().expect("session layer").identifier().to_string();
stage.set_edit_target(EditTarget::for_layer(session_id))?;
stage.set_default_prim("World")?;
assert_eq!(stage.default_prim().as_deref(), Some("World"));
Ok(())
}
#[test]
fn in_memory_session_layer() -> Result<()> {
let session = fixture_path("session_layer.usda");
let stage = Stage::builder().session_layer(&session).in_memory("anon.usda")?;
assert!(stage.has_session_layer());
assert_eq!(stage.layer_count(), 2);
assert_eq!(stage.edit_target().layer_identifier(), stage.root_layer().identifier());
stage.define_prim("/World")?.set_type_name("Xform")?;
assert!(stage.prim("/World")?.is_defined()?);
Ok(())
}
#[test]
fn edit_context_restores_on_drop() -> Result<()> {
let session = fixture_path("session_layer.usda");
let stage = Stage::builder().session_layer(&session).in_memory("anon.usda")?;
let root_id = stage.root_layer().identifier().to_string();
let session_id = stage.session_layer().expect("session layer").identifier().to_string();
assert_eq!(stage.edit_target().layer_identifier(), root_id);
{
let _ctx = stage.edit_context(EditTarget::for_layer(session_id.clone()))?;
assert_eq!(stage.edit_target().layer_identifier(), session_id);
}
assert_eq!(stage.edit_target().layer_identifier(), root_id);
Ok(())
}
#[test]
fn edit_context_restores_on_error() -> Result<()> {
let session = fixture_path("session_layer.usda");
let stage = Stage::builder().session_layer(&session).in_memory("anon.usda")?;
let root_id = stage.root_layer().identifier().to_string();
let session_id = stage.session_layer().expect("session layer").identifier().to_string();
assert_eq!(stage.edit_target().layer_identifier(), root_id);
let authored: std::result::Result<(), StageAuthoringError> = (|| {
let _ctx = stage.edit_context(EditTarget::for_layer(session_id))?;
stage.define_prim("/A.x")?;
Ok(())
})();
assert!(authored.is_err());
assert_eq!(stage.edit_target().layer_identifier(), root_id);
Ok(())
}
#[test]
fn variant_edit_invalidates_stripped_path() -> Result<()> {
let stage = in_memory_stage()?;
let root = stage.edit_target().layer_identifier().to_string();
stage.define_prim("/Prim")?;
assert!(!stage.prim("/Prim/child")?.is_valid()?);
assert!(stage.is_indexed(&sdf::path("/Prim/child")?));
stage.set_edit_target(EditTarget::for_local_direct_variant(
root,
sdf::path("/Prim{set=sel}")?,
)?)?;
stage.define_prim("/Prim/child")?;
assert!(!stage.is_indexed(&sdf::path("/Prim/child")?));
Ok(())
}
#[test]
fn clip_skips_missing_attr() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = write_clip_scene(
dir.path(),
r#"#usda 1.0
def "Model" (
clips = {
dictionary default = {
asset[] assetPaths = [@./clip.usda@]
asset manifestAssetPath = @./manifest.usda@
string primPath = "/Model"
double2[] active = [(0, 0)]
}
}
)
{
}
"#,
r#"#usda 1.0
def "Model"
{
float ghost
}
"#,
r#"#usda 1.0
def "Model"
{
float ghost.timeSamples = {
0: 7
}
}
"#,
)?;
let stage = Stage::open(&root)?;
assert!(
!stage
.prim("/Model")?
.property_names()?
.iter()
.any(|n| n.as_str() == "ghost"),
"the clip must not fabricate an attribute"
);
assert_eq!(
stage
.attribute("/Model.ghost")?
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))?,
None
);
Ok(())
}
#[test]
fn listener_fires_on_define() -> Result<()> {
let stage = in_memory_stage()?;
let resynced: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let count = Rc::new(Cell::new(0u32));
let _token = {
let (resynced, count) = (resynced.clone(), count.clone());
stage.add_sink(move |_stage: &Stage, oc: &CommittedChange<'_>| {
count.set(count.get() + 1);
resynced.borrow_mut().extend(oc.resynced.iter().cloned());
})
};
stage.define_prim("/World")?;
assert_eq!(count.get(), 1);
assert!(resynced.borrow().contains(&sdf::Path::new("/World")?));
Ok(())
}
#[test]
fn listener_info_only() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/World")?;
let attr = stage.create_attribute("/World.size", "double")?;
let info: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let resynced: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let has_default = Rc::new(Cell::new(false));
let _token = {
let (info, resynced, has_default) = (info.clone(), resynced.clone(), has_default.clone());
let size = sdf::Path::new("/World.size")?;
stage.add_sink(move |_stage: &Stage, oc: &CommittedChange<'_>| {
info.borrow_mut().extend(oc.changed_info_only.iter().cloned());
resynced.borrow_mut().extend(oc.resynced.iter().cloned());
if oc.changed_fields(&size).any(|t| t.as_str() == "default") {
has_default.set(true);
}
})
};
attr.set(2.0_f64)?;
assert!(info.borrow().contains(&sdf::Path::new("/World.size")?));
assert!(resynced.borrow().is_empty());
assert!(has_default.get());
Ok(())
}
#[test]
fn listener_info_under_variant_target() -> Result<()> {
let stage = in_memory_stage()?;
let root = stage.edit_target().layer_identifier().to_string();
stage.define_prim("/Prim")?;
stage.set_edit_target(EditTarget::for_local_direct_variant(
root.clone(),
sdf::path("/Prim{set=sel}")?,
)?)?;
let attr = stage.create_attribute("/Prim.size", "double")?;
stage.layer_mut(&root).expect("root layer is live").edit(|e| {
let mut prim = e.prim_mut("/Prim")?.expect("the prim spec is on the root layer");
prim.set(
sdf::FieldKey::VariantSetNames.as_str(),
sdf::Value::StringListOp(sdf::StringListOp::prepended(["set".to_owned()])),
);
prim.set(
sdf::FieldKey::VariantSelection.as_str(),
sdf::Value::VariantSelectionMap(HashMap::from([("set".to_string(), "sel".to_string())])),
);
Ok(())
})?;
let info: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let has_default = Rc::new(Cell::new(false));
let _token = {
let (info, has_default) = (info.clone(), has_default.clone());
let size = sdf::path("/Prim.size")?;
stage.add_sink(move |_stage: &Stage, oc: &CommittedChange<'_>| {
info.borrow_mut().extend(oc.changed_info_only.iter().cloned());
if oc.changed_fields(&size).any(|t| t.as_str() == "default") {
has_default.set(true);
}
})
};
attr.set(2.0_f64)?;
assert!(info.borrow().contains(&sdf::path("/Prim.size")?));
assert!(!info.borrow().contains(&sdf::path("/Prim{set=sel}.size")?));
assert!(has_default.get());
Ok(())
}
#[test]
fn listener_resync_under_variant_target() -> Result<()> {
let stage = in_memory_stage()?;
let root = stage.edit_target().layer_identifier().to_string();
stage.define_prim("/Prim")?;
stage.set_edit_target(EditTarget::for_local_direct_variant(
root,
sdf::path("/Prim{set=sel}")?,
)?)?;
let resynced: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let _token = {
let resynced = resynced.clone();
stage.add_sink(move |_stage: &Stage, oc: &CommittedChange<'_>| {
resynced.borrow_mut().extend(oc.resynced.iter().cloned());
})
};
stage.define_prim("/Prim/child")?;
assert!(resynced.borrow().contains(&sdf::path("/Prim/child")?));
assert!(!resynced.borrow().contains(&sdf::path("/Prim{set=sel}child")?));
Ok(())
}
#[test]
fn listener_referenced_variant() -> Result<()> {
let dir = tempfile::tempdir()?;
let src = dir.path().join("src.usda");
fs::write(
&src,
"#usda 1.0
def \"Source\" (
variants = { string set = \"sel\" }
prepend variantSets = \"set\"
)
{
variantSet \"set\" = {
\"sel\" {
def \"Kid\" {}
}
}
}
",
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0
def \"MyPrim\" (
references = @src.usda@</Source>
)
{
}
",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert!(
stage.prim("/MyPrim/Kid")?.is_valid()?,
"the variant composes through the reference"
);
let arc = stage.edit_target_for_node(&sdf::path("/MyPrim")?, EditTargetArc::Reference)?;
let variant = EditTarget::for_local_direct_variant(arc.layer_identifier(), sdf::path("/Source{set=sel}")?)?;
stage.set_edit_target(variant.compose_over(&arc))?;
let resynced: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let _token = {
let resynced = resynced.clone();
stage.add_sink(move |_stage: &Stage, oc: &CommittedChange<'_>| {
resynced.borrow_mut().extend(oc.resynced.iter().cloned());
})
};
stage.define_prim("/MyPrim/New")?;
assert_eq!(*resynced.borrow(), vec![sdf::path("/MyPrim/New")?]);
Ok(())
}
#[test]
fn local_variant_resync_stripped() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Prim")?;
let root = stage.root_layer().identifier().to_string();
let notices = capture_notices(&stage);
stage.layer_mut(&root).expect("the root layer is live").edit(|e| {
sdf::PrimSpec::new(
e.data_mut(),
sdf::path("/Prim{set=sel}child").expect("valid path"),
sdf::Specifier::Def,
"",
)?;
Ok(())
})?;
stage.prim("/Prim")?.is_valid()?;
assert_eq!(notices.borrow().resynced, paths(&["/Prim/child"]));
Ok(())
}
#[test]
fn listener_edit_target_changed() -> Result<()> {
let stage = in_memory_stage()?;
let root = stage.root_layer().identifier().to_string();
let sub = sdf::Layer::new_anonymous("sub.usda");
let sub_id = sub.identifier().to_string();
stage.insert_layer(&root, 0, sub, sdf::LayerOffset::IDENTITY)?;
let count = Rc::new(Cell::new(0u32));
let _token = {
let count = count.clone();
stage.add_sink(RecordingSink {
edit_target: Some(Box::new(move |_stage| count.set(count.get() + 1))),
..Default::default()
})
};
stage.set_edit_target(EditTarget::for_layer(sub_id.clone()))?;
assert_eq!(count.get(), 1);
stage.set_edit_target(EditTarget::for_layer(sub_id))?;
assert_eq!(count.get(), 1);
Ok(())
}
#[test]
fn listener_edit_context_restore() -> Result<()> {
let stage = in_memory_stage()?;
let root = stage.root_layer().identifier().to_string();
let sub = sdf::Layer::new_anonymous("sub.usda");
let sub_id = sub.identifier().to_string();
stage.insert_layer(&root, 0, sub, sdf::LayerOffset::IDENTITY)?;
let count = Rc::new(Cell::new(0u32));
let _token = {
let count = count.clone();
stage.add_sink(RecordingSink {
edit_target: Some(Box::new(move |_stage| count.set(count.get() + 1))),
..Default::default()
})
};
{
let _ctx = stage.edit_context(EditTarget::for_layer(sub_id))?;
assert_eq!(count.get(), 1); } assert_eq!(count.get(), 2);
Ok(())
}
#[test]
fn listener_layer_muting() -> Result<()> {
let stage = in_memory_stage()?;
let muted = Rc::new(RefCell::new(Vec::<String>::new()));
let unmuted = Rc::new(RefCell::new(Vec::<String>::new()));
let _token = {
let (muted, unmuted) = (muted.clone(), unmuted.clone());
stage.add_sink(RecordingSink {
muting: Some(Box::new(move |_stage, layer, is_muted, resynced: &[sdf::Path]| {
assert!(resynced.is_empty(), "nothing was cached against {layer}");
if is_muted {
muted.borrow_mut().push(layer.to_string());
} else {
unmuted.borrow_mut().push(layer.to_string());
}
})),
..Default::default()
})
};
stage.mute_layer("weak.usda");
stage.mute_layer("weak.usda"); stage.unmute_layer("weak.usda");
stage.unmute_layer("weak.usda"); assert_eq!(*muted.borrow(), vec!["weak.usda".to_string()]);
assert_eq!(*unmuted.borrow(), vec!["weak.usda".to_string()]);
Ok(())
}
fn nested_instancing_stage() -> Result<(tempfile::TempDir, Stage, sdf::Path, sdf::Path)> {
let dir = tempfile::tempdir()?;
let root = write_nested_instancing_scene(dir.path())?;
let stage = Stage::open(&root)?;
let outer = stage.prim("/A")?.prototype()?.expect("/A is an instance");
let nested = stage
.prim(outer.append_path("Nested")?)?
.prototype()?
.expect("the nested prim is an instance");
Ok((dir, stage, outer, nested))
}
fn write_nested_instancing_scene(dir: &FsPath) -> Result<String> {
fs::write(
dir.join("inner.usda"),
"#usda 1.0\n\ndef \"Inner\"\n{\n def \"Leaf\"\n {\n custom double v = 1\n }\n}\n",
)?;
fs::write(
dir.join("detail.usda"),
"#usda 1.0\n\nover \"Model\"\n{\n custom double d = 2\n}\n",
)?;
fs::write(
dir.join("model.usda"),
"#usda 1.0\n(\n subLayers = [@detail.usda@]\n)\n\ndef \"Model\"\n{\n def \"Nested\" (\n instanceable = true\n references = @inner.usda@</Inner>\n )\n {\n }\n}\n",
)?;
let root = dir.join("root.usda");
fs::write(
&root,
"#usda 1.0\n\ndef \"A\" (\n instanceable = true\n references = @model.usda@</Model>\n)\n{\n}\n",
)?;
Ok(root.to_str().expect("utf-8 path").to_string())
}
#[test]
fn mute_reports_prototypes() -> Result<()> {
let (_dir, stage, outer, nested) = nested_instancing_stage()?;
let notices = capture_notices(&stage);
stage.mute_layer(identifier_by_leaf(&stage, "detail.usda"));
let reported = ¬ices.borrow().muting;
assert!(reported.contains(&sdf::path("/A")?), "the instance: {reported:?}");
assert!(reported.contains(&outer), "the outer prototype: {reported:?}");
assert!(reported.contains(&nested), "the nested prototype: {reported:?}");
Ok(())
}
#[test]
fn unload_reports_prototypes() -> Result<()> {
let (_dir, stage, outer, nested) = nested_instancing_stage()?;
let notices = capture_notices(&stage);
stage.unload("/A")?;
let reported = ¬ices.borrow().load_rules;
assert!(reported.contains(&outer), "the outer prototype: {reported:?}");
assert!(reported.contains(&nested), "the nested prototype: {reported:?}");
Ok(())
}
#[test]
fn vars_edit_reports_prototypes() -> Result<()> {
let (_dir, stage, outer, nested) = nested_instancing_stage()?;
let notices = capture_notices(&stage);
let model = identifier_by_leaf(&stage, "model.usda");
stage
.layer_mut(&model)
.expect("the referenced layer is live")
.edit(|e| e.set_expression_variables(HashMap::from([("V".to_string(), sdf::Value::String("x".into()))])))?;
stage.prim("/A")?.is_valid()?;
let reported = ¬ices.borrow().resynced;
assert!(
!reported.contains(&sdf::path("/")?),
"a non-root stack resyncs no blanket"
);
assert!(reported.contains(&outer), "the outer prototype: {reported:?}");
assert!(reported.contains(&nested), "the nested prototype: {reported:?}");
Ok(())
}
#[test]
fn significant_edit_reports_prototypes() -> Result<()> {
let (_dir, stage, outer, nested) = nested_instancing_stage()?;
let notices = capture_notices(&stage);
stage
.prim("/A")?
.set_metadata(sdf::FieldKey::Instanceable.as_str(), sdf::Value::Bool(false))?;
let reported = ¬ices.borrow().resynced;
assert!(reported.contains(&sdf::path("/A")?), "the edited prim: {reported:?}");
assert!(reported.contains(&outer), "the outer prototype: {reported:?}");
assert!(reported.contains(&nested), "the nested prototype: {reported:?}");
Ok(())
}
#[test]
fn unset_stops_delivery() -> Result<()> {
let stage = in_memory_stage()?;
let count = Rc::new(Cell::new(0u32));
let id = {
let count = count.clone();
stage.add_sink(move |_: &Stage, _: &CommittedChange<'_>| count.set(count.get() + 1))
};
stage.define_prim("/A")?;
assert_eq!(count.get(), 1);
stage.remove_sink(id);
stage.define_prim("/B")?;
assert_eq!(count.get(), 1);
Ok(())
}
#[test]
fn layer_sink_sees_staged() -> Result<()> {
let stage = in_memory_stage()?;
let root = stage.root_layer().identifier().to_string();
let seen = Rc::new(RefCell::new(String::new()));
let staged = Rc::new(Cell::new(false));
{
let (seen, staged) = (seen.clone(), staged.clone());
stage
.layer_mut(&root)
.expect("root layer")
.add_sink(RecordingLayerSink {
before: Some(Box::new(move |change| {
seen.replace(change.layer_identifier.to_string());
if !change.overlay.is_empty() && !change.change_list.is_empty() {
staged.set(true);
}
Ok(())
})),
..Default::default()
});
}
stage.define_prim("/World")?;
assert_eq!(*seen.borrow(), root, "before_commit saw the edited layer");
assert!(
staged.get(),
"the staged overlay and change list are populated pre-commit"
);
Ok(())
}
#[test]
fn layer_sink_veto_rolls_back() -> Result<()> {
let stage = in_memory_stage()?;
let root = stage.root_layer().identifier().to_string();
stage
.layer_mut(&root)
.expect("root layer")
.add_sink(RecordingLayerSink {
before: Some(Box::new(|_change| {
Err(sdf::sink::Error::new("policy forbids this edit"))
})),
..Default::default()
});
let result = stage.define_prim("/World");
assert!(matches!(result, Err(StageAuthoringError::Rejected(_))));
assert!(!stage.prim("/World")?.is_valid()?, "the rejected edit rolled back");
Ok(())
}
#[test]
fn namespace_edit_fires_sink() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A/B")?;
let after = Rc::new(Cell::new(0u32));
{
let after = after.clone();
stage.add_sink(move |_: &Stage, _: &CommittedChange<'_>| after.set(after.get() + 1));
}
let mut editor = usd::NamespaceEditor::new(&stage);
editor.delete_prim("/A/B").unwrap();
editor.can_apply().unwrap();
assert_eq!(after.get(), 0, "a dry run does not reach after_commit");
editor.apply()?;
assert_eq!(after.get(), 1, "the namespace edit delivered after_commit once");
assert!(!stage.prim("/A/B")?.is_valid()?);
Ok(())
}
#[test]
fn can_apply_queued_edit() -> Result<()> {
let stage = in_memory_stage()?;
let root = stage.root_layer().identifier().to_string();
stage.layer_mut(&root).expect("root layer").edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/Queued", sdf::Specifier::Def, "")?;
Ok(())
})?;
let mut editor = usd::NamespaceEditor::new(&stage);
editor.delete_prim("/Queued").unwrap();
editor.can_apply().expect("planning drains, so /Queued composes");
assert!(
stage.prim("/Queued")?.is_valid()?,
"the dry run staged nothing, so the queued prim survives"
);
Ok(())
}
#[test]
fn namespace_edit_veto_atomic() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A/B")?;
let root = stage.root_layer().identifier().to_string();
stage
.layer_mut(&root)
.expect("root layer")
.add_sink(RecordingLayerSink {
before: Some(Box::new(|_| Err(sdf::sink::Error::new("locked")))),
..Default::default()
});
let mut editor = usd::NamespaceEditor::new(&stage);
editor.delete_prim("/A/B").unwrap();
assert!(matches!(
editor.apply(),
Err(usd::NamespaceEditError::Stage(StageAuthoringError::Rejected(_)))
));
assert!(stage.prim("/A/B")?.is_valid()?, "the vetoed batch left the prim intact");
Ok(())
}
#[test]
fn sink_handle_edit_fires() -> Result<()> {
let stage = in_memory_stage()?;
let prim = stage.define_prim("/World")?;
let count = Rc::new(Cell::new(0u32));
{
let count = count.clone();
stage.add_sink(move |_: &Stage, _: &CommittedChange<'_>| count.set(count.get() + 1));
}
prim.set_type_name("Xform")?;
assert_eq!(count.get(), 1, "a handle edit reaches the stage's sink");
Ok(())
}
#[test]
fn sink_only_sees_edits_after_install() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/Before")?;
let count = Rc::new(Cell::new(0u32));
{
let count = count.clone();
stage.add_sink(move |_: &Stage, _: &CommittedChange<'_>| count.set(count.get() + 1));
}
stage.define_prim("/After")?;
assert_eq!(count.get(), 1, "only the post-install edit is observed");
Ok(())
}
#[test]
fn listener_layer_stack_resync() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/World")?;
let resynced: Rc<RefCell<Vec<sdf::Path>>> = Rc::new(RefCell::new(Vec::new()));
let _token = {
let resynced = resynced.clone();
stage.add_sink(move |_stage: &Stage, oc: &CommittedChange<'_>| {
resynced.borrow_mut().extend(oc.resynced.iter().cloned());
})
};
stage.set_time_codes_per_second(48.0)?;
assert!(resynced.borrow().contains(&sdf::Path::abs_root()));
Ok(())
}
#[test]
fn listener_reentrant_author() -> Result<()> {
let stage = in_memory_stage()?;
let done = Rc::new(Cell::new(false));
let _token = {
let done = done.clone();
stage.add_sink(move |stage: &Stage, _change: &CommittedChange<'_>| {
if !done.replace(true) {
stage.define_prim("/Nested").unwrap();
}
})
};
stage.define_prim("/World")?;
assert!(stage.prim("/Nested")?.is_valid()?);
Ok(())
}
#[test]
fn two_sink_reentrant_order() -> Result<()> {
fn record(tag: &'static str, log: &Rc<RefCell<Vec<String>>>, change: &CommittedChange<'_>) {
let prim = change
.resynced
.iter()
.map(|p| p.as_str().to_string())
.collect::<Vec<_>>()
.join(",");
log.borrow_mut().push(format!("{tag}:{prim}"));
}
let stage = in_memory_stage()?;
let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let _first = {
let log = log.clone();
let done = Cell::new(false);
stage.add_sink(move |stage: &Stage, change: &CommittedChange<'_>| {
record("s1", &log, change);
if !done.replace(true) {
stage.define_prim("/Nested").unwrap();
}
})
};
let _second = {
let log = log.clone();
stage.add_sink(move |_: &Stage, change: &CommittedChange<'_>| record("s2", &log, change))
};
stage.define_prim("/World")?;
assert_eq!(
log.borrow().clone(),
vec!["s1:/World", "s1:/Nested", "s2:/Nested", "s2:/World"],
"a stage-routed re-entrant edit drains inside the outer fan-out"
);
let stage = in_memory_stage()?;
let root_id = stage.root_layer().identifier().to_string();
let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let _first = {
let (log, root_id) = (log.clone(), root_id.clone());
let done = Cell::new(false);
stage.add_sink(move |stage: &Stage, change: &CommittedChange<'_>| {
record("s1", &log, change);
if !done.replace(true) {
stage
.layer_mut(&root_id)
.expect("root layer is live")
.edit(|e| {
sdf::PrimSpec::new(e.data_mut(), "/Nested", sdf::Specifier::Def, "")?;
Ok(())
})
.unwrap();
}
})
};
let _second = {
let log = log.clone();
stage.add_sink(move |_: &Stage, change: &CommittedChange<'_>| record("s2", &log, change))
};
stage.define_prim("/World")?;
assert_eq!(
log.borrow().clone(),
vec!["s1:/World", "s2:/World", "s1:/Nested", "s2:/Nested"],
"a direct layer edit is picked up by the quiescence loop"
);
assert!(stage.prim("/Nested")?.is_valid()?, "the nested edit composed");
Ok(())
}
#[test]
fn lazy_layer_aggregated() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0
def \"A\" (
payload = @payload.usda@</P>
)
{
}
",
)?;
fs::write(
dir.path().join("payload.usda"),
"#usda 1.0
def \"P\" {
custom double v = 1
}
",
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().unwrap())?;
assert_eq!(stage.attribute("/A.v")?.get::<f64>()?, Some(1.0));
let payload_id = stage
.layer_identifiers()
.into_iter()
.find(|id| FsPath::new(id).ends_with("payload.usda"))
.expect("the payload layer loaded");
stage.layer_mut(&payload_id).expect("payload layer is live").edit(|e| {
e.attribute_mut("/P.v")?
.expect("authored attribute")
.set_default(sdf::Value::Double(7.0))?;
Ok(())
})?;
assert_eq!(
stage.attribute("/A.v")?.get::<f64>()?,
Some(7.0),
"the demand-loaded layer's aggregator drove a recompose"
);
Ok(())
}
#[test]
fn query_under_layer_ref() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A")?;
let root = stage.root_layer();
assert!(stage.prim("/A")?.is_valid()?, "composed query under a root_layer ref");
let named = stage.layer(root.identifier()).expect("layer by identifier");
assert!(stage.prim("/A")?.is_valid()?, "composed query under a layer ref");
drop(named);
drop(root);
let dir = tempfile::tempdir()?;
let session = dir.path().join("session.usda");
fs::write(
&session,
"#usda 1.0
",
)?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0
def \"B\" {}
",
)?;
let stage = Stage::builder()
.session_layer(session.to_str().unwrap())
.open(dir.path().join("root.usda").to_str().unwrap())?;
let session = stage.session_layer().expect("session layer");
assert!(
stage.prim("/B")?.is_valid()?,
"composed query under a session_layer ref"
);
drop(session);
Ok(())
}
#[test]
fn empty_edit_no_fire() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A")?;
let count = Rc::new(Cell::new(0u32));
let _token = {
let count = count.clone();
stage.add_sink(move |_: &Stage, _: &CommittedChange<'_>| count.set(count.get() + 1))
};
stage.define_prim("/A")?;
assert_eq!(count.get(), 0);
Ok(())
}
#[test]
fn remove_prim_drops_spec() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A/B")?;
assert!(stage.prim("/A/B")?.is_valid()?);
let notices = capture_notices(&stage);
assert!(stage.remove_prim("/A/B")?);
assert!(!stage.prim("/A/B")?.is_valid()?);
assert!(!child_names(&stage, "/A")?.contains(&"B".to_string()));
assert_eq!(notices.borrow().resynced, paths(&["/A/B"]));
assert!(!stage.remove_prim("/A/B")?);
Ok(())
}
#[test]
fn remove_property_drops_spec() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A")?;
stage.create_attribute("/A.size", "double")?;
assert!(stage.prim("/A")?.property_names()?.iter().any(|t| t == "size"));
assert!(stage.remove_property("/A.size")?);
assert!(!stage.prim("/A")?.property_names()?.iter().any(|t| t == "size"));
assert!(stage.prim("/A")?.is_valid()?);
assert!(!stage.remove_property("/A.size")?);
Ok(())
}
#[test]
fn remove_rejects_wrong_path_kind() -> Result<()> {
let stage = in_memory_stage()?;
stage.define_prim("/A")?;
stage.create_attribute("/A.size", "double")?;
assert!(matches!(
stage.remove_prim("/A.size"),
Err(StageAuthoringError::Layer(sdf::AuthoringError::InvalidPath { .. }))
));
assert!(matches!(
stage.remove_property("/A"),
Err(StageAuthoringError::Layer(sdf::AuthoringError::InvalidPath { .. }))
));
assert!(stage.prim("/A")?.is_valid()?);
assert!(stage.prim("/A")?.property_names()?.iter().any(|t| t == "size"));
Ok(())
}
#[test]
fn spec_tier_resync_keeps_info() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
(
subLayers = [@sub.usda@]
)
"#,
)?;
fs::write(
dir.path().join("sub.usda"),
r#"#usda 1.0
def "A" {
def "B" {
custom double x = 1
}
}
"#,
)?;
let stage = Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/A/B.x")?.get::<f64>()?, Some(1.0));
let notices = capture_notices(&stage);
stage.create_attribute("/A/B.x", "double")?;
assert!(stage.prim("/A")?.is_valid()?);
let notices = notices.borrow();
assert!(
notices.resynced.contains(&sdf::path("/A/B.x")?),
"the authored attribute must still be reported, got {:?}",
notices.resynced
);
assert_eq!(
notices.resynced,
paths(&["/A", "/A/B", "/A/B.x"]),
"the spec tier reports both sites; neither stands for the other's subtree"
);
Ok(())
}
#[test]
fn diagnostics_owner_order() -> Result<()> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("root.usda"),
"#usda 1.0
(
subLayers = [@loop.usda@]
)
def \"P\" (
references = @missing.usda@
)
{
}
",
)?;
fs::write(
dir.path().join("loop.usda"),
"#usda 1.0
(
subLayers = [@root.usda@]
)
",
)?;
let stage = Stage::open(dir.path().join("root.usda").to_str().unwrap())?;
let _ = stage.prim("/P")?.is_valid()?;
let held = stage.composition_errors();
let cycle = held
.iter()
.position(|e| matches!(e, pcp::CompositionDiagnostic::SublayerCycle { .. }))
.expect("the sublayer cycle is reported");
let unresolved = held
.iter()
.position(|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedLayer { .. }))
.expect("the missing reference target is reported");
assert!(
cycle < unresolved,
"the graph's diagnostics precede the cache's, got {held:?}"
);
let cycles = held
.iter()
.filter(|e| matches!(e, pcp::CompositionDiagnostic::SublayerCycle { .. }))
.count();
let unresolved_count = held
.iter()
.filter(|e| matches!(e, pcp::CompositionDiagnostic::UnresolvedLayer { .. }))
.count();
assert_eq!((cycles, unresolved_count), (1, 1), "each reads once, got {held:?}");
Ok(())
}
#[test]
fn unsorted_samples_normalized() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\ndef \"A\" {\n double x.timeSamples = { 10: 1.0, 0: 2.0, 10: 5.0 }\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let attr = stage.attribute("/A.x")?;
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(0.0))?, Some(2.0));
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(5.0))?, Some(3.5));
let attr = attr.set_at(3.0_f64, usd::TimeCode::new(10.0))?;
assert_eq!(attr.num_time_samples()?, 2, "the write replaces the sample at its time");
assert_eq!(attr.get_at::<f64>(usd::TimeCode::new(10.0))?, Some(3.0));
Ok(())
}
#[test]
fn instancing_edits_refused() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\ndef \"Source\"\n{\n def \"Child\" {}\n}\ndef \"Inst\" (\n instanceable = true\n references = </Source>\n) {\n def \"Stale\" {}\n}\n",
)?;
let stage = Stage::open(root.to_str().unwrap())?;
let proxy = stage.prim("/Inst/Child")?;
assert!(proxy.is_instance_proxy()?);
assert!(matches!(
proxy.clone().apply_api("SomeAPI"),
Err(StageAuthoringError::InstanceProxyEdit { .. })
));
assert!(matches!(
proxy.clone().set_kind("component"),
Err(StageAuthoringError::InstanceProxyEdit { .. })
));
assert!(matches!(
stage.define_prim("/Inst/Child/New"),
Err(StageAuthoringError::InstanceProxyEdit { .. })
));
assert!(matches!(
stage.define_prim("/Inst/Missing/New"),
Err(StageAuthoringError::InstanceProxyEdit { .. })
));
let prototype = proxy
.prim_in_prototype()?
.expect("the proxy stands in for a prototype prim");
assert!(matches!(
prototype.apply_api("SomeAPI"),
Err(StageAuthoringError::PrototypeEdit { .. })
));
assert!(matches!(
stage.define_prim("/__Prototype_7/Stray"),
Err(StageAuthoringError::PrototypeEdit { .. })
));
stage.prim("/Inst")?.set_kind("component")?;
stage.prim("/Source/Child")?.apply_api("SomeAPI")?;
assert!(stage.remove_prim("/Inst/Stale")?, "the stale local spec is removable");
assert!(
stage.root_layer().prim("/Inst/Child")?.is_none() && stage.root_layer().prim("/Inst/Stale")?.is_none(),
"nothing was authored under the instance"
);
stage.set_edit_target(
stage
.prim("/Inst")?
.edit_target_for_arc(usd::EditTargetArc::Reference)?,
)?;
proxy.set_kind("component")?;
assert_eq!(
stage
.root_layer()
.prim("/Source/Child")?
.expect("the source prim")
.kind()
.as_deref(),
Some("component")
);
Ok(())
}
#[test]
fn save_quiet() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = quiet_save_root(dir.path())?;
quiet_save(Stage::open(&root)?, &root)
}
#[cfg(feature = "mmap")]
#[test]
#[allow(unsafe_code)]
fn save_quiet_mapped() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = quiet_save_root(dir.path())?;
let resolver = unsafe { ar::DefaultResolver::new().map_files() };
quiet_save(Stage::builder().resolver(resolver).open(&root)?, &root)
}
fn quiet_save_root(dir: &FsPath) -> Result<String> {
let root = dir.join("scene.usdc");
sdf::Layer::from_bytes(
"scene",
b"#usda 1.0\ndef \"World\"\n{\n float[] sizes = [1, 2, 3]\n}\n".as_slice(),
)?
.export(root.to_string_lossy())?;
Ok(root.to_str().expect("utf-8 temp path").to_owned())
}
fn quiet_save(stage: Stage, root: &str) -> Result<()> {
stage.edit(|edit| edit.attribute_builder("/World.answer", "int").set(42).build())?;
let sizes = stage.attribute("/World.sizes")?.get::<Vec<f32>>()?;
let answer = stage.attribute("/World.answer")?.get::<i32>()?;
let status = stage.prim_status("/World")?;
let stage_fired = Rc::new(Cell::new(0));
stage.add_sink(RecordingSink {
after: Some(Box::new({
let fired = stage_fired.clone();
move |_, _| fired.set(fired.get() + 1)
})),
..Default::default()
});
let layer_fired = Rc::new(Cell::new(0));
let identifier = stage.root_layer().identifier().to_owned();
{
let mut layer = stage.layer_mut(&identifier).expect("the root layer is live");
let fired = layer_fired.clone();
layer.add_sink(move |_: &str, _: &sdf::ChangeList| fired.set(fired.get() + 1));
layer.save()?;
}
assert_eq!(stage_fired.get(), 0, "a save is not a stage change");
assert_eq!(layer_fired.get(), 0, "a save is not a layer change");
assert_eq!(stage.attribute("/World.sizes")?.get::<Vec<f32>>()?, sizes);
assert_eq!(stage.attribute("/World.answer")?.get::<i32>()?, answer);
assert_eq!(stage.prim_status("/World")?, status);
let reopened = Stage::open(root)?;
assert_eq!(reopened.attribute("/World.answer")?.get::<i32>()?, Some(42));
assert_eq!(reopened.attribute("/World.sizes")?.get::<Vec<f32>>()?, sizes);
Ok(())
}
#[test]
fn missing_target_found_later() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\ndef \"A\" (references = @./late.usda@</Model>) {}\ndef \"Other\" { def \"B\" (references = @./late.usda@</Model>) {} }\n",
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
assert!(!stage.prim("/A/Child")?.is_valid()?);
assert!(
stage
.composition_errors()
.iter()
.any(|error| matches!(error, pcp::CompositionDiagnostic::UnresolvedLayer { .. })),
"the missing target is reported"
);
fs::write(
dir.path().join("late.usda"),
"#usda 1.0\ndef \"Model\" { def \"Child\" {} }\n",
)?;
assert!(
stage.prim("/Other/B/Child")?.is_valid()?,
"a prim composed now finds the file"
);
assert!(
stage.prim("/A/Child")?.is_valid()?,
"and the prim that missed it recomposes"
);
Ok(())
}
#[test]
fn missing_target_found_malformed() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\ndef \"A\" (references = @./late.usda@</Model>) {}\ndef \"Other\" { def \"B\" (references = @./late.usda@</Model>) {} }\n",
)?;
let stage = Stage::open(root.to_str().expect("utf-8 temp path"))?;
assert!(!stage.prim("/A/Child")?.is_valid()?);
let count = |stage: &Stage, malformed: bool| {
stage
.composition_errors()
.iter()
.filter(|error| match error {
pcp::CompositionDiagnostic::MalformedLayer { .. } => malformed,
pcp::CompositionDiagnostic::UnresolvedLayer { .. } => !malformed,
_ => false,
})
.count()
};
assert_eq!((count(&stage, false), count(&stage, true)), (1, 0));
fs::write(dir.path().join("late.usda"), "not a layer")?;
assert!(!stage.prim("/Other/B/Child")?.is_valid()?);
assert!(!stage.prim("/A/Child")?.is_valid()?);
assert_eq!(
(count(&stage, false), count(&stage, true)),
(0, 2),
"both prims report the read failure"
);
Ok(())
}