use oa_core::*; use std::collections::{BTreeMap, BTreeSet}; fn pair() -> (Project, Id, Id) { let mut project = Project::empty(); let first = project .add_element(Kind::Activity, Layer::Oa, "First", None) .unwrap(); let second = project .add_element(Kind::Activity, Layer::Oa, "Second", None) .unwrap(); (project, first, second) } fn identities(project: &Project) -> BTreeSet { project.identities().into_iter().map(|(id, _)| id).collect() } fn valid_map(original: &Project, prepared: &PreparedCommand) { let origins = identities(original); let present = identities(&prepared.project); let targets: BTreeSet<_> = prepared.identity_map.values().copied().collect(); assert_eq!( targets.len(), prepared.identity_map.len(), "correspondence is injective" ); assert!(targets.is_subset(&present)); assert!(prepared.identity_map.keys().all(|id| origins.contains(id))); } #[test] fn explicit_chains_and_nested_swaps_compose_original_identity() { let (project, first, second) = pair(); let temporary = Id::new_v4(); let final_id = Id::new_v4(); let chained = prepare_command( &project, &Command::Batch(vec![ Command::Reidentify(first, temporary), Command::Batch(vec![Command::Reidentify(temporary, final_id)]), ]), ) .unwrap(); assert_eq!( chained.identity_map, BTreeMap::from([ (project.id, project.id), (first, final_id), (second, second) ]) ); assert_eq!(chained.selected, Some(final_id)); valid_map(&project, &chained); let swapped = prepare_command( &project, &Command::Batch(vec![ Command::Reidentify(first, temporary), Command::Batch(vec![ Command::Reidentify(second, first), Command::Reidentify(temporary, second), ]), ]), ) .unwrap(); assert_eq!( swapped.identity_map, BTreeMap::from([(project.id, project.id), (first, second), (second, first)]) ); assert_eq!(swapped.project.elements[&second].alias, "first"); assert_eq!(swapped.project.elements[&first].alias, "second"); valid_map(&project, &swapped); } #[test] fn all_edit_commands_record_their_implicit_identity_change() { let project = Project::demo(); let new = Id::new_v4(); let mut element = project.elements.values().next().unwrap().clone(); let element_id = element.id; element.id = new; element.alias = "edited_identity_and_alias".into(); let mut relation = project.relations.values().next().unwrap().clone(); let relation_id = relation.id; relation.id = new; let mut diagram = project.diagrams.values().next().unwrap().clone(); let diagram_id = diagram.id; let mut node = diagram.nodes.values().next().unwrap().clone(); let node_id = node.id; node.id = new; diagram.id = new; let mut artifact = project.artifact_links.values().next().unwrap().clone(); let artifact_id = artifact.id; artifact.id = new; let mut trace = project.traces.values().next().unwrap().clone(); let trace_id = trace.id; trace.id = new; let cases = [ (element_id, Command::EditElement(element_id, element)), (relation_id, Command::EditRelation(relation_id, relation)), (diagram_id, Command::EditDiagram(diagram_id, diagram)), ( node_id, Command::EditNode { diagram: diagram_id, original: node_id, node, }, ), (artifact_id, Command::EditArtifact(artifact_id, artifact)), (trace_id, Command::EditTrace(trace_id, trace)), ( project.id, Command::EditProject { id: new, alias: project.alias.clone(), name: project.name.clone(), description: project.description.clone(), }, ), ]; for (old, command) in cases { let prepared = prepare_command(&project, &command).unwrap(); let expected: BTreeMap<_, _> = identities(&project) .into_iter() .map(|id| (id, if id == old { new } else { id })) .collect(); assert_eq!(prepared.identity_map, expected, "{command:?}"); valid_map(&project, &prepared); } } #[test] fn deletion_and_cascade_discard_origins_before_uuid_reuse() { let (mut project, first, second) = pair(); let child = project .add_element(Kind::Activity, Layer::Oa, "Nested work", Some(first)) .unwrap(); let mut fresh = project.elements[&child].clone(); fresh.parent = None; fresh.alias = "replacement_work".into(); let prepared = prepare_command( &project, &Command::Batch(vec![Command::Delete(first), Command::CreateElement(fresh)]), ) .unwrap(); assert!(prepared.project.contains(child)); assert_eq!( prepared.identity_map, BTreeMap::from([(project.id, project.id), (second, second)]) ); valid_map(&project, &prepared); } #[test] fn embedded_diagram_replacement_cannot_revive_deleted_node_origin() { let mut project = Project::empty(); let element = project.add(Kind::Activity, "Work", 0., 0.); let mut diagram = project.diagrams.values().next().unwrap().clone(); let node = diagram.nodes.values().next().unwrap().clone(); diagram.nodes.clear(); let prepared = prepare_command( &project, &Command::Batch(vec![ Command::EditDiagram(diagram.id, diagram.clone()), Command::CreateNode { diagram: diagram.id, node: node.clone(), }, ]), ) .unwrap(); assert_eq!(prepared.project, project); assert_eq!( prepared.identity_map, BTreeMap::from([ (project.id, project.id), (element, element), (diagram.id, diagram.id) ]) ); assert!(!prepared.identity_map.contains_key(&node.id)); valid_map(&project, &prepared); } #[test] fn fresh_creation_has_no_origin_even_when_subsequently_reidentified() { let (project, _, _) = pair(); let mut fresh = project .make_element(Kind::Activity, Layer::Oa, "New work", None) .unwrap(); let created = fresh.id; let renamed = Id::new_v4(); fresh.alias = "fresh_identity".into(); let prepared = prepare_command( &project, &Command::Batch(vec![ Command::CreateElement(fresh), Command::Reidentify(created, renamed), ]), ) .unwrap(); assert_eq!(prepared.selected, Some(renamed)); assert!(prepared.project.contains(renamed)); assert_eq!( prepared.identity_map, identities(&project) .into_iter() .map(|id| (id, id)) .collect() ); valid_map(&project, &prepared); let generated = prepare_command( &project, &Command::AddElement { kind: Kind::Activity, layer: Layer::Oa, name: "Generated once".into(), parent: None, }, ) .unwrap(); let selected = generated.selected.unwrap(); assert!(generated.project.contains(selected)); assert!(!generated.identity_map.values().any(|id| *id == selected)); assert_eq!(generated.project.elements.len(), project.elements.len() + 1); } #[test] fn replacement_resets_correspondence_against_the_original_input() { let (project, first, second) = pair(); let temporary = Id::new_v4(); let mut replacement = project.clone(); replacement.elements.remove(&second); let new = replacement .add_element(Kind::Activity, Layer::Oa, "Replacement addition", None) .unwrap(); let final_id = Id::new_v4(); let prepared = prepare_command( &project, &Command::Batch(vec![ Command::Reidentify(first, temporary), Command::Delete(second), Command::Replace(replacement), Command::Reidentify(first, final_id), ]), ) .unwrap(); assert_eq!( prepared.identity_map, BTreeMap::from([(project.id, project.id), (first, final_id)]) ); assert!(!prepared.identity_map.values().any(|id| *id == new)); valid_map(&project, &prepared); let restored = prepare_command( &project, &Command::Batch(vec![ Command::Delete(first), Command::Replace(project.clone()), ]), ) .unwrap(); assert_eq!( restored.identity_map, identities(&project) .into_iter() .map(|id| (id, id)) .collect() ); } #[test] fn failed_preparation_leaves_input_unchanged_after_intermediate_identity_work() { let (project, first, second) = pair(); let before = project.clone(); let temporary = Id::new_v4(); let failed = prepare_command( &project, &Command::Batch(vec![ Command::Reidentify(first, temporary), Command::Reidentify(second, temporary), ]), ); assert!(failed.is_err()); assert_eq!(project, before); let failed_validation = prepare_command( &project, &Command::Batch(vec![ Command::Reidentify(first, temporary), Command::SetAlias(temporary, "second".into()), ]), ); assert!(failed_validation.is_err()); assert_eq!(project, before); }