use openusd::{sdf, usd};
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_at(path).is_valid().unwrap()
}
fn child_names(stage: &usd::Stage, path: &str) -> Vec<String> {
stage.prim_at(path).child_names().unwrap()
}
#[test]
fn author_at_one_prim_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_at(sdf::path("/Foo").unwrap()).type_name().unwrap();
let _ = stage.prim_at(sdf::path("/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 attribute_value_write_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_at(sdf::path("/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_at(sdf::path("/Inst").unwrap()).type_name().unwrap();
let _ = stage.prim_at(sdf::path("/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_at(sdf::path("/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_at("/A").kind().unwrap().as_deref(), Some("group"));
}
#[test]
fn default_prim_change_clears_root_cache() {
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_at(sdf::path("/World").unwrap()).type_name().unwrap();
let _ = stage.prim_at(sdf::path("/Other").unwrap()).type_name().unwrap();
let pre = stage.indexed_count();
assert!(pre >= 2);
stage.set_default_prim("World").unwrap();
assert_eq!(
stage.indexed_count(),
0,
"defaultPrim is significant-at-root; all indices must drop",
);
assert_eq!(stage.default_prim().as_deref(), Some("World"));
}
#[test]
fn override_prim_after_cached_miss_invalidates() {
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 — otherwise has_spec keeps returning the pre-author miss",
);
}
#[test]
fn define_prim_invalidates_auto_created_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 ancestor must be visible after define_prim('/A/B/C')",
);
assert!(exists(&stage, "/A/B"), "auto-created /A/B ancestor 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_auto_created_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_prim_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_at(sdf::path("/Foo").unwrap()).type_name().unwrap();
let _ = stage.prim_at(sdf::path("/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()),
"redundant define_prim with matching specifier must not drop the cached index",
);
assert!(
stage.is_indexed(&sdf::path("/Foo/Child").unwrap()),
"redundant define_prim must not invalidate descendant indices",
);
}
#[test]
fn idempotent_override_prim_preserves_cache() {
let stage = open_in_memory();
stage.define_prim("/Foo").unwrap();
let _ = stage.prim_at(sdf::path("/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()),
"redundant override_prim on existing spec must not drop the cached index",
);
}
#[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_at(prim.path()).api_schemas().unwrap(), Vec::<String>::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_at(sdf::path("/A").unwrap()).api_schemas().unwrap(),
vec!["MaterialBindingAPI".to_string()],
);
}
#[test]
fn idempotent_set_default_prim_preserves_cache() {
let stage = open_in_memory();
stage.define_prim("/World").unwrap();
stage.set_default_prim("World").unwrap();
let _ = stage.prim_at(sdf::path("/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",
);
}
#[test]
fn reference_fixture_composes_correctly() {
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()),
"/World index must be cached after prim_children query",
);
}