277 lines
9.1 KiB
Rust
277 lines
9.1 KiB
Rust
use super::*;
|
|
|
|
fn add_occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id {
|
|
let layer = p.elements[&owner].layer();
|
|
let id = p
|
|
.add_element(Kind::Occurrence, layer, name, Some(owner))
|
|
.unwrap();
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
if let Some(steps) = p.elements.get_mut(&owner).unwrap().steps_mut()
|
|
&& !steps.contains(&id)
|
|
{
|
|
steps.push(id);
|
|
}
|
|
id
|
|
}
|
|
|
|
fn targets(state: &State, p: &Project, owner: Id, subject: Id) -> BTreeSet<Id> {
|
|
state
|
|
.diagram_usages(p, owner, subject)
|
|
.into_iter()
|
|
.filter_map(|u| match u.target {
|
|
Target::ScenarioObject(id) => Some(id),
|
|
_ => None,
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_usage_counts_nested_occurrences_and_lifelines_but_keeps_reuse_opaque() {
|
|
for (layer, work_kind, participant_kind) in [
|
|
(Layer::Oa, Kind::Activity, Kind::Entity),
|
|
(Layer::Sa, Kind::Function, Kind::SystemActor),
|
|
] {
|
|
let mut p = Project::empty();
|
|
let owner = p
|
|
.add_element(Kind::Scenario, layer, "Caller", None)
|
|
.unwrap();
|
|
let other = p
|
|
.add_element(Kind::Scenario, layer, "Definition", None)
|
|
.unwrap();
|
|
let work = p.add_element(work_kind, layer, "Respond", None).unwrap();
|
|
let unused = p.add_element(work_kind, layer, "Respond", None).unwrap();
|
|
let participant = p
|
|
.add_element(participant_kind, layer, "Operator", None)
|
|
.unwrap();
|
|
let lanes = ["First role", "Second role"].map(|name| {
|
|
let id = p
|
|
.add_element(Kind::Lifeline, layer, name, Some(owner))
|
|
.unwrap();
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(participant),
|
|
};
|
|
id
|
|
});
|
|
let first = add_occurrence(
|
|
&mut p,
|
|
owner,
|
|
"First action",
|
|
OccurrenceBody::Action {
|
|
action: Some(work),
|
|
lifeline: Some(lanes[0]),
|
|
},
|
|
);
|
|
let fragment = add_occurrence(
|
|
&mut p,
|
|
owner,
|
|
"Decision",
|
|
OccurrenceBody::Fragment {
|
|
operator: FragmentOperator::Alternative,
|
|
branches: vec![],
|
|
},
|
|
);
|
|
let branch = add_occurrence(
|
|
&mut p,
|
|
fragment,
|
|
"When ready",
|
|
OccurrenceBody::Branch {
|
|
guard: "ready".into(),
|
|
occurrences: vec![],
|
|
},
|
|
);
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Fragment { branches, .. },
|
|
..
|
|
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
|
|
{
|
|
branches.push(branch);
|
|
}
|
|
let nested = add_occurrence(
|
|
&mut p,
|
|
branch,
|
|
"Again",
|
|
OccurrenceBody::Action {
|
|
action: Some(work),
|
|
lifeline: Some(lanes[1]),
|
|
},
|
|
);
|
|
let inside = add_occurrence(
|
|
&mut p,
|
|
other,
|
|
"Hidden inside definition",
|
|
OccurrenceBody::Action {
|
|
action: Some(unused),
|
|
lifeline: None,
|
|
},
|
|
);
|
|
let use_a = add_occurrence(
|
|
&mut p,
|
|
owner,
|
|
"Use A",
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(other),
|
|
bindings: vec![],
|
|
},
|
|
);
|
|
let use_b = add_occurrence(
|
|
&mut p,
|
|
owner,
|
|
"Use B",
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(other),
|
|
bindings: vec![],
|
|
},
|
|
);
|
|
let before = p.clone();
|
|
let state = State::default();
|
|
assert_eq!(
|
|
targets(&state, &p, owner, work),
|
|
BTreeSet::from([first, nested])
|
|
);
|
|
assert_eq!(
|
|
targets(&state, &p, owner, participant),
|
|
BTreeSet::from(lanes)
|
|
);
|
|
assert_eq!(
|
|
targets(&state, &p, owner, other),
|
|
BTreeSet::from([use_a, use_b])
|
|
);
|
|
assert!(targets(&state, &p, owner, unused).is_empty());
|
|
assert!(targets(&state, &p, owner, inside).is_empty());
|
|
assert_eq!(targets(&state, &p, owner, branch), BTreeSet::from([branch]));
|
|
assert_eq!(
|
|
state.diagram_usages(&p, owner, owner)[0].target,
|
|
Target::Whole
|
|
);
|
|
assert_eq!(p, before);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_usage_keeps_repeated_messages_and_reveal_is_read_only_and_revalidated() {
|
|
let f = super::super::direct_tests::fixture();
|
|
let mut p = f.project;
|
|
let mut state = State::default();
|
|
assert_eq!(
|
|
targets(&state, &p, f.owner, f.exchange),
|
|
BTreeSet::from([f.call, f.reply])
|
|
);
|
|
let before = p.clone();
|
|
let target = Target::ScenarioObject(f.reply);
|
|
state.viewport(Size::new(900., 500.));
|
|
assert!(state.reveal_usage(&p, Id::new_v4(), f.owner, &target));
|
|
let usage = state
|
|
.diagram_usages(&p, f.owner, f.reply)
|
|
.into_iter()
|
|
.find(|u| u.target == target)
|
|
.unwrap();
|
|
let zoom = state
|
|
.presentation
|
|
.scale(Scene::sequence(&p, f.owner, f.reply).size);
|
|
let centered = Point::new(
|
|
usage.bounds.center_x() * zoom,
|
|
usage.bounds.center_y() * zoom,
|
|
) + state.presentation.pan;
|
|
assert!((centered.x - 450.).abs() < 0.01 && (centered.y - 250.).abs() < 0.01);
|
|
assert!(zoom >= 0.5);
|
|
assert_eq!(p, before);
|
|
// Keeping an orphan semantic occurrence does not keep its painted target.
|
|
p.elements
|
|
.get_mut(&f.owner)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.retain(|id| *id != f.reply);
|
|
let camera = state.camera_boundary();
|
|
assert!(!state.reveal_usage(&p, Id::new_v4(), f.owner, &target));
|
|
assert!(camera == state.camera_boundary());
|
|
assert!(!state.reveal_usage(&p, Id::new_v4(), f.owner, &Target::Edge(f.exchange)));
|
|
// A message whose endpoints are incomplete paints only its repair box.
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { receiver, .. },
|
|
..
|
|
}) = p.elements.get_mut(&f.call).unwrap().shared_mut()
|
|
{
|
|
*receiver = None;
|
|
}
|
|
assert!(targets(&state, &p, f.owner, f.exchange).is_empty());
|
|
assert_eq!(
|
|
targets(&state, &p, f.owner, f.call),
|
|
BTreeSet::from([f.call])
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_operation_label_membership_excludes_ellipsis_only_and_normalizes_whitespace() {
|
|
assert!(label_field_visible(
|
|
"1. call · X · Operation: I / O",
|
|
"1. call · X · Operation: ",
|
|
"I"
|
|
));
|
|
assert!(!label_field_visible(
|
|
"1234567890123456789012345678901Ixyz",
|
|
"1234567890123456789012345678901",
|
|
"Ixyz"
|
|
));
|
|
assert!(!label_field_visible(
|
|
"A very long occurrence label · Operation: I",
|
|
"A very long occurrence label · Operation: ",
|
|
"I"
|
|
));
|
|
assert!(label_field_visible(
|
|
"call\n\t· Operation: 界",
|
|
"call\n\t· Operation: ",
|
|
"界"
|
|
));
|
|
assert!(!label_field_visible("Operation: ", "Operation: ", ""));
|
|
}
|
|
|
|
#[test]
|
|
fn scenario_usage_only_reports_the_resolved_operation_fields_actually_in_the_label() {
|
|
let mut p = persistence::decode(include_bytes!(concat!(
|
|
env!("CARGO_MANIFEST_DIR"),
|
|
"/../../harness/fixtures/interface-operation-probe/output/boundary-only.json"
|
|
)))
|
|
.unwrap();
|
|
let id = |n| Id::parse_str(&format!("52060906-0000-4000-8000-{n:012x}")).unwrap();
|
|
let (owner, call, exchange, allocation) = (id(40), id(43), id(34), id(30));
|
|
let (interface, item) = (
|
|
p.relations[&allocation].source(),
|
|
p.relations[&allocation].target(),
|
|
);
|
|
for (id, name) in [(call, "x"), (interface, "I"), (item, "O")] {
|
|
p.elements.get_mut(&id).unwrap().name = name.into();
|
|
}
|
|
p.relations.get_mut(&exchange).unwrap().name = "x".into();
|
|
p.relations.get_mut(&allocation).unwrap().name = "A".into();
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { operation, .. },
|
|
..
|
|
}) = p.elements.get_mut(&call).unwrap().shared_mut()
|
|
{
|
|
*operation = Some(OperationUse {
|
|
allocation: Some(allocation),
|
|
});
|
|
}
|
|
assert!(matches!(
|
|
operations::resolve(&p, call),
|
|
Ok(operations::OperationResolution::Selected(_))
|
|
));
|
|
let state = State::default();
|
|
let before = p.clone();
|
|
assert_eq!(
|
|
targets(&state, &p, owner, interface),
|
|
BTreeSet::from([call])
|
|
);
|
|
assert_eq!(targets(&state, &p, owner, item), BTreeSet::from([call]));
|
|
assert!(targets(&state, &p, owner, allocation).is_empty());
|
|
assert_eq!(p, before);
|
|
p.elements.get_mut(&call).unwrap().name = "This occurrence has a deliberately long name".into();
|
|
assert!(targets(&state, &p, owner, interface).is_empty());
|
|
assert!(targets(&state, &p, owner, item).is_empty());
|
|
}
|