use std::collections::HashMap;
use std::fs;
use std::path::Path as FsPath;
use anyhow::Result;
use openusd::{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).info_changed.insert(tf::Token::new("specifier"));
assert_eq!(cl.entries().len(), 1);
assert!(cl.entries()[0].1.flags.contains(sdf::ChangeFlags::ADD_NON_INERT_PRIM));
assert!(cl.entries()[0].1.info_changed.contains(&tf::Token::new("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).is_valid().unwrap()
}
fn child_names(stage: &usd::Stage, path: &str) -> Vec<String> {
stage
.prim(path)
.child_names()
.unwrap()
.into_iter()
.map(String::from)
.collect()
}
#[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(sdf::path("/Foo").unwrap()).type_name().unwrap();
let _ = stage.prim(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 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(sdf::path("/Foo").unwrap()).type_name().unwrap();
let _ = stage.prim(sdf::path("/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(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(sdf::path("/Inst").unwrap()).type_name().unwrap();
let _ = stage.prim(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(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("/A").kind().unwrap().as_deref(), Some("group"));
}
#[test]
fn default_prim_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(sdf::path("/World").unwrap()).type_name().unwrap();
let _ = stage.prim(sdf::path("/Other").unwrap()).type_name().unwrap();
assert!(stage.indexed_count() >= 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_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(sdf::path("/Foo").unwrap()).type_name().unwrap();
let _ = stage.prim(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()));
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(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()));
}
#[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()).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(sdf::path("/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(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"
);
}
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)?;
stage.set_expression_variables(HashMap::from([
("SEL".to_string(), sdf::Value::String("x".to_string())),
("FREE".to_string(), sdf::Value::String("z".to_string())),
]))?;
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)?;
stage.set_expression_variables(HashMap::from([(
"SEL".to_string(),
sdf::Value::String("y".to_string()),
)]))?;
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));
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"
);
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 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("x".to_string()))]))
})?;
assert!(
!stage.is_indexed(&sdf::path("/P")?),
"the target stack's variable source flipped, so its user resyncs"
);
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));
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"
);
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::Error::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()));
}