1331 lines
48 KiB
Rust
1331 lines
48 KiB
Rust
use super::{creation, scenario, source_draft as source, topology};
|
|
use crate::document::Document;
|
|
use oa_core::source_project::ProjectLimits;
|
|
use oa_core::*;
|
|
|
|
fn id(value: &serde_json::Value, field: &str) -> Id {
|
|
serde_json::from_value(value[field].clone()).unwrap()
|
|
}
|
|
fn child(value: &serde_json::Value, edit: source::Edit) -> source::Event {
|
|
source::Event::Edit(id(&value["source"]["editing"], "token"), edit)
|
|
}
|
|
fn scenario_project(layer: Layer, viewpoint: ScenarioViewpoint) -> (Project, Id) {
|
|
let mut p = Project::empty();
|
|
let owner = p
|
|
.add_element(Kind::Scenario, layer, "Emergency coordination", None)
|
|
.unwrap();
|
|
if let Some(SharedBody::Scenario { viewpoint: v, .. }) =
|
|
p.elements.get_mut(&owner).unwrap().shared_mut()
|
|
{
|
|
*v = viewpoint;
|
|
}
|
|
(p, owner)
|
|
}
|
|
fn scenario_source(s: &mut scenario::State, p: &Project, name: &str, kind: Option<Kind>) {
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
p,
|
|
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
if let Some(kind) = kind {
|
|
s.update(
|
|
p,
|
|
scenario::Event::Source(child(&raw, source::Edit::Kind(kind))),
|
|
)
|
|
.unwrap();
|
|
}
|
|
s.update(
|
|
p,
|
|
scenario::Event::Source(child(&raw, source::Edit::Name(name.into()))),
|
|
)
|
|
.unwrap();
|
|
s.update(p, scenario::Event::Source(child(&raw, source::Edit::Use)))
|
|
.unwrap();
|
|
}
|
|
fn history(p: &Project, command: Command) -> Project {
|
|
let expected = execute_batch(p, std::slice::from_ref(&command)).unwrap();
|
|
let mut doc = Document::from_project(p.clone(), ProjectLimits::default()).unwrap();
|
|
let before = doc.bundle().clone();
|
|
doc.execute(command).unwrap();
|
|
assert_eq!(doc.project(), &expected);
|
|
let after = doc.bundle().clone();
|
|
assert!(doc.undo());
|
|
assert_eq!(doc.project(), p);
|
|
assert_eq!(doc.bundle(), &before);
|
|
assert!(!doc.can_undo());
|
|
assert!(doc.redo());
|
|
assert_eq!(doc.project(), &expected);
|
|
assert_eq!(doc.bundle(), &after);
|
|
assert_eq!(
|
|
Document::from_bundle(after, ProjectLimits::default())
|
|
.unwrap()
|
|
.project(),
|
|
&expected
|
|
);
|
|
expected
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_lifelines_stage_exact_separate_records_and_one_history_entry() {
|
|
for (layer, viewpoint, kind) in [
|
|
(
|
|
Layer::Oa,
|
|
ScenarioViewpoint::OperationalActivity,
|
|
Kind::Activity,
|
|
),
|
|
(Layer::Sa, ScenarioViewpoint::Functional, Kind::Function),
|
|
(
|
|
Layer::Oa,
|
|
ScenarioViewpoint::ParticipantInteraction,
|
|
Kind::Role,
|
|
),
|
|
(
|
|
Layer::Sa,
|
|
ScenarioViewpoint::BoundaryInterface,
|
|
Kind::SystemActor,
|
|
),
|
|
] {
|
|
let (p, owner) = scenario_project(layer, viewpoint);
|
|
let before = p.clone();
|
|
let mut s = scenario::State::default();
|
|
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
s.update(&p, scenario::Event::Name("Assessment role 雨".into()))
|
|
.unwrap();
|
|
scenario_source(&mut s, &p, "Assess distress 雨", Some(kind));
|
|
assert_eq!(p, before);
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
let command = s.prepare_pending(&p).unwrap();
|
|
assert_eq!(command, s.prepare_pending(&p).unwrap());
|
|
let recovered = scenario::State::from_recovery(raw).unwrap();
|
|
assert_eq!(command, recovered.prepare_pending(&p).unwrap());
|
|
let selected = s.created_selection(&p, &command).unwrap();
|
|
let result = history(&p, command);
|
|
assert_eq!(result.elements.len(), p.elements.len() + 2);
|
|
assert_eq!(result.relations, p.relations);
|
|
assert_eq!(result.diagrams, p.diagrams);
|
|
let line = &result.elements[&selected];
|
|
assert_eq!(line.name, "Assessment role 雨");
|
|
assert_eq!(line.parent, Some(owner));
|
|
let Some(SharedBody::Lifeline {
|
|
represented: Some(represented),
|
|
}) = line.shared()
|
|
else {
|
|
panic!("lifeline")
|
|
};
|
|
let work = &result.elements[represented];
|
|
assert_eq!(work.name, "Assess distress 雨");
|
|
assert_eq!(work.kind(), kind);
|
|
assert_eq!(work.parent, None);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_default_consumer_name_has_stable_alias_and_correlated_selection() {
|
|
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut s = scenario::State::default();
|
|
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
scenario_source(&mut s, &p, "Same source and consumer name", None);
|
|
let command = s.prepare_pending(&p).unwrap();
|
|
assert_eq!(command, s.prepare_pending(&p).unwrap());
|
|
let selected = s.created_selection(&p, &command).unwrap();
|
|
let result = execute_batch(&p, std::slice::from_ref(&command)).unwrap();
|
|
assert_eq!(
|
|
result.elements[&selected].name,
|
|
"Same source and consumer name"
|
|
);
|
|
let unrelated = Command::Batch(vec![command, Command::Rename(selected, "Other".into())]);
|
|
assert!(s.created_selection(&p, &unrelated).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_back_retains_parent_and_rejects_late_child_event() {
|
|
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut s = scenario::State::default();
|
|
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
s.update(&p, scenario::Event::Name(" Parent name 雨\n".into()))
|
|
.unwrap();
|
|
let before = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Source(source::Event::Open(id(&before, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let open = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Source(child(&open, source::Edit::Name("discarded".into()))),
|
|
)
|
|
.unwrap();
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Source(child(&open, source::Edit::Back)),
|
|
)
|
|
.unwrap();
|
|
let after = s.recovery_snapshot().unwrap().unwrap();
|
|
for field in ["name", "target", "owner", "parent", "before", "invocation"] {
|
|
assert_eq!(before[field], after[field]);
|
|
}
|
|
assert!(
|
|
s.update(&p, scenario::Event::Source(child(&open, source::Edit::Use)))
|
|
.is_err()
|
|
);
|
|
assert_eq!(s.recovery_snapshot().unwrap().unwrap(), after);
|
|
assert_eq!(p.elements.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_recovery_keeps_inner_raw_fields_and_refuses_deleted_or_changed_owner() {
|
|
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut s = scenario::State::default();
|
|
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let open = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Source(child(&open, source::Edit::Name(" 未完成\n\0".into()))),
|
|
)
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
let mut recovered = scenario::State::from_recovery(raw.clone()).unwrap();
|
|
assert!(recovered.source_open());
|
|
assert_eq!(recovered.recovery_snapshot().unwrap().unwrap(), raw);
|
|
p.elements.remove(&owner);
|
|
assert!(recovered.prepare_pending(&p).is_err());
|
|
assert!(
|
|
recovered
|
|
.update(&p, scenario::Event::Source(child(&raw, source::Edit::Use)))
|
|
.is_err()
|
|
);
|
|
assert_eq!(recovered.recovery_snapshot().unwrap().unwrap(), raw);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_existing_choice_prunes_staged_dependency() {
|
|
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let existing = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Same name", None)
|
|
.unwrap();
|
|
let mut s = scenario::State::default();
|
|
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
scenario_source(&mut s, &p, "Same name", None);
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Target(crate::app::Choice {
|
|
id: existing,
|
|
label: "Same name".into(),
|
|
}),
|
|
)
|
|
.unwrap();
|
|
let result = history(&p, s.prepare_pending(&p).unwrap());
|
|
assert_eq!(result.elements.len(), p.elements.len() + 1);
|
|
let line = result
|
|
.children(owner)
|
|
.find(|e| e.kind() == Kind::Lifeline)
|
|
.unwrap();
|
|
assert!(
|
|
matches!(line.shared(),Some(SharedBody::Lifeline{represented:Some(id)}) if *id==existing)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_machine_before_any_owner_creates_optional_view_atomically() {
|
|
for (layer, kind) in [(Layer::Oa, Kind::Actor), (Layer::Sa, Kind::SystemActor)] {
|
|
let p = Project::empty();
|
|
let mut s = creation::State::default();
|
|
s.update(&p, creation::Event::Open(layer)).unwrap();
|
|
s.update(&p, creation::Event::Choose(creation::Family::Machine))
|
|
.unwrap();
|
|
s.update(&p, creation::Event::Name("Operating modes 雨".into()))
|
|
.unwrap();
|
|
s.update(&p, creation::Event::FirstView(true)).unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
creation::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
creation::Event::Source(child(&raw, source::Edit::Kind(kind))),
|
|
)
|
|
.unwrap();
|
|
s.update(
|
|
&p,
|
|
creation::Event::Source(child(
|
|
&raw,
|
|
source::Edit::Name("Rescue participant 雨".into()),
|
|
)),
|
|
)
|
|
.unwrap();
|
|
s.update(&p, creation::Event::Source(child(&raw, source::Edit::Use)))
|
|
.unwrap();
|
|
let command = s.prepare_pending(&p).unwrap();
|
|
assert_eq!(command, s.prepare_pending(&p).unwrap());
|
|
let target = s.created_by(&p, &command).unwrap();
|
|
assert_eq!(
|
|
creation::State::from_recovery(s.recovery_snapshot().unwrap().unwrap())
|
|
.unwrap()
|
|
.prepare_pending(&p)
|
|
.unwrap(),
|
|
command
|
|
);
|
|
let result = history(&p, command);
|
|
assert_eq!(result.elements.len(), 2);
|
|
assert_eq!(result.diagrams.len(), 1);
|
|
let machine = &result.elements[&target.behavior];
|
|
assert_eq!(machine.kind(), Kind::StateMachine);
|
|
let owner = &result.elements[&machine.parent.unwrap()];
|
|
assert_eq!(owner.kind(), kind);
|
|
assert_eq!(owner.name, "Rescue participant 雨");
|
|
let diagram = &result.diagrams[&target.diagram.unwrap()];
|
|
assert_eq!(
|
|
diagram.projection,
|
|
ProjectionKind::StateMachine {
|
|
machine: target.behavior
|
|
}
|
|
);
|
|
assert_eq!(diagram.nodes.len(), 1);
|
|
assert_eq!(
|
|
diagram.nodes.values().next().unwrap().element,
|
|
target.behavior
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_second_system_and_role_machine_owner_refuse_without_fallback() {
|
|
let mut p = Project::empty();
|
|
p.add_element(Kind::System, Layer::Sa, "Existing system", None)
|
|
.unwrap();
|
|
let mut source = source::State::default();
|
|
let scope = source::Scope::Machine { layer: Layer::Sa };
|
|
let invocation = Id::new_v4();
|
|
source
|
|
.update(&p, scope, invocation, source::Event::Open(invocation))
|
|
.unwrap();
|
|
let raw = serde_json::to_value(&source).unwrap();
|
|
let token = id(&raw["editing"], "token");
|
|
source
|
|
.update(
|
|
&p,
|
|
scope,
|
|
invocation,
|
|
source::Event::Edit(token, source::Edit::Kind(Kind::System)),
|
|
)
|
|
.unwrap();
|
|
source
|
|
.update(
|
|
&p,
|
|
scope,
|
|
invocation,
|
|
source::Event::Edit(token, source::Edit::Name("Illegal second system".into())),
|
|
)
|
|
.unwrap();
|
|
let before = serde_json::to_value(&source).unwrap();
|
|
assert!(
|
|
source
|
|
.update(
|
|
&p,
|
|
scope,
|
|
invocation,
|
|
source::Event::Edit(token, source::Edit::Use)
|
|
)
|
|
.is_err()
|
|
);
|
|
assert_eq!(serde_json::to_value(&source).unwrap(), before);
|
|
assert!(
|
|
source
|
|
.update(
|
|
&p,
|
|
scope,
|
|
invocation,
|
|
source::Event::Edit(token, source::Edit::Kind(Kind::Role))
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_work_uses_reserved_contribution_and_link_ids_and_one_undo() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let mut p = Project::empty();
|
|
let owner = p
|
|
.add_element(
|
|
if layer == Layer::Oa {
|
|
Kind::Process
|
|
} else {
|
|
Kind::FunctionalChain
|
|
},
|
|
layer,
|
|
"Response path",
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let mut s = topology::State::default();
|
|
s.update(
|
|
&p,
|
|
topology::Event::Start(owner, topology::Tool::Work, None),
|
|
)
|
|
.unwrap();
|
|
s.update(&p, topology::Event::Name("New contribution".into()))
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
topology::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
topology::Event::Source(child(&raw, source::Edit::Name("New semantic work".into()))),
|
|
)
|
|
.unwrap();
|
|
s.update(&p, topology::Event::Source(child(&raw, source::Edit::Use)))
|
|
.unwrap();
|
|
let command = s.prepare_pending(&p).unwrap();
|
|
assert_eq!(command, s.prepare_pending(&p).unwrap());
|
|
assert_eq!(
|
|
topology::State::from_recovery(s.recovery_snapshot().unwrap().unwrap())
|
|
.unwrap()
|
|
.prepare_pending(&p)
|
|
.unwrap(),
|
|
command
|
|
);
|
|
let selected = s.created_selection(&p, &command).unwrap();
|
|
let result = history(&p, command);
|
|
assert_eq!(result.elements.len(), 3);
|
|
let occurrence = &result.elements[&selected];
|
|
assert_eq!(occurrence.parent, Some(owner));
|
|
assert_eq!(occurrence.name, "New contribution");
|
|
let Some(SharedBody::Occurrence {
|
|
body:
|
|
OccurrenceBody::Action {
|
|
action: Some(work),
|
|
lifeline: None,
|
|
},
|
|
..
|
|
}) = occurrence.shared()
|
|
else {
|
|
panic!("Action contribution")
|
|
};
|
|
assert_eq!(result.elements[work].name, "New semantic work");
|
|
assert_eq!(result.elements[work].parent, None);
|
|
assert_eq!(result.elements[&owner].steps().unwrap(), &vec![selected]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_failed_source_budget_preserves_both_drafts_and_accepted_bytes() {
|
|
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut s = scenario::State::default();
|
|
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
scenario_source(&mut s, &p, &"Long raw semantic name 雨".repeat(40), None);
|
|
let initial = Document::from_project(p.clone(), ProjectLimits::default()).unwrap();
|
|
let bytes: usize = initial.bundle().files.values().map(String::len).sum();
|
|
let limits = ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_source_bytes: bytes + 16,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
};
|
|
let mut doc = Document::from_bundle(initial.bundle().clone(), limits).unwrap();
|
|
let raw = s.recovery_snapshot().unwrap();
|
|
assert!(doc.execute(s.prepare_pending(&p).unwrap()).is_err());
|
|
assert_eq!(doc.project(), &p);
|
|
assert_eq!(doc.bundle(), initial.bundle());
|
|
assert!(!doc.can_undo());
|
|
assert_eq!(s.recovery_snapshot().unwrap(), raw);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_owner_first_can_return_to_an_unnamed_machine_without_losing_source() {
|
|
let p = Project::empty();
|
|
let mut s = creation::State::default();
|
|
s.update(&p, creation::Event::Open(Layer::Oa)).unwrap();
|
|
s.update(&p, creation::Event::Choose(creation::Family::Machine))
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
creation::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
creation::Event::Source(child(&raw, source::Edit::Kind(Kind::Actor))),
|
|
)
|
|
.unwrap();
|
|
s.update(
|
|
&p,
|
|
creation::Event::Source(child(
|
|
&raw,
|
|
source::Edit::Name("Owner entered first 雨".into()),
|
|
)),
|
|
)
|
|
.unwrap();
|
|
s.update(&p, creation::Event::Source(child(&raw, source::Edit::Use)))
|
|
.unwrap();
|
|
assert!(!s.source_open());
|
|
assert!(s.prepare_pending(&p).is_err());
|
|
let staged = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(&p, creation::Event::Name("Now name the machine".into()))
|
|
.unwrap();
|
|
let result = history(&p, s.prepare_pending(&p).unwrap());
|
|
let owner = id(&staged["source"]["staged"], "id");
|
|
assert_eq!(result.named(owner), "Owner entered first 雨");
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_inserts_work_on_the_exact_guarded_link_and_refuses_exchange_identity_conflict()
|
|
{
|
|
let mut f = topology::direct_tests::fixture(Layer::Sa);
|
|
let start = |p: &Project| {
|
|
let mut s = topology::State::default();
|
|
s.begin(
|
|
p,
|
|
topology::direct_tests::context(f.owner),
|
|
topology::Tool::Work,
|
|
ControlKind::Choice,
|
|
topology::Insertion {
|
|
link: Some(f.link),
|
|
before: Some(f.nodes[1]),
|
|
},
|
|
)
|
|
.unwrap();
|
|
s
|
|
};
|
|
let mut s = start(&f.p);
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Name(" New contribution 雨 ".into()),
|
|
)
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(child(&raw, source::Edit::Name("Inserted function".into()))),
|
|
)
|
|
.unwrap();
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(child(&raw, source::Edit::Use)),
|
|
)
|
|
.unwrap();
|
|
let command = s.prepare_pending(&f.p).unwrap();
|
|
assert_eq!(command, s.prepare_pending(&f.p).unwrap());
|
|
let selected = s.created_selection(&f.p, &command).unwrap();
|
|
let result = history(&f.p, command);
|
|
let link = &result.relations[&f.link];
|
|
assert_eq!(link.source(), f.nodes[0]);
|
|
assert_eq!(link.target(), selected);
|
|
assert!(matches!(&link.body,RelationBody::SequenceLink{guard,..} if guard=="approved"));
|
|
assert_eq!(result.elements[&selected].name, " New contribution 雨 ");
|
|
assert!(
|
|
result
|
|
.relations
|
|
.values()
|
|
.any(|r| r.kind() == RelationKind::SequenceLink
|
|
&& r.source() == selected
|
|
&& r.target() == f.nodes[1])
|
|
);
|
|
for work in f.work {
|
|
assert_eq!(result.elements[&work], f.p.elements[&work]);
|
|
}
|
|
// The preceding exchange fixes its target work identity. A new source cannot match it.
|
|
if let Some(SharedBody::Occurrence { body, .. }) =
|
|
f.p.elements.get_mut(&f.nodes[0]).unwrap().shared_mut()
|
|
{
|
|
*body = OccurrenceBody::Exchange {
|
|
exchange: Some(f.exchange),
|
|
sender: None,
|
|
receiver: None,
|
|
message_kind: MessageKind::Asynchronous,
|
|
reply_to: None,
|
|
values: vec![],
|
|
operation: None,
|
|
};
|
|
}
|
|
let mut s = start(&f.p);
|
|
s.update(&f.p, topology::Event::Name("Retain this slot".into()))
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(child(
|
|
&raw,
|
|
source::Edit::Name("Cannot replace exchange endpoint".into()),
|
|
)),
|
|
)
|
|
.unwrap();
|
|
let before = s.recovery_snapshot().unwrap();
|
|
assert!(
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(child(&raw, source::Edit::Use))
|
|
)
|
|
.is_err()
|
|
);
|
|
assert_eq!(s.recovery_snapshot().unwrap(), before);
|
|
assert!(s.source_open());
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_app_empty_actions_and_child_open_refuse_other_raw_editors() {
|
|
use crate::app::{App, Message, Page};
|
|
for property in [true, false] {
|
|
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut app = App::with_project(p);
|
|
app.page = Page::Behavior;
|
|
if property {
|
|
app.draft
|
|
.as_mut()
|
|
.unwrap()
|
|
.input("/name", "Valid unapplied name".into());
|
|
} else {
|
|
app.source_workspace.edit(
|
|
format!("{}\n// retained draft\n", app.source_workspace.text()),
|
|
app.source_editor.cursor(),
|
|
);
|
|
}
|
|
let project = app.project().clone();
|
|
let bundle = app.document.bundle().clone();
|
|
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
|
let workspace = serde_json::to_value(app.source_workspace.recovery_snapshot()).unwrap();
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Start(owner, scenario::Tool::Lifeline),
|
|
)));
|
|
assert!(!app.behavior.has_pending());
|
|
assert_eq!(app.project(), &project);
|
|
assert_eq!(app.document.bundle(), &bundle);
|
|
assert_eq!(
|
|
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
|
raw
|
|
);
|
|
assert_eq!(
|
|
serde_json::to_value(app.source_workspace.recovery_snapshot()).unwrap(),
|
|
workspace
|
|
);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_app_modal_escape_then_child_back_then_parent_cancel_preserve_accepted_source()
|
|
{
|
|
use crate::app::{App, Message, Page, Pending};
|
|
for file in [false, true] {
|
|
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut app = App::with_project(p.clone());
|
|
app.page = Page::Behavior;
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Start(owner, scenario::Tool::Lifeline),
|
|
)));
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Name("Keep parent name 雨".into()),
|
|
)));
|
|
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)));
|
|
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Source(child(
|
|
&raw,
|
|
source::Edit::Name("Uncommitted source 雨".into()),
|
|
)),
|
|
)));
|
|
let before = app.behavior.recovery_snapshot().unwrap();
|
|
let bundle = app.document.bundle().clone();
|
|
if file {
|
|
app.file_prompt = Some(crate::app::FilePrompt {
|
|
operation: crate::app::FileOperation::Save,
|
|
path: "Unsubmitted.arcadia".into(),
|
|
overwrite: false,
|
|
});
|
|
} else {
|
|
app.pending = Some(Pending::Walkthrough);
|
|
}
|
|
let _ = app.update(Message::Escape);
|
|
assert_eq!(app.behavior.recovery_snapshot().unwrap(), before);
|
|
assert!(app.file_prompt.is_none());
|
|
assert!(app.pending.is_none());
|
|
let _ = app.update(Message::Escape);
|
|
assert!(!app.behavior.source_open());
|
|
assert!(app.behavior.has_pending());
|
|
assert_eq!(
|
|
app.behavior.scenario.recovery_snapshot().unwrap().unwrap()["name"],
|
|
"Keep parent name 雨"
|
|
);
|
|
let _ = app.update(Message::Escape);
|
|
assert!(!app.behavior.has_pending());
|
|
assert_eq!(app.project(), &p);
|
|
assert_eq!(app.document.bundle(), &bundle);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_app_checkpoint_restores_inner_raw_draft_without_model_adoption() {
|
|
use crate::app::{App, Message, Page};
|
|
use std::sync::Arc;
|
|
let (p, owner) = scenario_project(Layer::Sa, ScenarioViewpoint::Functional);
|
|
let mut app = App::with_project(p.clone());
|
|
app.page = Page::Behavior;
|
|
app.layer = Layer::Sa;
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Start(owner, scenario::Tool::Lifeline),
|
|
)));
|
|
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)));
|
|
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Source(child(&raw, source::Edit::Name(" exact 未完成\n".into()))),
|
|
)));
|
|
let raw = app.behavior.recovery_snapshot().unwrap();
|
|
let snapshot = crate::recovery::Snapshot::capture(
|
|
Id::new_v4(),
|
|
&app.document,
|
|
&app.source_workspace,
|
|
app.draft.as_ref(),
|
|
&app.behavior,
|
|
None,
|
|
owner,
|
|
)
|
|
.unwrap();
|
|
let mut recovered = App::with_project(Project::empty());
|
|
let task = recovered.update(Message::CheckpointLoaded(Ok(Arc::new(snapshot))));
|
|
assert_eq!(recovered.project(), &p);
|
|
assert_eq!(recovered.layer, Layer::Sa);
|
|
assert_eq!(recovered.page, Page::Behavior);
|
|
assert!(recovered.behavior.source_open());
|
|
assert_eq!(recovered.behavior.recovery_snapshot().unwrap(), raw);
|
|
assert!(!recovered.document.can_undo());
|
|
assert!(
|
|
iced_runtime::task::into_stream(task).is_some(),
|
|
"Recovery preserves its lint/focus task"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_mounted_small_editor_keeps_back_and_use_outside_field_scroll() {
|
|
use iced::advanced::{Layout, layout, widget};
|
|
use iced::{Rectangle, Size, Vector};
|
|
struct Geometry {
|
|
actions: Option<Rectangle>,
|
|
fields: Option<Rectangle>,
|
|
}
|
|
impl widget::Operation for Geometry {
|
|
fn traverse(&mut self, visit: &mut dyn FnMut(&mut dyn widget::Operation)) {
|
|
visit(self);
|
|
}
|
|
fn container(&mut self, id: Option<&widget::Id>, bounds: Rectangle) {
|
|
if id == Some(&widget::Id::new("behavior-source-actions")) {
|
|
self.actions = Some(bounds);
|
|
}
|
|
}
|
|
fn scrollable(
|
|
&mut self,
|
|
id: Option<&widget::Id>,
|
|
bounds: Rectangle,
|
|
_: Rectangle,
|
|
_: Vector,
|
|
_: &mut dyn widget::operation::Scrollable,
|
|
) {
|
|
if id == Some(&widget::Id::new("behavior-source-fields")) {
|
|
self.fields = Some(bounds);
|
|
}
|
|
}
|
|
}
|
|
let p = Project::empty();
|
|
let scope = source::Scope::Machine { layer: Layer::Oa };
|
|
let invocation = Id::new_v4();
|
|
let mut state = source::State::default();
|
|
state
|
|
.update(&p, scope, invocation, source::Event::Open(invocation))
|
|
.unwrap();
|
|
for size in [Size::new(340., 320.), Size::new(500., 570.)] {
|
|
let mut view: iced::Element<'_, source::Event, iced::Theme, ()> = state.view(&p).unwrap();
|
|
let mut tree = widget::Tree::new(view.as_widget());
|
|
let node =
|
|
view.as_widget_mut()
|
|
.layout(&mut tree, &(), &layout::Limits::new(Size::ZERO, size));
|
|
let mut geometry = Geometry {
|
|
actions: None,
|
|
fields: None,
|
|
};
|
|
view.as_widget_mut()
|
|
.operate(&mut tree, Layout::new(&node), &(), &mut geometry);
|
|
let actions = geometry.actions.unwrap();
|
|
let fields = geometry.fields.unwrap();
|
|
assert!(
|
|
actions.x >= 0.
|
|
&& actions.y >= 0.
|
|
&& actions.x + actions.width <= size.width
|
|
&& actions.y + actions.height <= size.height
|
|
);
|
|
assert!(fields.height >= 100. && fields.y + fields.height <= actions.y);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_back_remains_available_after_owner_deletion_and_with_dirty_editors() {
|
|
use crate::app::{App, Message, Page};
|
|
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut state = scenario::State::default();
|
|
state
|
|
.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
state
|
|
.update(&p, scenario::Event::Name("Retain parent".into()))
|
|
.unwrap();
|
|
let raw = state.recovery_snapshot().unwrap().unwrap();
|
|
state
|
|
.update(
|
|
&p,
|
|
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let open = state.recovery_snapshot().unwrap().unwrap();
|
|
p.elements.remove(&owner);
|
|
state
|
|
.update(
|
|
&p,
|
|
scenario::Event::Source(child(&open, source::Edit::Back)),
|
|
)
|
|
.unwrap();
|
|
assert!(!state.source_open());
|
|
assert_eq!(
|
|
state.recovery_snapshot().unwrap().unwrap()["name"],
|
|
"Retain parent"
|
|
);
|
|
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut app = App::with_project(p.clone());
|
|
app.page = Page::Behavior;
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Start(owner, scenario::Tool::Lifeline),
|
|
)));
|
|
let raw = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)));
|
|
let open = app.behavior.scenario.recovery_snapshot().unwrap().unwrap();
|
|
app.draft
|
|
.as_mut()
|
|
.unwrap()
|
|
.input("/name", "Other raw input".into());
|
|
let property = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
|
|
let _ = app.update(Message::Behavior(super::Event::Scenario(
|
|
scenario::Event::Source(child(&open, source::Edit::Back)),
|
|
)));
|
|
assert!(!app.behavior.source_open());
|
|
assert!(app.behavior.has_pending());
|
|
assert_eq!(
|
|
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
|
|
property
|
|
);
|
|
assert_eq!(app.project(), &p);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_incomplete_recovered_kind_reports_error_without_panicking() {
|
|
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut s = scenario::State::default();
|
|
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
scenario_source(&mut s, &p, "Recovered source", None);
|
|
let mut raw = s.recovery_snapshot().unwrap().unwrap();
|
|
let mut wrong_scope = raw.clone();
|
|
wrong_scope["source"]["staged"]["scope"] =
|
|
serde_json::to_value(source::Scope::Machine { layer: Layer::Oa }).unwrap();
|
|
assert!(
|
|
scenario::State::from_recovery(wrong_scope)
|
|
.unwrap()
|
|
.prepare_pending(&p)
|
|
.unwrap_err()
|
|
.contains("different consumer context")
|
|
);
|
|
raw["source"]["staged"]["kind"] = serde_json::Value::Null;
|
|
let recovered = scenario::State::from_recovery(raw.clone()).unwrap();
|
|
assert!(recovered.prepare_pending(&p).is_err());
|
|
let selected = serde_json::from_value(raw["target"].clone()).unwrap();
|
|
let nested: source::State = serde_json::from_value(raw["source"].clone()).unwrap();
|
|
assert!(nested.label(selected).unwrap().contains("Choose kind"));
|
|
assert_eq!(recovered.recovery_snapshot().unwrap().unwrap(), raw);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_empty_palette_lifeline_cannot_open_a_previous_owner_or_layer() {
|
|
use crate::app::{App, Message, Page};
|
|
let (mut p, a) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let b = p
|
|
.add_element(Kind::Scenario, Layer::Oa, "Second scenario", None)
|
|
.unwrap();
|
|
let mut app = App::with_project(p.clone());
|
|
app.page = Page::Behavior;
|
|
app.layer = Layer::Oa;
|
|
app.behavior.active_owner = Some(a);
|
|
app.selected = a;
|
|
let context = app.scenario_context().unwrap();
|
|
let source = app.document.bundle().clone();
|
|
let begin = |context| scenario::CanvasEvent::BeginOccurrence {
|
|
context,
|
|
tool: scenario::Tool::Lifeline,
|
|
slot: scenario::Slot {
|
|
parent: a,
|
|
before: None,
|
|
},
|
|
lifeline: None,
|
|
};
|
|
app.behavior.active_owner = Some(b);
|
|
app.selected = b;
|
|
let _ = app.update(Message::ScenarioCanvas(begin(context)));
|
|
assert!(!app.behavior.has_pending());
|
|
assert_eq!(app.project(), &p);
|
|
app.behavior.active_owner = Some(a);
|
|
app.selected = a;
|
|
app.layer = Layer::Sa;
|
|
let _ = app.update(Message::ScenarioCanvas(begin(context)));
|
|
assert!(!app.behavior.has_pending());
|
|
app.layer = Layer::Oa;
|
|
let current = app.scenario_context().unwrap();
|
|
let _ = app.update(Message::ScenarioCanvas(begin(current)));
|
|
assert_eq!(app.behavior.pending_owner(), Some(a));
|
|
assert_eq!(app.project(), &p);
|
|
assert_eq!(app.document.bundle(), &source);
|
|
assert!(!app.document.can_undo());
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_deleted_owner_view_retains_draft_instead_of_panicking_or_using_fallback() {
|
|
use crate::app::{App, Page};
|
|
for fallback in [false, true] {
|
|
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
if fallback {
|
|
p.add_element(
|
|
Kind::Scenario,
|
|
Layer::Oa,
|
|
"Different remaining scenario",
|
|
None,
|
|
)
|
|
.unwrap();
|
|
}
|
|
let mut s = scenario::State::default();
|
|
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
s.update(&p, scenario::Event::Name("Recovered consumer".into()))
|
|
.unwrap();
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let open = s.recovery_snapshot().unwrap().unwrap();
|
|
p.elements.remove(&owner);
|
|
let mut app = App::with_project(p.clone());
|
|
app.page = Page::Behavior;
|
|
app.layer = Layer::Oa;
|
|
app.behavior.scenario = scenario::State::from_recovery(open.clone()).unwrap();
|
|
drop(super::view(&app));
|
|
assert!(app.behavior.source_open());
|
|
assert_eq!(
|
|
app.behavior.scenario.recovery_snapshot().unwrap().unwrap(),
|
|
open
|
|
);
|
|
assert_eq!(app.project(), &p);
|
|
assert!(app.behavior.source_back());
|
|
drop(super::view(&app));
|
|
assert!(app.behavior.has_pending());
|
|
assert_eq!(
|
|
app.behavior.scenario.recovery_snapshot().unwrap().unwrap()["name"],
|
|
"Recovered consumer"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_nested_semantic_parent_is_explicit_and_revalidated() {
|
|
let (mut p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let parent = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Parent activity", None)
|
|
.unwrap();
|
|
let wrong = p
|
|
.add_element(
|
|
Kind::Entity,
|
|
Layer::Oa,
|
|
"Participant is not work ownership",
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let scope = source::Scope::Lifeline {
|
|
owner,
|
|
viewpoint: ScenarioViewpoint::OperationalActivity,
|
|
};
|
|
let invocation = Id::new_v4();
|
|
let mut state = source::State::default();
|
|
state
|
|
.update(&p, scope, invocation, source::Event::Open(invocation))
|
|
.unwrap();
|
|
let raw = serde_json::to_value(&state).unwrap();
|
|
let token = id(&raw["editing"], "token");
|
|
assert!(
|
|
state
|
|
.update(
|
|
&p,
|
|
scope,
|
|
invocation,
|
|
source::Event::Edit(token, source::Edit::Parent(Some(wrong)))
|
|
)
|
|
.is_err()
|
|
);
|
|
state
|
|
.update(
|
|
&p,
|
|
scope,
|
|
invocation,
|
|
source::Event::Edit(token, source::Edit::Parent(Some(parent))),
|
|
)
|
|
.unwrap();
|
|
state
|
|
.update(
|
|
&p,
|
|
scope,
|
|
invocation,
|
|
source::Event::Edit(token, source::Edit::Name("Explicit child work".into())),
|
|
)
|
|
.unwrap();
|
|
let new = state
|
|
.update(
|
|
&p,
|
|
scope,
|
|
invocation,
|
|
source::Event::Edit(token, source::Edit::Use),
|
|
)
|
|
.unwrap()
|
|
.unwrap();
|
|
let preview = state.preview(&p, Some(new)).unwrap().unwrap();
|
|
assert_eq!(preview.elements[&new].parent, Some(parent));
|
|
assert_eq!(preview.elements[&new].kind(), Kind::Activity);
|
|
assert_eq!(p.elements.len() + 1, preview.elements.len());
|
|
let raw = serde_json::to_value(&state).unwrap();
|
|
p.elements.get_mut(&parent).unwrap().body = ElementBody::new(Kind::Entity, Layer::Oa).unwrap();
|
|
assert!(state.preview(&p, Some(new)).is_err());
|
|
assert_eq!(serde_json::to_value(&state).unwrap(), raw);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_new_callee_lifeline_preserves_caller_bindings_as_an_explicit_warning() {
|
|
let (mut p, callee) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let caller = p
|
|
.add_element(Kind::Scenario, Layer::Oa, "Caller", None)
|
|
.unwrap();
|
|
let call = topology::occurrence(
|
|
&p,
|
|
caller,
|
|
"Use callee",
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(callee),
|
|
bindings: vec![],
|
|
},
|
|
);
|
|
let call_id = call.id;
|
|
p = execute_batch(
|
|
&p,
|
|
&[
|
|
Command::CreateElement(call),
|
|
Command::SetSteps(caller, vec![call_id]),
|
|
],
|
|
)
|
|
.unwrap();
|
|
let mut state = scenario::State::default();
|
|
state
|
|
.update(&p, scenario::Event::Start(callee, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
scenario_source(&mut state, &p, "New represented activity", None);
|
|
let result = history(&p, state.prepare_pending(&p).unwrap());
|
|
for (id, element) in &p.elements {
|
|
assert_eq!(result.elements[id], *element);
|
|
}
|
|
assert!(
|
|
validation::validate(&result)
|
|
.iter()
|
|
.any(|issue| issue.element == Some(call_id)
|
|
&& issue.code == "missing_lifeline_binding"
|
|
&& issue.severity == validation::Severity::Warning)
|
|
);
|
|
assert_eq!(result.relations, p.relations);
|
|
assert_eq!(result.diagrams, p.diagrams);
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_empty_history_reports_no_change_and_success_requires_a_real_history_step() {
|
|
use crate::app::{App, Message, Page};
|
|
use std::sync::Arc;
|
|
let (p, owner) = scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let mut app = App::with_project(p.clone());
|
|
app.page = Page::Behavior;
|
|
app.selected = owner;
|
|
app.behavior.active_owner = Some(owner);
|
|
let before = app.document.snapshot();
|
|
let bundle = app.document.bundle().clone();
|
|
for (message, status) in [
|
|
(Message::Undo, "Nothing to undo"),
|
|
(Message::Redo, "Nothing to redo"),
|
|
] {
|
|
let _ = app.update(message);
|
|
assert_eq!(app.status, status);
|
|
assert!(Arc::ptr_eq(&before, &app.document.snapshot()));
|
|
assert_eq!(app.document.bundle(), &bundle);
|
|
assert_eq!(app.selected, owner);
|
|
assert_eq!(app.behavior.active_owner, Some(owner));
|
|
}
|
|
let _ = app.update(Message::Command(Command::Rename(
|
|
owner,
|
|
"Accepted renamed scenario".into(),
|
|
)));
|
|
assert_eq!(app.project().named(owner), "Accepted renamed scenario");
|
|
let _ = app.update(Message::Undo);
|
|
assert_eq!(app.status, "Model change undone");
|
|
assert_eq!(app.project(), &p);
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.status, "Model change redone");
|
|
assert_eq!(app.project().named(owner), "Accepted renamed scenario");
|
|
let _ = app.update(Message::Redo);
|
|
assert_eq!(app.status, "Nothing to redo");
|
|
}
|
|
|
|
#[test]
|
|
fn contextual_source_original_document_remains_bound_after_back_or_choosing_existing() {
|
|
for legacy in [false, true] {
|
|
for inactive in [false, true] {
|
|
let (mut p, owner) =
|
|
scenario_project(Layer::Oa, ScenarioViewpoint::OperationalActivity);
|
|
let existing = p
|
|
.add_element(Kind::Activity, Layer::Oa, "Existing activity", None)
|
|
.unwrap();
|
|
let unrelated = p
|
|
.add_element(Kind::Scenario, Layer::Oa, "Another valid scenario", None)
|
|
.unwrap();
|
|
let choice = || crate::app::Choice {
|
|
id: existing,
|
|
label: "Existing activity".into(),
|
|
};
|
|
let mut s = scenario::State::default();
|
|
s.update(&p, scenario::Event::Start(owner, scenario::Tool::Lifeline))
|
|
.unwrap();
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Name("Retained valid lifeline 雨".into()),
|
|
)
|
|
.unwrap();
|
|
s.update(&p, scenario::Event::Target(choice())).unwrap();
|
|
if legacy {
|
|
let mut raw = s.recovery_snapshot().unwrap().unwrap();
|
|
raw.as_object_mut().unwrap().remove("source_origin");
|
|
s = scenario::State::from_recovery(raw).unwrap();
|
|
}
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let open = s.recovery_snapshot().unwrap().unwrap();
|
|
assert_eq!(
|
|
open["source_origin"][0],
|
|
serde_json::to_value(p.id).unwrap()
|
|
);
|
|
if inactive {
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Source(child(
|
|
&open,
|
|
source::Edit::Name("Unused staged activity".into()),
|
|
)),
|
|
)
|
|
.unwrap();
|
|
s.update(&p, scenario::Event::Source(child(&open, source::Edit::Use)))
|
|
.unwrap();
|
|
s.update(&p, scenario::Event::Target(choice())).unwrap();
|
|
} else {
|
|
s.update(
|
|
&p,
|
|
scenario::Event::Source(child(&open, source::Edit::Back)),
|
|
)
|
|
.unwrap();
|
|
}
|
|
assert!(
|
|
s.prepare_pending(&p).is_ok(),
|
|
"Control must have a valid existing target"
|
|
);
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
let mut other = p.clone();
|
|
other.id = Id::new_v4();
|
|
assert!(
|
|
s.prepare_pending(&other)
|
|
.unwrap_err()
|
|
.contains("original consumer document")
|
|
);
|
|
assert_eq!(s.recovery_snapshot().unwrap().unwrap(), raw);
|
|
let mut borrowed = raw.clone();
|
|
borrowed["source_origin"][1] = serde_json::to_value(&p.elements[&unrelated]).unwrap();
|
|
let mut wrong = scenario::State::from_recovery(borrowed.clone()).unwrap();
|
|
assert!(
|
|
wrong
|
|
.prepare_pending(&p)
|
|
.unwrap_err()
|
|
.contains("original consumer")
|
|
);
|
|
assert!(
|
|
wrong
|
|
.update(
|
|
&p,
|
|
scenario::Event::Source(source::Event::Open(id(&borrowed, "invocation")))
|
|
)
|
|
.is_err()
|
|
);
|
|
assert_eq!(wrong.recovery_snapshot().unwrap().unwrap(), borrowed);
|
|
let mut malformed = raw.clone();
|
|
malformed["source_origin"] = serde_json::Value::Null;
|
|
assert!(
|
|
scenario::State::from_recovery(malformed)
|
|
.unwrap()
|
|
.prepare_pending(&p)
|
|
.is_err()
|
|
);
|
|
|
|
let mut f = topology::direct_tests::fixture(Layer::Sa);
|
|
let unrelated =
|
|
f.p.add_element(
|
|
Kind::FunctionalChain,
|
|
Layer::Sa,
|
|
"Another valid chain",
|
|
None,
|
|
)
|
|
.unwrap();
|
|
let choice = || crate::app::Choice {
|
|
id: f.work[0],
|
|
label: "Existing function".into(),
|
|
};
|
|
let mut s = topology::State::default();
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Start(f.owner, topology::Tool::Work, None),
|
|
)
|
|
.unwrap();
|
|
s.update(&f.p, topology::Event::Target(choice())).unwrap();
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Name("Retained valid contribution 雨".into()),
|
|
)
|
|
.unwrap();
|
|
if legacy {
|
|
let mut raw = s.recovery_snapshot().unwrap().unwrap();
|
|
raw.as_object_mut().unwrap().remove("source_origin");
|
|
s = topology::State::from_recovery(raw).unwrap();
|
|
}
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(source::Event::Open(id(&raw, "invocation"))),
|
|
)
|
|
.unwrap();
|
|
let open = s.recovery_snapshot().unwrap().unwrap();
|
|
assert_eq!(
|
|
open["source_origin"][0],
|
|
serde_json::to_value(f.p.id).unwrap()
|
|
);
|
|
if inactive {
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(child(
|
|
&open,
|
|
source::Edit::Name("Unused staged function".into()),
|
|
)),
|
|
)
|
|
.unwrap();
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(child(&open, source::Edit::Use)),
|
|
)
|
|
.unwrap();
|
|
s.update(&f.p, topology::Event::Target(choice())).unwrap();
|
|
} else {
|
|
s.update(
|
|
&f.p,
|
|
topology::Event::Source(child(&open, source::Edit::Back)),
|
|
)
|
|
.unwrap();
|
|
}
|
|
assert!(s.prepare_pending(&f.p).is_ok());
|
|
let raw = s.recovery_snapshot().unwrap().unwrap();
|
|
let mut other = f.p.clone();
|
|
other.id = Id::new_v4();
|
|
assert!(
|
|
s.prepare_pending(&other)
|
|
.unwrap_err()
|
|
.contains("original consumer document")
|
|
);
|
|
assert_eq!(s.recovery_snapshot().unwrap().unwrap(), raw);
|
|
let mut borrowed = raw.clone();
|
|
borrowed["source_origin"][1] = serde_json::to_value(&f.p.elements[&unrelated]).unwrap();
|
|
let mut wrong = topology::State::from_recovery(borrowed.clone()).unwrap();
|
|
assert!(
|
|
wrong
|
|
.prepare_pending(&f.p)
|
|
.unwrap_err()
|
|
.contains("original consumer")
|
|
);
|
|
assert!(
|
|
wrong
|
|
.update(
|
|
&f.p,
|
|
topology::Event::Source(source::Event::Open(id(&borrowed, "invocation")))
|
|
)
|
|
.is_err()
|
|
);
|
|
assert_eq!(wrong.recovery_snapshot().unwrap().unwrap(), borrowed);
|
|
let mut malformed = raw.clone();
|
|
malformed["source_origin"] = serde_json::Value::Null;
|
|
assert!(
|
|
topology::State::from_recovery(malformed)
|
|
.unwrap()
|
|
.prepare_pending(&f.p)
|
|
.is_err()
|
|
);
|
|
}
|
|
}
|
|
}
|