mod error;
mod decl;
mod emit;
mod load;
mod plug_info;
mod resolve;
mod shader_defs;
mod tokens;
mod validate;
mod doc;
mod model;
mod names;
mod types;
use std::collections::{BTreeMap, BTreeSet};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
use openusd::{tf, usd};
use crate::model::Library;
pub(crate) type Externs = BTreeMap<String, String>;
pub use error::{Error, TokenEnumError};
pub use validate::Violation;
#[derive(Debug)]
pub struct Output {
pub rust: String,
pub views: bool,
pub warnings: Vec<String>,
library: model::Library,
source: PathBuf,
}
impl Output {
pub fn library_name(&self) -> &str {
&self.library.name
}
pub fn layers(&self) -> &[PathBuf] {
&self.library.source_layers
}
pub fn with_family<R>(&self, f: impl FnOnce(&usd::SchemaFamily<'_>) -> R) -> R {
decl::with_family(&self.library, f)
}
}
#[derive(Debug)]
pub struct NodeOutput {
pub rust: String,
layers: Vec<PathBuf>,
}
impl NodeOutput {
pub fn layers(&self) -> &[PathBuf] {
&self.layers
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Views {
Generate,
Skip,
}
#[derive(Debug, Clone)]
pub struct TokenEnum {
pub(crate) name: String,
pub(crate) library: String,
source: String,
pub(crate) default: bool,
pub(crate) variants: BTreeMap<String, String>,
}
impl TokenEnum {
pub fn new(name: impl Into<String>, library: impl Into<String>, source: impl Into<String>) -> Self {
TokenEnum {
name: name.into(),
library: library.into(),
source: source.into(),
default: false,
variants: BTreeMap::new(),
}
}
#[must_use]
pub fn with_default(mut self) -> Self {
self.default = true;
self
}
#[must_use]
pub fn variant(mut self, token: impl Into<String>, variant: impl Into<String>) -> Self {
self.variants.insert(token.into(), variant.into());
self
}
pub(crate) fn source(&self) -> Result<(&str, &str), TokenEnumError> {
self.source
.split_once('.')
.filter(|(class, property)| !class.is_empty() && !property.is_empty())
.ok_or_else(|| TokenEnumError::MalformedSource {
spelling: self.source.clone(),
})
}
}
pub fn configure() -> Builder {
Builder {
out_dir: None,
search_paths: Vec::new(),
extern_libraries: BTreeMap::new(),
schemas: Vec::new(),
token_enums: Vec::new(),
plug_infos: Vec::new(),
declaration_families: Vec::new(),
shader_defs: Vec::new(),
}
}
#[derive(Debug)]
pub struct Builder {
out_dir: Option<PathBuf>,
search_paths: Vec<PathBuf>,
extern_libraries: Externs,
schemas: Vec<(PathBuf, Views)>,
token_enums: Vec<TokenEnum>,
plug_infos: Vec<PathBuf>,
declaration_families: Vec<String>,
shader_defs: Vec<(String, PathBuf)>,
}
impl Builder {
#[must_use]
pub fn out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.out_dir = Some(dir.into());
self
}
#[must_use]
pub fn plug_info(mut self, path: impl Into<PathBuf>) -> Self {
self.plug_infos.push(path.into());
self
}
#[must_use]
pub fn declaration_family(mut self, name: impl Into<String>) -> Self {
self.declaration_families.push(name.into());
self
}
#[must_use]
pub fn shader_defs(mut self, library: impl Into<String>, path: impl Into<PathBuf>) -> Self {
self.shader_defs.push((library.into(), path.into()));
self
}
#[must_use]
pub fn search_path(mut self, dir: impl Into<PathBuf>) -> Self {
self.search_paths.push(dir.into());
self
}
#[must_use]
pub fn extern_library(mut self, library: impl Into<String>, rust_path: impl Into<String>) -> Self {
self.extern_libraries.insert(library.into(), rust_path.into());
self
}
#[must_use]
pub fn schema(mut self, path: impl Into<PathBuf>) -> Self {
self.schemas.push((path.into(), Views::Generate));
self
}
#[must_use]
pub fn schema_data(mut self, path: impl Into<PathBuf>) -> Self {
self.schemas.push((path.into(), Views::Skip));
self
}
#[must_use]
pub fn token_enum(mut self, declared: TokenEnum) -> Self {
self.token_enums.push(declared);
self
}
pub fn generate(self) -> Result<(), Error> {
if self.schemas.is_empty() && self.shader_defs.is_empty() && self.declaration_families.is_empty() {
return Ok(());
}
let out_dir = match &self.out_dir {
Some(dir) => dir.clone(),
None => env::var_os("OUT_DIR").map(PathBuf::from).ok_or(Error::NoOutDir)?,
};
fs::create_dir_all(&out_dir).map_err(|source| Error::Io {
path: out_dir.clone(),
source,
})?;
let plugins = self.plugins()?;
let mut outputs = self
.schemas
.iter()
.map(|(schema, views)| self.build(schema, *views, &plugins))
.collect::<Result<Vec<_>, Error>>()?;
for family in &self.declaration_families {
outputs.push(self.declare(family, &plugins)?);
}
let known: BTreeSet<&str> = outputs
.iter()
.map(Output::library_name)
.chain(self.extern_libraries.keys().map(String::as_str))
.collect();
if let Some(declared) = self
.token_enums
.iter()
.find(|declared| !known.contains(declared.library.as_str()))
{
return Err(Error::TokenEnum {
name: declared.name.clone(),
cause: TokenEnumError::UnknownLibrary {
library: declared.library.clone(),
},
});
}
for plugin in &plugins {
if !outputs.iter().any(|output| output.library_name() == plugin.name) {
return Err(Error::UnknownPlugin {
plugin: plugin.name.clone(),
path: plugin.path.clone(),
});
}
}
let nodes = self
.shader_defs
.iter()
.map(|(library, path)| Ok((library.as_str(), path.as_path(), self.build_shader_defs(library, path)?)))
.collect::<Result<Vec<_>, Error>>()?;
let mut destinations: BTreeMap<&str, &Path> = BTreeMap::new();
let named = outputs
.iter()
.map(|output| (output.library_name(), output.source.as_path()))
.chain(nodes.iter().map(|(library, path, _)| (*library, *path)));
for (library, source) in named {
if let Some(first) = destinations.insert(library, source) {
return Err(Error::DuplicateLibrary {
library: library.to_owned(),
first: first.to_path_buf(),
second: source.to_path_buf(),
});
}
}
let fields = declared_twice(&outputs, |library| {
library.metadata.iter().map(|field| field.name.as_str()).collect()
});
if let Some((field, first, second)) = fields {
return Err(Error::DuplicateMetadataField { field, first, second });
}
let kinds = declared_twice(&outputs, |library| {
library.kinds.iter().map(|kind| kind.name.as_str()).collect()
});
if let Some((kind, first, second)) = kinds {
return Err(Error::DuplicateKind { kind, first, second });
}
for (library, _, output) in &nodes {
write(&out_dir.join(format!("{library}.rs")), &output.rust)?;
for layer in output.layers() {
println!("cargo:rerun-if-changed={}", layer.display());
}
}
for output in &outputs {
for warning in &output.warnings {
println!("cargo:warning={warning}");
}
write(&out_dir.join(format!("{}.rs", output.library_name())), &output.rust)?;
for layer in output.layers() {
println!("cargo:rerun-if-changed={}", layer.display());
}
}
Ok(())
}
pub fn build_library(&self, schema: impl AsRef<Path>, views: Views) -> Result<Output, Error> {
self.build(schema.as_ref(), views, &self.plugins()?)
}
pub fn build_declarations(&self, family: &str) -> Result<Output, Error> {
self.declare(family, &self.plugins()?)
}
fn declare(&self, family: &str, plugins: &[plug_info::Plugin]) -> Result<Output, Error> {
if !tf::is_valid_identifier(family) {
return Err(Error::InvalidDeclarationFamily {
family: family.to_owned(),
});
}
let Some(plugin) = plugins.iter().find(|plugin| plugin.name == family) else {
return Err(Error::UnknownDeclarationFamily {
family: family.to_owned(),
});
};
let mut library = Library::declarations(family);
library.take_declarations(plugins);
let warnings = validate::check(&library)?;
self.emit(library, warnings, &plugin.path, Views::Generate)
}
fn emit(&self, library: Library, warnings: Vec<String>, source: &Path, views: Views) -> Result<Output, Error> {
let named = source.file_name().unwrap_or(source.as_os_str());
let rust = emit::emit(
&library,
&self.extern_libraries,
&named.to_string_lossy(),
views,
&self.token_enums,
)?;
Ok(Output {
rust,
views: views == Views::Generate,
warnings,
library,
source: source.to_path_buf(),
})
}
fn plugins(&self) -> Result<Vec<plug_info::Plugin>, Error> {
let mut plugins = Vec::new();
for path in &self.plug_infos {
plugins.extend(plug_info::read(path)?);
}
Ok(plugins)
}
pub fn build_shader_defs(&self, library: &str, path: impl AsRef<Path>) -> Result<NodeOutput, Error> {
let path = path.as_ref();
let shade = self.extern_libraries.get("usdShade").ok_or_else(|| Error::ShaderDefs {
path: path.to_path_buf(),
cause: "a node's view wraps `usdShade`'s `Shader`, and no extern_library places usdShade".to_owned(),
})?;
let shade: syn::Path = syn::parse_str(shade).map_err(|_| Error::ShaderDefs {
path: path.to_path_buf(),
cause: format!("usdShade is placed at `{shade}`, which is not a Rust path"),
})?;
let library = shader_defs::read(self, library, path)?;
let rust = emit::nodes(&library, &shade)?;
Ok(NodeOutput {
rust,
layers: library.layers,
})
}
fn build(&self, schema: &Path, views: Views, plugins: &[plug_info::Plugin]) -> Result<Output, Error> {
let (library, warnings) = self.read(schema, plugins)?;
let views = match library.skip_code_generation {
true => Views::Skip,
false => views,
};
self.emit(library, warnings, schema, views)
}
fn read(&self, schema: &Path, plugins: &[plug_info::Plugin]) -> Result<(Library, Vec<String>), Error> {
let source = load::open(self, schema)?;
let mut library = resolve::library(&source)?;
library.take_declarations(plugins);
Error::composition(schema, &source.stage).map_or(Ok(()), Err)?;
let warnings = validate::check(&library)?;
Ok((library, warnings))
}
}
fn declared_twice<'a>(
outputs: &'a [Output],
names: impl Fn(&'a Library) -> Vec<&'a str>,
) -> Option<(String, String, String)> {
let mut declared: BTreeMap<&str, &str> = BTreeMap::new();
for output in outputs {
for name in names(&output.library) {
if let Some(first) = declared.insert(name, output.library_name()) {
return Some((name.to_owned(), first.to_owned(), output.library_name().to_owned()));
}
}
}
None
}
fn write(path: &Path, contents: &str) -> Result<(), Error> {
if fs::read(path).is_ok_and(|current| current == contents.as_bytes()) {
return Ok(());
}
let mut staged = path.as_os_str().to_owned();
staged.push(format!(".tmp-{}", process::id()));
let staged = PathBuf::from(staged);
let written = fs::write(&staged, contents).map_err(|source| Error::Io {
path: staged.clone(),
source,
});
let replaced = written.and_then(|()| {
fs::rename(&staged, path).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})
});
if replaced.is_err() {
let _ = fs::remove_file(&staged);
}
replaced
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, SystemTime};
use openusd::sdf;
pub(crate) fn read_fixture(name: &str) -> Result<Library, Error> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/testUsdGenSchema");
configure()
.search_path(&dir)
.read(&dir.join(name), &[])
.map(|(library, _)| library)
}
pub(crate) fn read_source(dir: &Path, source: &str) -> Result<Library, Error> {
fs::write(dir.join("schema.usda"), source).expect("writes the schema");
configure()
.read(&dir.join("schema.usda"), &[])
.map(|(library, _)| library)
}
pub(crate) fn schema(library: &str, classes: &str) -> String {
format!(
r#"#usda 1.0
def "GLOBAL" (
customData = {{
string libraryName = "{library}"
}}
)
{{
}}
class "Typed" {{}}
class "APISchemaBase" {{}}
{classes}
"#
)
}
pub(crate) const TAG_API: &str = r#"class "TagAPI" (
inherits = </APISchemaBase>
customData = { token apiSchemaType = "singleApply" }
) {
string tag = ""
int dup = 0
}"#;
const MISSING_SUFFIX: &str = r#"#usda 1.0
def "GLOBAL" (
customData = {
string libraryName = "testWarn"
}
)
{
}
class "APISchemaBase"
{
}
class "MissingSuffix" (
inherits = </APISchemaBase>
customData = {
token apiSchemaType = "singleApply"
}
)
{
}
"#;
#[test]
fn build_library_reports_warnings() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("schema.usda");
fs::write(&path, MISSING_SUFFIX).expect("writes the schema");
let output = configure().build_library(&path, Views::Generate).expect("builds");
let warnings = &output.warnings;
assert_eq!(warnings.len(), 1, "{warnings:?}");
assert!(warnings[0].contains("conventionally ends in `API`"), "{warnings:?}");
}
#[test]
fn no_schemas_writes_nothing() {
let dir = tempfile::tempdir().expect("tempdir");
let out = dir.path().join("generated");
configure().out_dir(&out).generate().expect("nothing to do");
assert!(!out.exists(), "a run with no schemas creates no output directory");
}
#[test]
fn out_dir_created() {
let dir = tempfile::tempdir().expect("tempdir");
let out = dir.path().join("nested/generated");
let schema = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/testUsdGenSchema/schema.usda");
configure()
.out_dir(&out)
.search_path(schema.parent().expect("a parent"))
.extern_library("usd", "::openusd::usd")
.schema(&schema)
.generate()
.expect("generates");
assert!(out.is_dir(), "the output directory is created if it is missing");
}
fn missing_suffix(dir: &Path) -> PathBuf {
let path = dir.join("schema.usda");
fs::write(&path, MISSING_SUFFIX).expect("writes the schema");
path
}
#[test]
fn generated_file_written() {
let dir = tempfile::tempdir().expect("tempdir");
let schema = missing_suffix(dir.path());
let out = dir.path().join("generated");
configure().out_dir(&out).schema(&schema).generate().expect("generates");
let expected = configure()
.build_library(&schema, Views::Generate)
.expect("builds")
.rust;
let written = fs::read_to_string(out.join("testWarn.rs")).expect("reads the output");
assert_eq!(written, expected);
let names: Vec<_> = fs::read_dir(&out)
.expect("lists the output")
.map(|entry| entry.expect("an entry").file_name())
.collect();
assert_eq!(names, vec!["testWarn.rs"], "nothing else is left behind");
}
#[test]
fn unchanged_file_untouched() {
let dir = tempfile::tempdir().expect("tempdir");
let schema = missing_suffix(dir.path());
let out = dir.path().join("generated");
let file = out.join("testWarn.rs");
let generate = || configure().out_dir(&out).schema(&schema).generate();
generate().expect("generates");
let long_ago = SystemTime::UNIX_EPOCH + Duration::from_secs(1_000_000);
fs::File::options()
.write(true)
.open(&file)
.and_then(|written| written.set_modified(long_ago))
.expect("backdates the output");
generate().expect("regenerates");
let modified = || {
fs::metadata(&file)
.and_then(|meta| meta.modified())
.expect("reads mtime")
};
assert_eq!(modified(), long_ago, "the same text is not written again");
let changed = MISSING_SUFFIX.replace(
"class \"MissingSuffix\" (",
"class \"MissingSuffix\" (\n doc = \"Changed.\"",
);
fs::write(&schema, changed).expect("edits the schema");
generate().expect("regenerates");
assert_ne!(modified(), long_ago, "different text replaces the file");
assert!(fs::read_to_string(&file).expect("reads").contains("Changed."));
}
#[test]
fn duplicate_library_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let [first, second] = ["a", "b"].map(|name| {
let own = dir.path().join(name);
fs::create_dir(&own).expect("creates a schema directory");
missing_suffix(&own)
});
let out = dir.path().join("generated");
let error = configure()
.out_dir(&out)
.schema(&first)
.schema(&second)
.generate()
.expect_err("both declare testWarn");
match error {
Error::DuplicateLibrary {
library,
first: named_first,
second: named_second,
} => {
assert_eq!(library, "testWarn");
assert_eq!((named_first, named_second), (first, second));
}
other => panic!("{other}"),
}
assert!(!out.join("testWarn.rs").exists(), "nothing is written");
}
fn plug_info(dir: &Path, plugin: &str, field: &str) -> PathBuf {
let path = dir.join(format!("{plugin}-{field}.json"));
let text = format!(
r#"{{"Plugins": [{{"Name": "{plugin}", "Info": {{"SdfMetadata": {{"{field}": {{"type": "token"}}}}}}}}]}}"#
);
fs::write(&path, text).expect("writes the plugInfo");
path
}
fn mixed_plug_info(dir: &Path) -> PathBuf {
let path = dir.join("mixed.json");
fs::write(
&path,
r#"{"Plugins": [
{"Name": "testWarn", "Info": {
"SdfMetadata": {"role": {"type": "token"}},
"Kinds": {"prop": {"baseKind": "component"}}
}},
{"Name": "siteKinds", "Info": {"Kinds": {
"chargroup": {"baseKind": "assembly"},
"site_root": {}
}}}
]}"#,
)
.expect("writes the plugInfo");
path
}
fn declared_kinds(output: &Output) -> Vec<(String, Option<String>)> {
output.with_family(|family| {
family
.declared_kinds()
.iter()
.map(|kind| (kind.name().to_owned(), kind.declared_base().map(str::to_owned)))
.collect()
})
}
#[test]
fn mixed_plugins_generate() {
let dir = tempfile::tempdir().expect("tempdir");
let schema = missing_suffix(dir.path());
let info = mixed_plug_info(dir.path());
let out = dir.path().join("generated");
configure()
.out_dir(&out)
.schema(&schema)
.plug_info(&info)
.declaration_family("siteKinds")
.generate()
.expect("generates both");
let library = fs::read_to_string(out.join("testWarn.rs")).expect("the schema library is written");
assert!(
library.contains(r#"::openusd::kind::Decl::new("prop").base("component")"#),
"{library}"
);
let family = fs::read_to_string(out.join("siteKinds.rs")).expect("the declaration family is written");
assert!(
family.contains(r#"::openusd::kind::Decl::new("chargroup").base("assembly")"#),
"{family}"
);
assert!(
family.contains(r#"::openusd::kind::Decl::new("site_root")"#),
"{family}"
);
assert!(!family.contains("prop"), "{family}");
}
#[test]
fn declarations_alone_generate() {
let dir = tempfile::tempdir().expect("tempdir");
let info = mixed_plug_info(dir.path());
let out = dir.path().join("generated");
configure()
.out_dir(&out)
.plug_info(&info)
.generate()
.expect("nothing to do");
assert!(!out.exists(), "nothing is written");
let error = configure()
.out_dir(&out)
.plug_info(&info)
.declaration_family("siteKinds")
.generate()
.expect_err("testWarn has no schema library here");
assert!(
matches!(&error, Error::UnknownPlugin { plugin, .. } if plugin == "testWarn"),
"{error}"
);
let kinds = dir.path().join("kinds.json");
fs::write(
&kinds,
r#"{"Plugins": [{"Name": "siteKinds", "Info": {"Kinds": {"chargroup": {"baseKind": "assembly"}}}}]}"#,
)
.expect("writes the plugInfo");
configure()
.out_dir(&out)
.plug_info(&kinds)
.declaration_family("siteKinds")
.generate()
.expect("generates the family");
assert!(out.join("siteKinds.rs").exists());
}
#[test]
fn declaration_family_kinds() {
let dir = tempfile::tempdir().expect("tempdir");
let info = mixed_plug_info(dir.path());
let output = configure()
.plug_info(&info)
.build_declarations("siteKinds")
.expect("builds");
assert_eq!(output.library_name(), "siteKinds");
assert_eq!(output.layers(), [info]);
assert_eq!(
declared_kinds(&output),
[
("chargroup".to_owned(), Some("assembly".to_owned())),
("site_root".to_owned(), None)
]
);
output.with_family(|family| assert!(family.schemas().is_empty()));
}
#[test]
fn declaration_family_name_checked() {
let dir = tempfile::tempdir().expect("tempdir");
let out = dir.path().join("generated");
for name in ["../kinds", "site/kinds", "my-kinds", ""] {
let error = configure()
.out_dir(&out)
.plug_info(mixed_plug_info(dir.path()))
.declaration_family(name)
.generate()
.expect_err("not an identifier");
assert!(
matches!(&error, Error::InvalidDeclarationFamily { family } if family == name),
"{error}"
);
}
let written = fs::read_dir(&out).expect("the output directory").count();
assert_eq!(written, 0, "nothing is written");
assert!(!dir.path().join("kinds.rs").exists(), "nor beside it");
}
#[test]
fn unknown_declaration_family_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let schema = missing_suffix(dir.path());
let error = configure()
.out_dir(dir.path().join("generated"))
.schema(&schema)
.plug_info(mixed_plug_info(dir.path()))
.declaration_family("siteKindz")
.generate()
.expect_err("no plugin is called siteKindz");
assert!(
matches!(&error, Error::UnknownDeclarationFamily { family } if family == "siteKindz"),
"{error}"
);
}
#[test]
fn duplicate_kind_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let schema = missing_suffix(dir.path());
let info = dir.path().join("twice.json");
fs::write(
&info,
r#"{"Plugins": [
{"Name": "testWarn", "Info": {"Kinds": {"prop": {"baseKind": "component"}}}},
{"Name": "siteKinds", "Info": {"Kinds": {"prop": {}}}}
]}"#,
)
.expect("writes the plugInfo");
let error = configure()
.out_dir(dir.path().join("generated"))
.schema(&schema)
.plug_info(&info)
.declaration_family("siteKinds")
.generate()
.expect_err("both declare prop");
assert!(
matches!(&error, Error::DuplicateKind { kind, .. } if kind == "prop"),
"{error}"
);
}
#[test]
fn unknown_plugin_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let schema = missing_suffix(dir.path());
let info = plug_info(dir.path(), "testElsewhere", "role");
let error = configure()
.out_dir(dir.path().join("generated"))
.schema(&schema)
.plug_info(&info)
.generate()
.expect_err("no library is called testElsewhere");
assert!(
matches!(&error, Error::UnknownPlugin { plugin, path } if plugin == "testElsewhere" && path == &info),
"{error}"
);
}
#[test]
fn plugin_fields_reach_library() {
let dir = tempfile::tempdir().expect("tempdir");
let schema = missing_suffix(dir.path());
let info = plug_info(dir.path(), "testWarn", "role");
let output = configure()
.plug_info(&info)
.build_library(&schema, Views::Generate)
.expect("builds");
assert!(output.layers().contains(&info), "{:?}", output.layers());
let names: Vec<String> = output.with_family(|family| {
family
.declared_metadata()
.iter()
.map(|field| field.name().to_owned())
.collect()
});
assert_eq!(names, vec!["role"]);
}
#[test]
fn list_defaults_emitted() {
let dir = tempfile::tempdir().expect("tempdir");
let schema = missing_suffix(dir.path());
let info = dir.path().join("plugInfo.json");
fs::write(
&info,
r#"{"Plugins": [{"Name": "testWarn", "Info": {"SdfMetadata": {
"weights": {"type": "double[]", "default": [0.0, 1.0], "appliesTo": "layers"},
"up": {"type": "float3", "default": [0, 1, 0], "appliesTo": "layers"}
}}}]}"#,
)
.expect("writes the plugInfo");
let output = configure()
.plug_info(&info)
.build_library(&schema, Views::Generate)
.expect("builds");
let fallbacks: Vec<Option<sdf::Value>> = output.with_family(|family| {
family
.declared_metadata()
.iter()
.map(usd::MetadataDecl::declared_fallback)
.collect()
});
assert!(
fallbacks.contains(&Some(sdf::Value::DoubleVec(vec![0.0, 1.0]))),
"{fallbacks:?}"
);
assert!(output.rust.contains(".fallback(||"), "{}", output.rust);
}
#[test]
fn duplicate_field_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let first = missing_suffix(dir.path());
let second_dir = dir.path().join("other");
fs::create_dir(&second_dir).expect("creates a schema directory");
let second = second_dir.join("schema.usda");
fs::write(&second, MISSING_SUFFIX.replace("testWarn", "testOther")).expect("writes the schema");
let error = configure()
.out_dir(dir.path().join("generated"))
.schema(&first)
.schema(&second)
.plug_info(plug_info(dir.path(), "testWarn", "role"))
.plug_info(plug_info(dir.path(), "testOther", "role"))
.generate()
.expect_err("both declare role");
assert!(
matches!(&error, Error::DuplicateMetadataField { field, .. } if field == "role"),
"{error}"
);
}
#[test]
fn shader_def_sublayers_watched() {
let dir = tempfile::tempdir().expect("tempdir");
let base = dir.path().join("base.usda");
fs::write(
&base,
"#usda 1.0\n\ndef Shader \"UsdBase\"\n{\n uniform token info:id = \"UsdBase\"\n}\n",
)
.expect("writes the sublayer");
let root = dir.path().join("shaderDefs.usda");
fs::write(&root, "#usda 1.0\n(\n subLayers = [@./base.usda@]\n)\n").expect("writes the root");
let output = configure()
.extern_library("usdShade", "crate::shade")
.build_shader_defs("testNodes", &root)
.expect("generates");
let watched: Vec<_> = output
.layers()
.iter()
.map(|layer| layer.file_name().expect("a file").to_owned())
.collect();
assert_eq!(watched, vec!["shaderDefs.usda", "base.usda"]);
assert!(output.rust.contains("pub struct Base("), "{}", output.rust);
}
#[test]
fn shader_def_references_watched() {
let dir = tempfile::tempdir().expect("tempdir");
fs::write(
dir.path().join("base.usda"),
"#usda 1.0\n\ndef Shader \"Base\"\n{\n float inputs:gain = 1\n}\n",
)
.expect("writes the referenced layer");
let root = dir.path().join("shaderDefs.usda");
fs::write(
&root,
"#usda 1.0\n\ndef Shader \"UsdGain\" (\n references = @./base.usda@</Base>\n)\n{\n uniform token info:id = \"UsdGain\"\n}\n",
)
.expect("writes the root");
let output = configure()
.extern_library("usdShade", "crate::shade")
.build_shader_defs("testNodes", &root)
.expect("generates");
assert!(output.rust.contains("pub fn gain_input("), "{}", output.rust);
let watched: Vec<_> = output
.layers()
.iter()
.map(|layer| layer.file_name().expect("a file").to_owned())
.collect();
assert_eq!(watched, vec!["shaderDefs.usda", "base.usda"]);
}
#[test]
fn shader_def_missing_reference_refused() {
let dir = tempfile::tempdir().expect("tempdir");
let root = dir.path().join("shaderDefs.usda");
fs::write(
&root,
"#usda 1.0\n\ndef Shader \"UsdGain\" (\n references = @./missing.usda@</Base>\n)\n{\n uniform token info:id = \"UsdGain\"\n}\n",
)
.expect("writes the root");
let error = configure()
.extern_library("usdShade", "crate::shade")
.build_shader_defs("testNodes", &root)
.expect_err("the reference resolves to nothing");
assert!(matches!(error, Error::Composition { .. }), "{error}");
}
#[test]
fn shader_defs_alone_generated() {
let dir = tempfile::tempdir().expect("tempdir");
let out = dir.path().join("generated");
let defs = Path::new(env!("CARGO_MANIFEST_DIR")).join("fixtures/nodes/shaderDefs.usda");
configure()
.out_dir(&out)
.extern_library("usdShade", "crate::shade")
.shader_defs("tinyNodes", &defs)
.generate()
.expect("generates");
assert!(out.join("tinyNodes.rs").is_file(), "the node library is written");
}
#[test]
fn missing_out_dir_reported() {
let error = configure()
.schema("schemas/usdGeom/schema.usda")
.generate()
.expect_err("this crate has no build script, so its tests run without OUT_DIR set");
assert!(matches!(error, Error::NoOutDir), "{error}");
}
#[test]
fn repeated_options() {
let builder = configure()
.schema("schemas/usdGeom/schema.usda")
.schema("schemas/usdLux/schema.usda")
.extern_library("usdGeom", "crate::geom")
.extern_library("usdGeom", "openusd_schemas::geom");
assert_eq!(builder.schemas.len(), 2);
assert_eq!(
builder.extern_libraries.get("usdGeom").map(String::as_str),
Some("openusd_schemas::geom")
);
}
}