use std::cell::RefCell;
use std::collections::HashMap;
use std::fs;
use std::path::Path as FsPath;
use std::rc::Rc;
use openusd::Result;
use openusd::{gf, pcp, sdf, tf, usd};
#[test]
fn change_list_entry_dedups() {
let mut cl = sdf::ChangeList::new();
let p = sdf::Path::abs_root();
cl.entry_mut(&p).flags |= sdf::ChangeFlags::ADD_NON_INERT_PRIM;
cl.entry_mut(&p).note("specifier", sdf::FieldChange::Value);
assert_eq!(cl.entries().len(), 1);
assert!(cl.entries()[0].1.flags.contains(sdf::ChangeFlags::ADD_NON_INERT_PRIM));
assert!(cl.entries()[0].1.changed("specifier"));
}
#[test]
fn change_list_empty_until_entry() {
let mut cl = sdf::ChangeList::new();
assert!(cl.is_empty());
cl.entry_mut(&sdf::Path::abs_root());
assert!(!cl.is_empty());
}
fn open_in_memory() -> usd::Stage {
usd::Stage::builder().in_memory("anon.usda").expect("in-memory stage")
}
fn exists(stage: &usd::Stage, path: &str) -> bool {
stage.prim(path).unwrap().is_valid().unwrap()
}
fn child_names(stage: &usd::Stage, path: &str) -> Vec<String> {
stage
.prim(path)
.unwrap()
.child_names()
.unwrap()
.into_iter()
.map(String::from)
.collect()
}
#[derive(Default, Debug)]
struct Notices {
resynced: Vec<sdf::Path>,
changed_info_only: Vec<sdf::Path>,
asset_paths_resynced: Vec<sdf::Path>,
}
fn capture_notices(stage: &usd::Stage) -> Rc<RefCell<Notices>> {
let notices = Rc::new(RefCell::new(Notices::default()));
let sink = notices.clone();
stage.add_sink(move |_stage: &usd::Stage, change: &usd::CommittedChange<'_>| {
let mut n = sink.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());
});
notices
}
fn paths(paths: &[&str]) -> Vec<sdf::Path> {
paths.iter().map(|p| sdf::path(p).expect("valid path")).collect()
}
fn identifier_by_leaf(stage: &usd::Stage, leaf: &str) -> String {
stage
.layer_identifiers()
.into_iter()
.find(|id| FsPath::new(id).ends_with(leaf))
.expect("layer is loaded")
}
#[test]
fn author_keeps_sibling_indexed() {
let stage = open_in_memory();
stage.define_prim("/Foo").unwrap().set_type_name("Xform").unwrap();
stage.define_prim("/Bar").unwrap().set_type_name("Xform").unwrap();
let _ = stage.prim("/Foo").unwrap().type_name().unwrap();
let _ = stage.prim("/Bar").unwrap().type_name().unwrap();
assert!(stage.is_indexed(&sdf::path("/Foo").unwrap()));
assert!(stage.is_indexed(&sdf::path("/Bar").unwrap()));
stage.override_prim("/Foo").unwrap().set_kind("component").unwrap();
assert!(
stage.is_indexed(&sdf::path("/Bar").unwrap()),
"/Bar's index must not be dropped when authoring at /Foo",
);
}
#[test]
fn significant_at_ancestor_drops_descendant() {
let stage = open_in_memory();
stage.define_prim("/Foo").unwrap().set_type_name("Xform").unwrap();
stage.define_prim("/Foo/Bar").unwrap().set_type_name("Xform").unwrap();
let _ = stage.prim("/Foo").unwrap().type_name().unwrap();
let _ = stage.prim("/Foo/Bar").unwrap().type_name().unwrap();
assert!(stage.is_indexed(&sdf::path("/Foo").unwrap()));
assert!(stage.is_indexed(&sdf::path("/Foo/Bar").unwrap()));
stage.override_prim("/Foo").unwrap().set_instanceable(true).unwrap();
assert!(
!stage.is_indexed(&sdf::path("/Foo/Bar").unwrap()),
"a significant change at /Foo must invalidate its self-root-only descendant /Foo/Bar",
);
}
#[test]
fn attribute_value_keeps_owner_indexed() {
let stage = open_in_memory();
let attr = stage
.define_prim("/A")
.unwrap()
.set_type_name("Xform")
.unwrap()
.create_attribute("x", "double")
.unwrap()
.set(sdf::Value::Double(1.0))
.unwrap();
let _ = stage.prim("/A").unwrap().type_name().unwrap();
assert!(stage.is_indexed(&sdf::path("/A").unwrap()));
let attr = attr.set(sdf::Value::Double(2.0)).unwrap();
assert!(
stage.is_indexed(&sdf::path("/A").unwrap()),
"attribute value writes must not invalidate the prim graph",
);
assert_eq!(attr.get().unwrap(), Some(sdf::Value::Double(2.0)));
}
#[test]
fn set_instanceable_invalidates_owner() {
let stage = open_in_memory();
stage.define_prim("/Inst").unwrap().set_type_name("Xform").unwrap();
stage.define_prim("/Other").unwrap().set_type_name("Xform").unwrap();
let _ = stage.prim("/Inst").unwrap().type_name().unwrap();
let _ = stage.prim("/Other").unwrap().type_name().unwrap();
assert!(stage.is_indexed(&sdf::path("/Inst").unwrap()));
stage.override_prim("/Inst").unwrap().set_instanceable(true).unwrap();
assert!(
!stage.is_indexed(&sdf::path("/Inst").unwrap()),
"instanceable is a significant-tier field; owner must be invalidated",
);
assert!(
stage.is_indexed(&sdf::path("/Other").unwrap()),
"unrelated prim must remain indexed",
);
}
#[test]
fn kind_change_no_op_for_cache() {
let stage = open_in_memory();
let prim = stage.define_prim("/A").unwrap().set_type_name("Xform").unwrap();
let _ = stage.prim("/A").unwrap().type_name().unwrap();
assert!(stage.is_indexed(&sdf::path("/A").unwrap()));
prim.set_kind("group").unwrap();
assert!(
stage.is_indexed(&sdf::path("/A").unwrap()),
"spec-only field changes must not invalidate the prim graph",
);
assert_eq!(stage.prim("/A").unwrap().kind().unwrap().as_deref(), Some("group"));
}
#[test]
fn default_prim_keeps_unrelated() {
let stage = open_in_memory();
stage.define_prim("/World").unwrap().set_type_name("Xform").unwrap();
stage.define_prim("/Other").unwrap().set_type_name("Xform").unwrap();
let _ = stage.prim("/World").unwrap().type_name().unwrap();
let _ = stage.prim("/Other").unwrap().type_name().unwrap();
let pre = stage.indexed_count();
assert!(pre >= 2);
stage.set_default_prim("World").unwrap();
assert_eq!(
stage.indexed_count(),
pre,
"no prim resolves an arc through the default, so none is evicted",
);
assert_eq!(stage.default_prim().as_deref(), Some("World"));
}
#[test]
fn override_after_cached_miss() {
let stage = open_in_memory();
assert!(!exists(&stage, "/A"));
assert!(stage.is_indexed(&sdf::path("/A").unwrap()));
stage.override_prim("/A").unwrap();
assert!(exists(&stage, "/A"), "inert add must invalidate cached empty index");
}
#[test]
fn define_invalidates_ancestors() {
let stage = open_in_memory();
assert!(!exists(&stage, "/A"));
assert!(!exists(&stage, "/A/B"));
stage.define_prim("/A/B/C").unwrap().set_type_name("Xform").unwrap();
assert!(exists(&stage, "/A"), "auto-created /A must be visible");
assert!(exists(&stage, "/A/B"), "auto-created /A/B must be visible");
assert!(child_names(&stage, "/A").contains(&"B".to_string()));
assert!(child_names(&stage, "/A/B").contains(&"C".to_string()));
}
#[test]
fn create_attribute_invalidates_owner() {
let stage = open_in_memory();
assert!(!exists(&stage, "/Mesh"));
stage.create_attribute("/Mesh.x", "double").unwrap();
assert!(
exists(&stage, "/Mesh"),
"auto-created owning prim /Mesh must be visible after create_attribute",
);
}
#[test]
fn idempotent_define_preserves_cache() {
let stage = open_in_memory();
stage.define_prim("/Foo").unwrap().set_type_name("Xform").unwrap();
stage.define_prim("/Foo/Child").unwrap();
let _ = stage.prim("/Foo").unwrap().type_name().unwrap();
let _ = stage.prim("/Foo/Child").unwrap().type_name().unwrap();
assert!(stage.is_indexed(&sdf::path("/Foo").unwrap()));
assert!(stage.is_indexed(&sdf::path("/Foo/Child").unwrap()));
stage.define_prim("/Foo").unwrap();
assert!(stage.is_indexed(&sdf::path("/Foo").unwrap()));
assert!(stage.is_indexed(&sdf::path("/Foo/Child").unwrap()));
}
#[test]
fn idempotent_override_preserves_cache() {
let stage = open_in_memory();
stage.define_prim("/Foo").unwrap();
let _ = stage.prim("/Foo").unwrap().type_name().unwrap();
assert!(stage.is_indexed(&sdf::path("/Foo").unwrap()));
stage.override_prim("/Foo").unwrap();
assert!(stage.is_indexed(&sdf::path("/Foo").unwrap()));
}
#[test]
fn add_applied_schema_invalidates_owner() {
let stage = open_in_memory();
let prim = stage.define_prim("/A").unwrap().set_type_name("Xform").unwrap();
assert_eq!(
stage.prim(prim.path()).unwrap().api_schemas().unwrap(),
Vec::<tf::Token>::new()
);
assert!(stage.is_indexed(&sdf::path("/A").unwrap()));
prim.add_applied_schema("MaterialBindingAPI").unwrap();
assert!(
!stage.is_indexed(&sdf::path("/A").unwrap()),
"apiSchemas authoring must invalidate the owner's cached prim index",
);
assert_eq!(
stage.prim("/A").unwrap().api_schemas().unwrap(),
vec![tf::Token::from("MaterialBindingAPI")],
);
}
#[test]
fn idempotent_default_prim_preserves_cache() {
let stage = open_in_memory();
stage.define_prim("/World").unwrap();
stage.set_default_prim("World").unwrap();
let _ = stage.prim("/World").unwrap().type_name().unwrap();
let pre = stage.indexed_count();
assert!(pre > 0);
stage.set_default_prim("World").unwrap();
assert_eq!(
stage.indexed_count(),
pre,
"redundant set_default_prim must not clear cached indices"
);
}
fn open_default_prim_fixture(dir: &tempfile::TempDir, default_prim: Option<&str>) -> Result<usd::Stage> {
let header = match default_prim {
Some(name) => format!("#usda 1.0\n(\n defaultPrim = \"{name}\"\n)\n"),
None => "#usda 1.0\n".to_string(),
};
fs::write(
dir.path().join("t.usda"),
format!("{header}def \"Source\" {{ custom double y = 1 }}\ndef \"Second\" {{ custom double y = 2 }}\n"),
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "Empty" (
references = @./t.usda@
)
{
}
def "Explicit" (
references = @./t.usda@</Source>
)
{
}
def "Source" {
custom double local = 3
}
"#,
)?;
let stage = usd::Stage::open(root.to_str().unwrap())?;
stage.attribute("/Empty.y")?.get::<f64>()?;
assert_eq!(stage.attribute("/Explicit.y")?.get::<f64>()?, Some(1.0));
assert_eq!(stage.attribute("/Source.local")?.get::<f64>()?, Some(3.0));
Ok(stage)
}
fn set_target_default_prim(stage: &usd::Stage, name: &str) -> Result<()> {
let target = identifier_by_leaf(stage, "t.usda");
stage
.layer_mut(&target)
.expect("target layer is live")
.edit(|e| e.set_default_prim(name))?;
Ok(())
}
#[test]
fn default_prim_resync_notice() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_default_prim_fixture(&dir, Some("Source"))?;
let notices = capture_notices(&stage);
set_target_default_prim(&stage, "Second")?;
assert!(!stage.is_indexed(&sdf::path("/Empty")?), "the consumer is evicted");
assert!(
stage.is_indexed(&sdf::path("/Explicit")?),
"an explicit target composes the same, so it is reported but not evicted"
);
assert_eq!(stage.attribute("/Empty.y")?.get::<f64>()?, Some(2.0));
let notices = notices.borrow();
assert_eq!(notices.resynced, paths(&["/Empty", "/Explicit"]));
assert_eq!(
notices.changed_info_only,
paths(&["/"]),
"the layer's own metadata changed, and no resync covers its pseudo-root"
);
Ok(())
}
#[test]
fn absent_default_notice() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_default_prim_fixture(&dir, None)?;
let notices = capture_notices(&stage);
assert_eq!(
stage.attribute("/Empty.y")?.get::<f64>()?,
None,
"nothing to resolve yet"
);
set_target_default_prim(&stage, "Source")?;
assert_eq!(
stage.attribute("/Empty.y")?.get::<f64>()?,
Some(1.0),
"the arc that had no default recomposes"
);
let notices = notices.borrow();
assert_eq!(notices.resynced, paths(&["/Empty"]));
assert_eq!(
notices.changed_info_only,
paths(&["/"]),
"no resync covers the pseudo-root, so the metadata change is still reported"
);
Ok(())
}
#[test]
fn root_layer_default_notice() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "Source" {
custom double y = 1
}
def "Empty" (
references = <>
)
{
}
def "Other" {
custom double o = 3
}
"#,
)?;
let stage = usd::Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/Empty.y")?.get::<f64>()?,
None,
"no default to resolve"
);
assert_eq!(stage.attribute("/Other.o")?.get::<f64>()?, Some(3.0));
let notices = capture_notices(&stage);
stage.set_default_prim("Source")?;
assert_eq!(
notices.borrow().resynced,
paths(&["/Empty"]),
"only the internal reference consults the field, though every prim reads this layer"
);
assert_eq!(
stage.attribute("/Empty.y")?.get::<f64>()?,
Some(1.0),
"and it recomposes against the new default"
);
Ok(())
}
#[test]
fn respelling_info_only_notice() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_default_prim_fixture(&dir, Some("Source"))?;
let pre = stage.indexed_count();
let notices = capture_notices(&stage);
set_target_default_prim(&stage, "/Source")?;
assert_eq!(stage.indexed_count(), pre, "nothing is evicted");
let notices = notices.borrow();
assert!(notices.resynced.is_empty(), "nothing resyncs: {:?}", notices.resynced);
assert_eq!(notices.changed_info_only, paths(&["/"]));
Ok(())
}
fn open_variant_fixture(dir: &tempfile::TempDir) -> Result<usd::Stage> {
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
(
expressionVariables = { string SEL = "x" }
)
def "User" (
variantSets = "v"
variants = { string v = "`${SEL}`" }
)
{
variantSet "v" = {
"x" { custom double vx = 1 }
"y" { custom double vy = 2 }
}
}
def "Other" {
custom double o = 3
}
"#,
)?;
let stage = usd::Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/User.vx")?.get::<f64>()?, Some(1.0), "SEL=x at open");
assert_eq!(stage.attribute("/Other.o")?.get::<f64>()?, Some(3.0));
assert!(stage.is_indexed(&sdf::path("/User")?));
assert!(stage.is_indexed(&sdf::path("/Other")?));
Ok(stage)
}
#[test]
fn unused_var_keeps_index() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_variant_fixture(&dir)?;
let notices = capture_notices(&stage);
stage.set_expression_variables(HashMap::from([
("SEL".to_string(), sdf::Value::String("x".to_string())),
("FREE".to_string(), sdf::Value::String("z".to_string())),
]))?;
{
let notices = notices.borrow();
assert!(
notices.resynced.is_empty(),
"no prim reads FREE, got {:?}",
notices.resynced
);
assert_eq!(
notices.asset_paths_resynced,
paths(&["/"]),
"the root stack's composed variables moved, so any asset value may re-resolve"
);
assert_eq!(notices.changed_info_only, paths(&["/"]));
}
assert!(
stage.is_indexed(&sdf::path("/User")?),
"/User reads SEL, whose value is unchanged — its index survives"
);
assert!(
stage.is_indexed(&sdf::path("/Other")?),
"/Other reads no variable — its index survives"
);
Ok(())
}
#[test]
fn used_var_drops_user() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_variant_fixture(&dir)?;
let notices = capture_notices(&stage);
stage.set_expression_variables(HashMap::from([(
"SEL".to_string(),
sdf::Value::String("y".to_string()),
)]))?;
{
let notices = notices.borrow();
assert_eq!(notices.resynced, paths(&["/User"]), "only the reader recomposes");
assert_eq!(notices.asset_paths_resynced, paths(&["/"]));
assert_eq!(notices.changed_info_only, paths(&["/"]));
}
assert!(
!stage.is_indexed(&sdf::path("/User")?),
"/User recorded reading SEL, so its index drops"
);
assert!(
stage.is_indexed(&sdf::path("/Other")?),
"/Other reads no variable — its index survives"
);
assert_eq!(
stage.attribute("/User.vy")?.get::<f64>()?,
Some(2.0),
"/User recomposes with the new selection"
);
assert_eq!(stage.attribute("/User.vx")?.get::<f64>()?, None);
Ok(())
}
#[test]
fn sublayer_dep_drops_stack() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
(
expressionVariables = { string W = "a" }
subLayers = [@`"${W}.usda"`@]
)
def "Other" {
custom double o = 3
}
"#,
)?;
fs::write(
dir.path().join("a.usda"),
r#"#usda 1.0
def "A" {
custom double x = 1
}
"#,
)?;
fs::write(
dir.path().join("b.usda"),
r#"#usda 1.0
def "B" {
custom double y = 2
}
"#,
)?;
let stage = usd::Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/A.x")?.get::<f64>()?, Some(1.0), "W=a at open");
assert_eq!(stage.attribute("/Other.o")?.get::<f64>()?, Some(3.0));
let notices = capture_notices(&stage);
stage.set_expression_variables(HashMap::from([("W".to_string(), sdf::Value::String("b".to_string()))]))?;
assert!(
!stage.is_indexed(&sdf::path("/Other")?),
"W selects a root-stack sublayer, so the change is stack-significant"
);
{
let notices = notices.borrow();
assert_eq!(notices.resynced, paths(&["/"]), "the whole root stack recomposes");
assert!(
notices.asset_paths_resynced.is_empty(),
"the stage-wide resync already covers every asset value, got {:?}",
notices.asset_paths_resynced
);
assert!(
notices.changed_info_only.is_empty(),
"the pseudo-root info change is subsumed by the pseudo-root resync"
);
}
assert_eq!(
stage.attribute("/B.y")?.get::<f64>()?,
Some(2.0),
"the newly selected b.usda loads and composes"
);
assert_eq!(
stage.attribute("/A.x")?.get::<f64>()?,
None,
"a.usda's selection dropped"
);
Ok(())
}
#[test]
fn source_change_resyncs() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "P" (
references = @t.usda@
) {}
def "Other" {
custom double o = 3
}
"#,
)?;
fs::write(
dir.path().join("t.usda"),
r#"#usda 1.0
(
defaultPrim = "P"
)
def "P" {
custom double x = 1
}
"#,
)?;
let stage = usd::Stage::open(root.to_str().unwrap())?;
assert_eq!(stage.attribute("/P.x")?.get::<f64>()?, Some(1.0));
assert_eq!(stage.attribute("/Other.o")?.get::<f64>()?, Some(3.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([("V".to_string(), sdf::Value::String("x".to_string()))]))
})?;
assert!(
!stage.is_indexed(&sdf::path("/P")?),
"the target stack's variable source flipped, so its user resyncs"
);
{
let notices = notices.borrow();
assert_eq!(notices.resynced, paths(&["/P"]));
assert!(
notices.asset_paths_resynced.is_empty(),
"the /P resync already covers its asset values, got {:?}",
notices.asset_paths_resynced
);
assert_eq!(notices.changed_info_only, paths(&["/"]));
}
assert!(
stage.is_indexed(&sdf::path("/Other")?),
"/Other does not use the target stack — its index survives"
);
assert_eq!(stage.attribute("/P.x")?.get::<f64>()?, Some(1.0), "/P recomposes");
Ok(())
}
#[test]
fn chain_propagation() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
(
expressionVariables = { string V = "x" }
)
def "P" (
references = @a.usda@</Outer/Inner>
) {}
def "Other" {
custom double o = 3
}
"#,
)?;
fs::write(
dir.path().join("a.usda"),
r#"#usda 1.0
def "Outer" (
references = @`"${V}.usda"`@
)
{
def "Inner" {}
}
"#,
)?;
fs::write(
dir.path().join("x.usda"),
r#"#usda 1.0
(
defaultPrim = "O"
)
def "O" {
def "Inner" {
custom double vx = 1
}
}
"#,
)?;
fs::write(
dir.path().join("y.usda"),
r#"#usda 1.0
(
defaultPrim = "O"
)
def "O" {
def "Inner" {
custom double vy = 2
}
}
"#,
)?;
let stage = usd::Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P.vx")?.get::<f64>()?,
Some(1.0),
"V=x reaches the referenced stack's `${{V}}` reference"
);
assert_eq!(stage.attribute("/Other.o")?.get::<f64>()?, Some(3.0));
let notices = capture_notices(&stage);
stage.set_expression_variables(HashMap::from([("V".to_string(), sdf::Value::String("y".to_string()))]))?;
assert!(
!stage.is_indexed(&sdf::path("/P")?),
"the delta cascades to the seeded target stack and finds /P's recorded read"
);
{
let notices = notices.borrow();
assert_eq!(notices.resynced, paths(&["/P"]), "only the recorded reader recomposes");
assert_eq!(
notices.asset_paths_resynced,
paths(&["/"]),
"the root stack moved too, so its pseudo-root subsumes the cascaded stack's users"
);
assert_eq!(notices.changed_info_only, paths(&["/"]));
}
assert!(
stage.is_indexed(&sdf::path("/Other")?),
"/Other reads no variable — its index survives"
);
assert_eq!(
stage.attribute("/P.vy")?.get::<f64>()?,
Some(2.0),
"/P recomposes against the newly selected y.usda"
);
assert_eq!(stage.attribute("/P.vx")?.get::<f64>()?, None);
Ok(())
}
#[test]
fn failure_records_dep() -> Result<()> {
let dir = tempfile::tempdir()?;
let root = dir.path().join("root.usda");
fs::write(
&root,
r#"#usda 1.0
def "P" (
references = @`${TGT}`@
)
{
custom double local = 5
}
"#,
)?;
fs::write(
dir.path().join("t.usda"),
r#"#usda 1.0
(
defaultPrim = "P"
)
def "P" {
custom double x = 1
}
"#,
)?;
let stage = usd::Stage::open(root.to_str().unwrap())?;
assert_eq!(
stage.attribute("/P.local")?.get::<f64>()?,
Some(5.0),
"the prim composes past the failed arc expression"
);
assert!(
stage
.composition_errors()
.iter()
.any(|e| matches!(e, pcp::CompositionDiagnostic::InvalidExpression { .. })),
"the undefined reference expression is diagnosed"
);
assert!(stage.is_indexed(&sdf::path("/P")?));
stage.set_expression_variables(HashMap::from([(
"TGT".to_string(),
sdf::Value::String("t.usda".to_string()),
)]))?;
assert_eq!(
stage.attribute("/P.x")?.get::<f64>()?,
Some(1.0),
"defining TGT resyncs /P and the repaired reference composes"
);
assert!(
stage.composition_errors().is_empty(),
"the healed expression stops reporting, got {:?}",
stage.composition_errors()
);
Ok(())
}
#[test]
fn reference_fixture_composes() {
let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let path = format!("{manifest}/fixtures/sublayer_override.usda");
let stage = usd::Stage::open(&path).expect("open sublayer fixture");
let children = child_names(&stage, "/World");
assert!(children.contains(&"Cube".to_string()));
assert!(children.contains(&"Sphere".to_string()));
assert!(stage.is_indexed(&sdf::path("/World").unwrap()));
}
#[test]
fn threshold_sweep_recomposes() -> Result<()> {
let dir = tempfile::tempdir()?;
let count = 40;
let mut root_text = String::from("#usda 1.0\n");
for i in 0..count {
root_text.push_str(&format!("def \"P{i}\" (\n references = @t{i}.usda@</P>\n) {{}}\n"));
fs::write(
dir.path().join(format!("t{i}.usda")),
format!("#usda 1.0\ndef \"P\" {{\n custom double x = {i}\n}}\n"),
)?;
}
let root = dir.path().join("root.usda");
fs::write(&root, root_text)?;
let stage = usd::Stage::open(root.to_str().unwrap())?;
for i in 0..count {
assert_eq!(stage.attribute(format!("/P{i}.x"))?.get::<f64>()?, Some(i as f64));
}
let t0 = identifier_by_leaf(&stage, "t0.usda");
stage.mute_layer(t0.clone());
assert_eq!(
stage.attribute("/P0.x")?.get::<f64>()?,
None,
"the muted target is gone"
);
assert!(
stage.is_indexed(&sdf::path("/P39")?),
"unrelated indices survive the sweep"
);
stage.unmute_layer(&t0);
for i in 0..count {
assert_eq!(
stage.attribute(format!("/P{i}.x"))?.get::<f64>()?,
Some(i as f64),
"every target composes after the sweep-and-recompose round trip"
);
}
assert!(
stage.composition_errors().is_empty(),
"no diagnostics accrete across the sweep, got {:?}",
stage.composition_errors()
);
Ok(())
}
fn clip_metadata(active_index: f64) -> sdf::Value {
let set = HashMap::from([
(
"assetPaths".to_string(),
sdf::Value::AssetPathVec(vec!["./clipA.usda".into(), "./clipB.usda".into()]),
),
(
"manifestAssetPath".to_string(),
sdf::Value::AssetPath("./manifest.usda".into()),
),
("primPath".to_string(), sdf::Value::String("/Model".to_string())),
(
"active".to_string(),
sdf::Value::Vec2dVec(vec![gf::vec2d(0.0, active_index)]),
),
]);
sdf::Value::Dictionary(HashMap::from([("default".to_string(), sdf::Value::Dictionary(set))]))
}
fn open_clip_fixture(dir: &tempfile::TempDir) -> Result<usd::Stage> {
for (name, sample) in [("clipA.usda", 1.0), ("clipB.usda", 2.0)] {
fs::write(
dir.path().join(name),
format!("#usda 1.0\ndef \"Model\"\n{{\n float size.timeSamples = {{\n 0: {sample}\n }}\n}}\n"),
)?;
}
fs::write(
dir.path().join("manifest.usda"),
"#usda 1.0\ndef \"Model\"\n{\n float size\n}\n",
)?;
let root = dir.path().join("root.usda");
fs::write(
&root,
"#usda 1.0\ndef \"Model\"\n{\n float size\n def \"Child\"\n {\n }\n}\n",
)?;
let stage = usd::Stage::open(root.to_str().unwrap())?;
stage
.prim("/Model")?
.set_metadata(sdf::FieldKey::Clips.as_str(), clip_metadata(0.0))?;
assert!(exists(&stage, "/Model/Child"));
assert_eq!(clip_value(&stage), Some(1.0));
Ok(stage)
}
fn clip_value(stage: &usd::Stage) -> Option<f64> {
stage
.attribute("/Model.size")
.unwrap()
.get_at::<sdf::Value>(usd::TimeCode::new(0.0))
.expect("value_at")
.map(|value| value.cast::<f64>().expect("numeric"))
}
#[test]
fn clip_edit_is_inert() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_clip_fixture(&dir)?;
let pre = stage.indexed_count();
let notices = capture_notices(&stage);
stage
.prim("/Model")?
.set_metadata(sdf::FieldKey::Clips.as_str(), clip_metadata(1.0))?;
assert_eq!(clip_value(&stage), Some(2.0), "the new schedule takes effect");
assert_eq!(stage.indexed_count(), pre, "nothing is evicted");
let notices = notices.borrow();
assert!(notices.resynced.is_empty(), "nothing resyncs: {:?}", notices.resynced);
Ok(())
}
#[test]
fn clip_removal_resyncs_subtree() -> Result<()> {
let dir = tempfile::tempdir()?;
let stage = open_clip_fixture(&dir)?;
let notices = capture_notices(&stage);
stage.prim("/Model")?.clear_metadata(sdf::FieldKey::Clips.as_str())?;
assert_eq!(clip_value(&stage), None, "the clips no longer source the value");
let notices = notices.borrow();
assert_eq!(notices.resynced, paths(&["/Model"]));
Ok(())
}
#[test]
fn info_change_names_referrer() {
let stage = open_in_memory();
stage.define_prim("/Source").unwrap();
stage.create_attribute("/Source.x", "double").unwrap();
stage
.override_prim("/Ref")
.unwrap()
.set_metadata(
sdf::FieldKey::References.as_str(),
sdf::Value::ReferenceListOp(sdf::ReferenceListOp::prepended([sdf::Reference {
prim_path: sdf::path("/Source").unwrap(),
..Default::default()
}])),
)
.unwrap();
assert_eq!(stage.attribute("/Ref.x").unwrap().get::<f64>().unwrap(), None);
let notices = capture_notices(&stage);
stage
.attribute("/Source.x")
.unwrap()
.set(sdf::Value::Double(4.0))
.unwrap();
let notices = notices.borrow();
assert!(
notices.changed_info_only.contains(&sdf::path("/Ref.x").unwrap()),
"the referrer's composed property must be reported, got {:?}",
notices.changed_info_only
);
assert!(
notices.changed_info_only.contains(&sdf::path("/Source.x").unwrap()),
"so must the authored path itself, got {:?}",
notices.changed_info_only
);
}
#[test]
fn relocated_resync_notice() -> Result<()> {
let dir = relocated_reference()?;
let stage = usd::Stage::open(dir.path().join("root.usda").to_str().unwrap())?;
assert!(stage.prim("/Ref/Moved")?.is_valid()?);
let notices = capture_notices(&stage);
let source = identifier_by_leaf(&stage, "source.usda");
stage.layer_mut(&source).expect("loaded").edit(|edit| {
edit.prim_mut(&sdf::path("/Source/Inner")?)
.expect("the source layer parsed")
.expect("the prim is authored there")
.set_active(false);
Ok(())
})?;
let _ = stage.prim("/Ref/Moved")?.is_valid()?;
let notices = notices.borrow();
assert!(
notices.resynced.contains(&sdf::path("/Ref/Moved")?),
"the relocated composed path must be resynced, got {:?}",
notices.resynced
);
assert!(
!notices.resynced.contains(&sdf::path("/Ref/Inner")?),
"the pre-relocation path is not a composed path, got {:?}",
notices.resynced
);
Ok(())
}
#[test]
fn relocated_info_notice() -> Result<()> {
let dir = relocated_reference()?;
let stage = usd::Stage::open(dir.path().join("root.usda").to_str().unwrap())?;
assert_eq!(stage.attribute("/Ref/Moved.x")?.get::<f64>()?, Some(1.0));
let notices = capture_notices(&stage);
let source = identifier_by_leaf(&stage, "source.usda");
stage.layer_mut(&source).expect("loaded").edit(|edit| {
edit.attribute_mut(&sdf::path("/Source/Inner.x")?)
.expect("the source layer parsed")
.expect("the attribute is authored there")
.set_default(sdf::Value::Double(2.0))?;
Ok(())
})?;
assert_eq!(stage.attribute("/Ref/Moved.x")?.get::<f64>()?, Some(2.0));
let notices = notices.borrow();
assert!(
notices.changed_info_only.contains(&sdf::path("/Ref/Moved.x")?),
"the relocated composed property must be named, got {:?}",
notices.changed_info_only
);
assert!(
!notices.changed_info_only.contains(&sdf::path("/Ref/Inner.x")?),
"the identity-derived path must not appear, got {:?}",
notices.changed_info_only
);
Ok(())
}
fn relocated_reference() -> Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
fs::write(
dir.path().join("source.usda"),
r#"#usda 1.0
def "Source"
{
def "Inner"
{
double x = 1
}
}
"#,
)?;
fs::write(
dir.path().join("root.usda"),
r#"#usda 1.0
(
relocates = {
</Ref/Inner>: </Ref/Moved>
}
)
def "Ref" (
references = @./source.usda@</Source>
)
{
}
"#,
)?;
Ok(dir)
}