use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::mem;
use bitflags::bitflags;
use crate::sdf;
use crate::sdf::schema::FieldKey;
use crate::sdf::{ChangeEntry, ChangeList, Path};
use crate::tf;
use super::clip;
use super::index_store::{ScopedInvalidation, ValueScope};
use super::layer_graph::LayerGraph;
use super::layer_stack::StackVarsDelta;
use super::prim_index::{PropertyTargetKind, TargetMemoKey};
use super::{IndexCache, LayerId, LayerStackId};
#[derive(Debug, Default)]
pub(crate) struct Changes {
pub cache: CacheChanges,
pub layer_stack: LayerStackChanges,
default_prim_edits: Vec<(LayerId, Option<tf::Token>)>,
layer_stack_layers: HashSet<LayerId>,
edited_layers: HashSet<LayerId>,
type_opinions: bool,
report: bool,
}
pub(crate) struct LayerChanges<'a> {
pub layer: LayerId,
pub changes: &'a ChangeList,
pub prior_default_prim: Option<tf::Token>,
}
#[cfg(test)]
impl<'a> LayerChanges<'a> {
pub(crate) fn plain(layer: LayerId, changes: &'a ChangeList) -> Self {
Self {
layer,
changes,
prior_default_prim: None,
}
}
}
#[derive(Debug, Default)]
pub struct CacheChanges {
pub(crate) did_change_significantly: BTreeSet<Path>,
pub(crate) authored_significant: BTreeSet<Path>,
pub(crate) did_change_prims: BTreeSet<Path>,
pub(crate) did_change_specs: BTreeSet<(LayerId, Path)>,
pub(crate) did_change_values: BTreeMap<Path, ScopedInvalidation>,
pub(crate) reported: ReportedPaths,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Report {
Info,
Resync,
}
#[derive(Debug, Default)]
pub(crate) struct ReportedPaths {
pub stage_info: BTreeSet<Path>,
pub authored_info: BTreeSet<Path>,
pub stage_resync: BTreeSet<Path>,
pub authored_resync: BTreeSet<Path>,
}
#[derive(Default)]
struct InvalidationEffects {
significant: bool,
spec: bool,
value: Option<ValueScope>,
targets: Vec<TargetMemoKey>,
properties: bool,
type_opinion: bool,
report: Option<Report>,
stack: LayerStackChanges,
default_prim: bool,
}
impl CacheChanges {
pub(crate) fn stage_resynced_paths(&self) -> impl Iterator<Item = &Path> {
self.did_change_significantly
.iter()
.chain(self.did_change_prims.iter())
.chain(self.reported.stage_resync.iter())
}
pub(crate) fn authored_resynced_paths(&self) -> impl Iterator<Item = &Path> {
self.authored_significant
.iter()
.chain(self.did_change_specs.iter().map(|(_, path)| path))
.chain(self.reported.authored_resync.iter())
}
pub(crate) fn stage_subtree_paths(&self) -> impl Iterator<Item = &Path> {
self.did_change_significantly.iter()
}
pub(crate) fn authored_subtree_paths(&self) -> impl Iterator<Item = &Path> {
self.authored_significant.iter()
}
fn all_significant(&self) -> impl Iterator<Item = &Path> {
self.did_change_significantly.iter().chain(
self.authored_significant
.iter()
.filter(|path| !self.did_change_significantly.contains(*path)),
)
}
}
bitflags! {
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct LayerStackChanges: u8 {
const LAYERS = 1 << 0;
const OFFSETS = 1 << 1;
const RELOCATES = 1 << 2;
const SIGNIFICANT = 1 << 3;
const TIME_CODES = 1 << 4;
const EXPRESSION_VARS = 1 << 5;
const NEEDS_LAYER_STACK_REBUILD =
Self::LAYERS.bits() | Self::OFFSETS.bits() | Self::TIME_CODES.bits() | Self::EXPRESSION_VARS.bits();
const NEEDS_RELOCATES_REBUILD = Self::LAYERS.bits() | Self::RELOCATES.bits();
}
}
#[derive(Debug, Default)]
pub(crate) struct ApplyOutcome {
pub(crate) resynced: Vec<Path>,
pub(crate) resynced_prims: Vec<Path>,
pub(crate) asset_paths_resynced: Vec<Path>,
}
impl Changes {
pub fn new() -> Self {
Self::default()
}
pub fn reporting() -> Self {
Self {
report: true,
..Self::default()
}
}
pub fn did_change(&mut self, cache: &IndexCache, graph: &LayerGraph, changes: &[LayerChanges<'_>]) {
for edit in changes {
self.edited_layers.insert(edit.layer);
for (path, entry) in edit.changes.entries() {
self.apply_effects(cache, graph, edit, path, Self::effects_of(path, entry));
}
}
}
fn effects_of(path: &Path, entry: &ChangeEntry) -> InvalidationEffects {
let mut effects = InvalidationEffects::default();
if entry.is_child_bookkeeping() {
return effects;
}
if path.is_abs_root() {
effects.stack = Self::stack_flags(entry);
effects.default_prim = entry.changed(FieldKey::DefaultPrim.as_str());
effects.type_opinion = entry.changed(FieldKey::FallbackPrimTypes.as_str());
effects.value = Some(ValueScope::Prim);
effects.report = Some(Report::Info);
return effects;
}
if path.is_property_path() {
effects.value = Some(ValueScope::Prim);
effects.targets = Self::target_keys(path, entry);
let shape = entry
.flags
.intersects(sdf::ChangeFlags::ADD_PROPERTY | sdf::ChangeFlags::REMOVE_PROPERTY);
effects.properties = shape;
effects.report = Some(if shape { Report::Resync } else { Report::Info });
return effects;
}
effects.report = Some(Report::Info);
effects.type_opinion =
entry.changed(FieldKey::TypeName.as_str()) || entry.changed(FieldKey::ApiSchemas.as_str());
let significant = entry.flags.intersects(sdf::ChangeFlags::NON_INERT_PRIM)
|| entry.fields().any(|(field, change)| {
Self::field_promotes_to_significant(field.as_str())
|| (change == sdf::FieldChange::Presence && clip::is_clip_field(field.as_str()))
});
if significant {
effects.significant = true;
return effects;
}
if entry.flags.intersects(sdf::ChangeFlags::INERT_PRIM) {
effects.spec = true;
return effects;
}
let clips = entry.authored_fields().any(|field| clip::is_clip_field(field.as_str()));
effects.value = Some(if clips { ValueScope::Subtree } else { ValueScope::Prim });
effects
}
fn stack_flags(entry: &ChangeEntry) -> LayerStackChanges {
let mut flags = LayerStackChanges::empty();
for key in entry.authored_fields() {
if *key == FieldKey::SubLayers.as_str() {
flags |= LayerStackChanges::LAYERS | LayerStackChanges::SIGNIFICANT;
} else if *key == FieldKey::SubLayerOffsets.as_str() {
flags |= LayerStackChanges::OFFSETS | LayerStackChanges::SIGNIFICANT;
} else if *key == FieldKey::LayerRelocates.as_str() {
flags |= LayerStackChanges::RELOCATES | LayerStackChanges::SIGNIFICANT;
} else if *key == FieldKey::TimeCodesPerSecond.as_str() || *key == FieldKey::FramesPerSecond.as_str() {
flags |= LayerStackChanges::TIME_CODES | LayerStackChanges::SIGNIFICANT;
} else if *key == FieldKey::ExpressionVariables.as_str() {
flags |= LayerStackChanges::EXPRESSION_VARS;
}
}
flags
}
fn target_keys(path: &Path, entry: &ChangeEntry) -> Vec<TargetMemoKey> {
let is_connection = entry.flags.contains(sdf::ChangeFlags::CHANGE_ATTRIBUTE_CONNECTION)
|| entry.changed(FieldKey::ConnectionPaths.as_str());
let is_relationship = entry.flags.contains(sdf::ChangeFlags::CHANGE_RELATIONSHIP_TARGETS)
|| entry.changed(FieldKey::TargetPaths.as_str());
if !is_connection && !is_relationship {
return Vec::new();
}
let suffix = path.property_suffix();
[
is_relationship.then_some(PropertyTargetKind::Relationship),
is_connection.then_some(PropertyTargetKind::Connection),
]
.into_iter()
.flatten()
.map(|kind| TargetMemoKey {
kind,
property_suffix: suffix.to_owned(),
})
.collect()
}
fn apply_effects(
&mut self,
cache: &IndexCache,
graph: &LayerGraph,
edit: &LayerChanges<'_>,
path: &Path,
effects: InvalidationEffects,
) {
let layer = edit.layer;
if effects.significant {
self.fanout_significant(cache, graph, layer, path);
if path.contains_prim_variant_selection() {
self.fanout_significant(cache, graph, layer, &path.strip_all_variant_selections());
}
}
if effects.spec {
self.cache.did_change_specs.insert((layer, path.clone()));
}
if let Some(scope) = effects.value {
let work = ScopedInvalidation {
scope,
properties: effects.properties,
target_keys: effects.targets.into_iter().collect(),
};
let prim = path.prim_path();
self.fanout_values(cache, graph, layer, &prim, &work);
if prim.contains_prim_variant_selection() {
let stripped = prim.strip_all_variant_selections();
self.fanout_values(cache, graph, layer, &stripped, &work);
}
}
self.type_opinions |= effects.type_opinion;
if !effects.stack.is_empty() {
self.layer_stack |= effects.stack;
self.layer_stack_layers.insert(layer);
}
if let Some(report) = effects.report.filter(|_| self.report) {
self.report_entry(cache, graph, layer, path, report);
}
if effects.default_prim {
self.default_prim_edits.push((layer, edit.prior_default_prim.clone()));
}
}
fn report_entry(&mut self, cache: &IndexCache, graph: &LayerGraph, layer: LayerId, path: &Path, report: Report) {
let reported = &mut self.cache.reported;
let (stage, authored) = match report {
Report::Info => (&mut reported.stage_info, &mut reported.authored_info),
Report::Resync => (&mut reported.stage_resync, &mut reported.authored_resync),
};
authored.insert(path.clone());
stage.extend(cache.store().graph_ancestor_lookup(graph, layer, path));
}
fn fanout_values(
&mut self,
cache: &IndexCache,
graph: &LayerGraph,
layer: LayerId,
prim: &Path,
work: &ScopedInvalidation,
) {
for dep in cache.store().dependencies().exact_lookup(layer, prim) {
self.record_scoped(dep, work);
}
for dep in cache.store().translated_ancestor_dependents(graph, layer, prim) {
self.record_scoped(dep, work);
}
self.record_scoped(prim.clone(), work);
}
fn record_scoped(&mut self, path: Path, work: &ScopedInvalidation) {
let item = self.cache.did_change_values.entry(path).or_default();
item.scope = item.scope.max(work.scope);
item.target_keys.extend(work.target_keys.iter().cloned());
item.properties |= work.properties;
}
fn fanout_significant(&mut self, cache: &IndexCache, graph: &LayerGraph, layer: LayerId, path: &Path) {
for dep in cache.store().lookup_with_ancestors(graph, layer, path) {
self.cache.did_change_significantly.insert(dep);
}
for dep in cache.store().dependencies().subtree_lookup(layer, path) {
self.cache.did_change_significantly.insert(dep);
}
self.cache.authored_significant.insert(path.clone());
}
fn field_promotes_to_significant(field: &str) -> bool {
field == FieldKey::References.as_str()
|| field == FieldKey::Payload.as_str()
|| field == FieldKey::InheritPaths.as_str()
|| field == FieldKey::Specializes.as_str()
|| field == FieldKey::VariantSetNames.as_str()
|| field == FieldKey::VariantSelection.as_str()
|| field == FieldKey::Instanceable.as_str()
|| field == FieldKey::Specifier.as_str()
|| field == FieldKey::Active.as_str()
|| field == FieldKey::ApiSchemas.as_str()
|| field == FieldKey::Relocates.as_str()
}
fn touches_population(&self) -> bool {
!self.layer_stack.is_empty()
|| !self.default_prim_edits.is_empty()
|| self.cache.all_significant().next().is_some()
|| !self.cache.did_change_prims.is_empty()
|| !self.cache.did_change_specs.is_empty()
}
pub fn apply(mut self, cache: &mut IndexCache, graph: &mut LayerGraph) -> ApplyOutcome {
cache.retire_query_errors();
if self.type_opinions {
cache.bump_type_opinion_epoch();
}
if self.touches_population() {
cache.invalidate_population();
}
let (affected, vars_deltas) = if self
.layer_stack
.intersects(LayerStackChanges::NEEDS_LAYER_STACK_REBUILD)
{
let recompute = graph.recompute_sublayers(Some(&self.layer_stack_layers));
(recompute.affected, recompute.vars_deltas)
} else if self.layer_stack.intersects(LayerStackChanges::NEEDS_RELOCATES_REBUILD) {
let mut relocated = graph.recompute_relocates();
relocated.extend(self.layer_stack_layers.iter().copied());
(relocated, Vec::new())
} else {
(HashSet::new(), Vec::new())
};
let asset_paths_resynced = asset_path_victims(cache, &vars_deltas);
for victim in &asset_paths_resynced {
self.record_scoped(
victim.clone(),
&ScopedInvalidation {
scope: ValueScope::Subtree,
..ScopedInvalidation::default()
},
);
}
let mut resynced = if self.layer_stack.contains(LayerStackChanges::SIGNIFICANT) {
cache.invalidate_layers(&affected);
vec![Path::abs_root()]
} else {
apply_vars_deltas(cache, graph, &vars_deltas)
};
resynced.extend(apply_default_prim_edits(cache, graph, &self.default_prim_edits));
let changed: Vec<Path> = self
.cache
.all_significant()
.chain(self.cache.did_change_prims.iter())
.map(Path::prim_path)
.chain(self.cache.did_change_specs.iter().map(|(_, path)| path.prim_path()))
.collect();
if !changed.is_empty() {
resynced.extend(cache.invalidate_prototypes(&changed));
}
for path in self.cache.all_significant() {
cache.drop_index_subtree(path);
}
for path in &self.cache.did_change_prims {
if self.cache.did_change_significantly.iter().any(|p| path.has_prefix(p)) {
continue;
}
cache.drop_index(path);
}
let sites: Vec<(LayerId, Path)> = mem::take(&mut self.cache.did_change_specs)
.into_iter()
.filter(|(_, path)| !self.cache.authored_significant.iter().any(|p| path.has_prefix(p)))
.collect();
let mut resynced_prims = Vec::new();
if !sites.is_empty() {
resynced_prims = cache.rescan_specs(graph, &sites);
}
cache.restale_values(graph, normalize_scopes(mem::take(&mut self.cache.did_change_values)));
if cache.has_clip_sources() {
for &layer in &self.edited_layers {
let Some(identifier) = graph.try_identifier(layer) else {
continue;
};
resynced.extend(cache.invalidate_clip_source(identifier));
}
}
ApplyOutcome {
resynced,
resynced_prims,
asset_paths_resynced,
}
}
}
fn normalize_scopes(items: BTreeMap<Path, ScopedInvalidation>) -> Vec<(Path, ScopedInvalidation)> {
let mut out: Vec<(Path, ScopedInvalidation)> = items.into_iter().collect();
out.dedup_by(|(path, item), (root, covering)| {
let absorbed = covering.scope == ValueScope::Subtree && path != root && path.has_prefix(root);
if absorbed {
covering.target_keys.append(&mut item.target_keys);
covering.properties |= item.properties;
}
absorbed
});
out
}
fn asset_path_victims(cache: &IndexCache, deltas: &[StackVarsDelta]) -> Vec<Path> {
let mut victims = Vec::new();
for delta in deltas.iter().filter(|delta| delta.old_expr != delta.new_expr) {
victims.extend(cache.store().dependencies().prims_for_stack(delta.stack));
if delta.stack == LayerStackId::ROOT {
break;
}
}
victims
}
fn apply_default_prim_edits(
cache: &mut IndexCache,
graph: &LayerGraph,
edits: &[(LayerId, Option<tf::Token>)],
) -> Vec<Path> {
let mut reported: BTreeSet<Path> = BTreeSet::new();
let mut victims: BTreeSet<Path> = BTreeSet::new();
for (layer, prior) in edits {
let old = prior.as_deref().and_then(sdf::default_prim_path);
let current = graph
.default_prim_token(*layer)
.as_deref()
.and_then(sdf::default_prim_path);
if old == current {
continue;
}
if let Some(path) = &old {
reported.extend(cache.store().graph_ancestor_lookup(graph, *layer, path));
reported.extend(cache.store().dependencies().subtree_lookup(*layer, path));
}
victims.extend(cache.store().dependencies().prims_using_default_prim(*layer));
}
reported.extend(cache.drop_index_victims(victims.into_iter().collect()));
reported.into_iter().collect()
}
fn apply_vars_deltas(cache: &mut IndexCache, graph: &LayerGraph, deltas: &[StackVarsDelta]) -> Vec<Path> {
let mut victims: BTreeSet<Path> = BTreeSet::new();
for delta in deltas {
if delta.old_source == delta.new_source {
let changed = graph.changed_var_names(delta.old_expr, delta.new_expr);
if graph.stack_sublayer_var_deps(delta.stack).is_disjoint(&changed) {
victims.extend(cache.store().dependencies().prims_using_vars(delta.stack, &changed));
continue;
}
}
if delta.stack == LayerStackId::ROOT {
return cache.drop_index_victims(vec![Path::abs_root()]);
}
victims.extend(cache.store().dependencies().prims_for_stack(delta.stack));
}
cache.drop_index_victims(victims.into_iter().collect())
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::pcp::Diagnostics;
use crate::pcp::layer_stack::{ExprVarId, ExprVarInterner, VarsSource};
use crate::pcp::{LoadRules, PopulationMask, VariantFallbackMap};
use crate::sdf::{ChangeFlags, ChangeList, Value};
fn p(s: &str) -> Path {
Path::new(s).expect("valid path")
}
fn first_layer(graph: &LayerGraph) -> LayerId {
graph.all_ids().first().copied().unwrap_or(LayerId::INVALID)
}
fn empty_cache() -> (LayerGraph, IndexCache) {
let graph = LayerGraph::from_layers(Vec::new(), 0, sdf::LayerRegistry::default());
(
graph,
IndexCache::new(
VariantFallbackMap::new(),
LoadRules::all(),
PopulationMask::all(),
Diagnostics::default(),
),
)
}
fn intern(interner: &mut ExprVarInterner, name: &str, value: &str) -> ExprVarId {
interner.intern(&HashMap::from([(name.to_string(), Value::String(value.to_string()))]))
}
#[test]
fn asset_victims_need_value() {
let (_graph, cache) = empty_cache();
let mut interner = ExprVarInterner::default();
let before = intern(&mut interner, "V", "a");
let after = intern(&mut interner, "V", "b");
assert_ne!(before, after);
let changed = StackVarsDelta {
stack: LayerStackId::ROOT,
old_expr: before,
new_expr: after,
old_source: VarsSource::Root,
new_source: VarsSource::Root,
};
assert_eq!(asset_path_victims(&cache, &[changed]), vec![Path::abs_root()]);
let source_only = StackVarsDelta {
stack: LayerStackId::ROOT,
old_expr: before,
new_expr: before,
old_source: VarsSource::Root,
new_source: VarsSource::Instance(LayerStackId::ROOT),
};
assert!(asset_path_victims(&cache, &[source_only]).is_empty());
}
#[test]
fn references_promotes_to_significant() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&p("/Foo"))
.note(FieldKey::References.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.cache.authored_significant.contains(&p("/Foo")));
}
#[test]
fn variant_selection_promotes_to_significant() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&p("/Foo"))
.note(FieldKey::VariantSelection.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.cache.authored_significant.contains(&p("/Foo")));
}
#[test]
fn variant_edit_stays_authored() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&p("/Foo{set=sel}Bar"))
.note(FieldKey::References.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert_eq!(
changes.cache.authored_significant.iter().collect::<Vec<_>>(),
[&p("/Foo/Bar"), &p("/Foo{set=sel}Bar")]
);
assert!(
changes.cache.did_change_significantly.is_empty(),
"nothing depends on either site, so no stage path is derived"
);
}
#[test]
fn permission_metadata_drops_nothing() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&p("/Foo"))
.note(FieldKey::Permission.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.cache.all_significant().next().is_none());
assert!(changes.cache.did_change_specs.is_empty());
}
#[test]
fn kind_metadata_drops_nothing() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&p("/Foo"))
.note(FieldKey::Kind.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.cache.all_significant().next().is_none());
assert!(changes.cache.did_change_specs.is_empty());
}
#[test]
fn clip_significance_follows_presence() {
let significant_for = |presence: bool| {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
let entry = cl.entry_mut(&p("/X"));
entry.note(FieldKey::Clips.as_str(), sdf::FieldChange::Value);
if presence {
entry.note(FieldKey::Clips.as_str(), sdf::FieldChange::Presence);
}
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
!changes.cache.authored_significant.is_empty()
};
assert!(!significant_for(false), "a value edit invalidates nothing");
assert!(significant_for(true), "the metadata appearing resyncs the subtree");
}
#[test]
fn inert_add_with_instanceable_is_significant() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
let entry = cl.entry_mut(&p("/X"));
entry.flags = ChangeFlags::ADD_INERT_PRIM;
entry.note(FieldKey::Instanceable.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.cache.authored_significant.contains(&p("/X")));
}
#[test]
fn inert_add_lands_on_spec_tier() {
let (graph, cache) = empty_cache();
let layer = first_layer(&graph);
let mut cl = ChangeList::new();
cl.entry_mut(&p("/Foo")).flags = ChangeFlags::ADD_INERT_PRIM;
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(layer, &cl)]);
assert!(!changes.cache.all_significant().any(|path| *path == p("/Foo")));
assert!(changes.cache.did_change_specs.contains(&(layer, p("/Foo"))));
}
#[test]
fn non_inert_add_is_significant_with_self_path() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&p("/Foo")).flags = ChangeFlags::ADD_NON_INERT_PRIM;
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.cache.authored_significant.contains(&p("/Foo")));
}
#[test]
fn type_epoch_tracks_identity() {
for (path, field, advances) in [
("/Foo", FieldKey::TypeName.as_str(), true),
("/Foo", FieldKey::ApiSchemas.as_str(), true),
("/", FieldKey::FallbackPrimTypes.as_str(), true),
("/", FieldKey::StartTimeCode.as_str(), false),
("/Foo", FieldKey::Kind.as_str(), false),
("/Foo.x", FieldKey::Default.as_str(), false),
] {
let (mut graph, mut cache) = empty_cache();
let before = cache.type_opinion_epoch();
let mut cl = ChangeList::new();
cl.entry_mut(&p(path)).note(field, sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
changes.apply(&mut cache, &mut graph);
assert_eq!(
cache.type_opinion_epoch() != before,
advances,
"authoring {field} at {path} must {} the type-opinion epoch",
if advances { "advance" } else { "leave" }
);
}
}
#[test]
fn epoch_tracks_structure_only() {
for (path, flags, advances) in [
("/Foo.attr", ChangeFlags::empty(), false),
("/Foo", ChangeFlags::ADD_NON_INERT_PRIM, true),
] {
let (mut graph, mut cache) = empty_cache();
let before = cache.population_epoch();
let mut cl = ChangeList::new();
cl.entry_mut(&p(path)).flags = flags;
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
changes.apply(&mut cache, &mut graph);
assert_eq!(
cache.population_epoch() != before,
advances,
"a change at {path} must {} the population epoch",
if advances { "advance" } else { "leave" }
);
}
}
#[test]
fn sublayers_change_is_layer_stack_significant() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&Path::abs_root())
.note(FieldKey::SubLayers.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.layer_stack.contains(LayerStackChanges::SIGNIFICANT));
assert!(changes.layer_stack.contains(LayerStackChanges::LAYERS));
}
#[test]
fn default_prim_records_edit() {
let (graph, cache) = empty_cache();
let layer = first_layer(&graph);
let mut cl = ChangeList::new();
cl.entry_mut(&Path::abs_root())
.note(FieldKey::DefaultPrim.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(
&cache,
&graph,
&[LayerChanges {
layer,
changes: &cl,
prior_default_prim: Some(tf::Token::from("Source")),
}],
);
assert_eq!(changes.default_prim_edits, [(layer, Some(tf::Token::from("Source")))]);
assert!(changes.cache.did_change_significantly.is_empty());
assert!(changes.cache.authored_significant.is_empty());
assert!(!changes.layer_stack.contains(LayerStackChanges::SIGNIFICANT));
}
#[test]
fn time_codes_per_second_change_is_significant() {
for field in [FieldKey::TimeCodesPerSecond, FieldKey::FramesPerSecond] {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&Path::abs_root())
.note(field.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.layer_stack.contains(LayerStackChanges::SIGNIFICANT));
}
}
#[test]
fn expression_vars_not_significant() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&Path::abs_root())
.note(FieldKey::ExpressionVariables.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.layer_stack.contains(LayerStackChanges::EXPRESSION_VARS));
assert!(!changes.layer_stack.contains(LayerStackChanges::SIGNIFICANT));
}
#[test]
fn layer_relocates_change_flags_relocates() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&Path::abs_root())
.note(FieldKey::LayerRelocates.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.layer_stack.contains(LayerStackChanges::RELOCATES));
assert!(changes.layer_stack.contains(LayerStackChanges::SIGNIFICANT));
}
#[test]
fn property_changes_no_op() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
cl.entry_mut(&p("/Foo.attr")).flags = ChangeFlags::ADD_PROPERTY;
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
assert!(changes.cache.all_significant().next().is_none());
assert!(changes.cache.did_change_specs.is_empty());
assert!(!changes.layer_stack.contains(LayerStackChanges::SIGNIFICANT));
}
#[test]
fn variant_target_edit_restales_stripped_prim() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
let entry = cl.entry_mut(&p("/P{v=x}Child.r"));
entry.flags = ChangeFlags::CHANGE_RELATIONSHIP_TARGETS;
entry.note(FieldKey::TargetPaths.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
let key = TargetMemoKey {
kind: PropertyTargetKind::Relationship,
property_suffix: ".r".to_owned(),
};
assert!(restaled_keys(&changes, "/P/Child").contains(&key));
assert!(restaled_keys(&changes, "/P{v=x}Child").contains(&key));
}
#[test]
fn property_replace_restales_both_kinds() {
let (graph, cache) = empty_cache();
let mut cl = ChangeList::new();
let entry = cl.entry_mut(&p("/P.x"));
entry.note(FieldKey::TargetPaths.as_str(), sdf::FieldChange::Value);
entry.note(FieldKey::ConnectionPaths.as_str(), sdf::FieldChange::Value);
let mut changes = Changes::new();
changes.did_change(&cache, &graph, &[LayerChanges::plain(first_layer(&graph), &cl)]);
let key = |kind| TargetMemoKey {
kind,
property_suffix: ".x".to_owned(),
};
let restaled = restaled_keys(&changes, "/P");
assert!(restaled.contains(&key(PropertyTargetKind::Relationship)));
assert!(restaled.contains(&key(PropertyTargetKind::Connection)));
}
fn restaled_keys(changes: &Changes, path: &str) -> BTreeSet<TargetMemoKey> {
changes
.cache
.did_change_values
.get(&p(path))
.map(|item| item.target_keys.clone())
.unwrap_or_default()
}
}