1376 lines
40 KiB
Rust
1376 lines
40 KiB
Rust
use oa_core::{
|
|
references,
|
|
transitions::*,
|
|
validation::{self, Severity},
|
|
*,
|
|
};
|
|
|
|
fn add_occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id {
|
|
let mut e = p
|
|
.make_element(
|
|
Kind::Occurrence,
|
|
p.elements[&owner].layer(),
|
|
name,
|
|
Some(owner),
|
|
)
|
|
.unwrap();
|
|
*e.shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
let id = e.id;
|
|
p.elements.insert(id, e);
|
|
p.elements
|
|
.get_mut(&owner)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(id);
|
|
id
|
|
}
|
|
fn line(p: &mut Project, scenario: Id, represented: Id, name: &str) -> Id {
|
|
let id = p
|
|
.add_element(
|
|
Kind::Lifeline,
|
|
p.elements[&scenario].layer(),
|
|
name,
|
|
Some(scenario),
|
|
)
|
|
.unwrap();
|
|
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(represented),
|
|
};
|
|
id
|
|
}
|
|
fn message(
|
|
exchange: Option<Id>,
|
|
sender: Id,
|
|
receiver: Option<Id>,
|
|
kind: MessageKind,
|
|
call: Option<Id>,
|
|
) -> OccurrenceBody {
|
|
OccurrenceBody::Exchange {
|
|
exchange,
|
|
sender: Some(sender),
|
|
receiver,
|
|
message_kind: kind,
|
|
reply_to: call,
|
|
values: vec![],
|
|
}
|
|
}
|
|
struct Fixture {
|
|
p: Project,
|
|
scenario: Id,
|
|
a: Id,
|
|
b: Id,
|
|
call: Id,
|
|
reply: Id,
|
|
exchange: Id,
|
|
field: Id,
|
|
}
|
|
fn fixture(layer: Layer) -> Fixture {
|
|
let mut p = Project::empty();
|
|
let work_kind = if layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
let client = p
|
|
.add_element(work_kind, layer, "Request service", None)
|
|
.unwrap();
|
|
let server = p
|
|
.add_element(work_kind, layer, "Provide service", None)
|
|
.unwrap();
|
|
let scenario = p
|
|
.add_element(Kind::Scenario, layer, "Service conversation", None)
|
|
.unwrap();
|
|
let a = line(&mut p, scenario, client, "Client");
|
|
let b = line(&mut p, scenario, server, "Provider");
|
|
let item = p
|
|
.add_element(Kind::ExchangeItem, layer, "Service request", None)
|
|
.unwrap();
|
|
let record = p
|
|
.add_element(Kind::DataType, layer, "Request record", None)
|
|
.unwrap();
|
|
*p.elements.get_mut(&record).unwrap().shared_mut().unwrap() = SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype: None },
|
|
default: None,
|
|
unit: String::new(),
|
|
};
|
|
let field = p
|
|
.add_element(Kind::DataField, layer, "Requested level", Some(record))
|
|
.unwrap();
|
|
let payload = p
|
|
.add_element(Kind::DataField, layer, "Payload", Some(item))
|
|
.unwrap();
|
|
if let Some(SharedBody::DataField { type_ref, .. }) =
|
|
p.elements.get_mut(&payload).unwrap().shared_mut()
|
|
{
|
|
*type_ref = TypeRef::Named(record);
|
|
}
|
|
let exchange = p.relate(
|
|
if layer == Layer::Oa {
|
|
RelationKind::Interaction
|
|
} else {
|
|
RelationKind::FunctionalExchange
|
|
},
|
|
"Service invocation",
|
|
client,
|
|
server,
|
|
);
|
|
match &mut p.relations.get_mut(&exchange).unwrap().body {
|
|
RelationBody::Interaction { items, .. }
|
|
| RelationBody::FunctionalExchange { items, .. } => items.push(item),
|
|
_ => unreachable!(),
|
|
}
|
|
if let RelationBody::FunctionalExchange {
|
|
source_port,
|
|
target_port,
|
|
..
|
|
} = p.relations[&exchange].body
|
|
{
|
|
for port in [source_port, target_port] {
|
|
match p.elements.get_mut(&port).unwrap().shared_mut() {
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::FunctionInput { incoming_items },
|
|
..
|
|
}) => incoming_items.push(item),
|
|
Some(SharedBody::Port {
|
|
contract: PortContract::FunctionOutput { outgoing_items },
|
|
..
|
|
}) => outgoing_items.push(item),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
let call = add_occurrence(
|
|
&mut p,
|
|
scenario,
|
|
"Request service",
|
|
message(Some(exchange), a, Some(b), MessageKind::Call, None),
|
|
);
|
|
let reply = add_occurrence(
|
|
&mut p,
|
|
scenario,
|
|
"Return result",
|
|
message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)),
|
|
);
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = p.elements.get_mut(&call).unwrap().shared_mut()
|
|
{
|
|
values.push(MessageValue {
|
|
path: vec![payload],
|
|
field,
|
|
expression: "requested_level".into(),
|
|
});
|
|
}
|
|
Fixture {
|
|
p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
call,
|
|
reply,
|
|
exchange,
|
|
field,
|
|
}
|
|
}
|
|
fn valid(p: &Project) {
|
|
let errors: Vec<_> = validation::validate(p)
|
|
.into_iter()
|
|
.filter(|i| i.severity == Severity::Error)
|
|
.collect();
|
|
assert!(errors.is_empty(), "{errors:?}");
|
|
}
|
|
fn edit_message(p: &Project, id: Id, edit: impl FnOnce(&mut OccurrenceBody)) -> Element {
|
|
let mut element = p.elements[&id].clone();
|
|
if let Some(SharedBody::Occurrence { body, .. }) = element.shared_mut() {
|
|
edit(body);
|
|
}
|
|
element
|
|
}
|
|
|
|
#[test]
|
|
fn calls_and_replies_have_real_direction_correlation_and_order() {
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
let Fixture {
|
|
p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
call,
|
|
reply,
|
|
exchange,
|
|
..
|
|
} = fixture(layer);
|
|
valid(&p);
|
|
let mut session = Session::new(p.clone());
|
|
assert!(
|
|
session
|
|
.execute(Command::MoveStep(scenario, 1, 0))
|
|
.unwrap_err()
|
|
.0
|
|
.contains("reply_order")
|
|
);
|
|
assert_eq!(session.project(), &p);
|
|
for edit in [
|
|
edit_message(&p, reply, |body| {
|
|
if let OccurrenceBody::Exchange {
|
|
sender, receiver, ..
|
|
} = body
|
|
{
|
|
*sender = Some(a);
|
|
*receiver = Some(b);
|
|
}
|
|
}),
|
|
edit_message(&p, reply, |body| {
|
|
if let OccurrenceBody::Exchange { reply_to, .. } = body {
|
|
*reply_to = Some(reply);
|
|
}
|
|
}),
|
|
] {
|
|
assert!(session.execute(Command::EditElement(reply, edit)).is_err());
|
|
assert_eq!(session.project(), &p);
|
|
}
|
|
let mut duplicate = p.clone();
|
|
add_occurrence(
|
|
&mut duplicate,
|
|
scenario,
|
|
"Second return",
|
|
message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)),
|
|
);
|
|
assert!(
|
|
validate_candidate(&duplicate)
|
|
.unwrap_err()
|
|
.0
|
|
.contains("duplicate_reply")
|
|
);
|
|
let mut reverse = p.clone();
|
|
let original = &p.relations[&exchange];
|
|
let reverse_exchange = reverse.relate(
|
|
original.kind(),
|
|
"Explicit return contract",
|
|
original.target(),
|
|
original.source(),
|
|
);
|
|
let edited = edit_message(&reverse, reply, |body| {
|
|
if let OccurrenceBody::Exchange { exchange, .. } = body {
|
|
*exchange = Some(reverse_exchange);
|
|
}
|
|
});
|
|
reverse.elements.insert(reply, edited);
|
|
valid(&reverse);
|
|
assert_eq!(reverse.relations[&exchange], p.relations[&exchange]);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn reused_scenarios_bind_existing_roles_and_transition_the_reference_closure() {
|
|
for direction in [TransitionDirection::OaToSa, TransitionDirection::SaToOa] {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
..
|
|
} = fixture(direction.source_layer());
|
|
let represented = |p: &Project, id| match p.elements[&id].shared().unwrap() {
|
|
SharedBody::Lifeline {
|
|
represented: Some(id),
|
|
} => *id,
|
|
_ => unreachable!(),
|
|
};
|
|
let client = represented(&p, a);
|
|
let provider = represented(&p, b);
|
|
let mut callers = vec![];
|
|
for name in ["Nominal service", "Degraded service"] {
|
|
let caller = p
|
|
.add_element(Kind::Scenario, direction.source_layer(), name, None)
|
|
.unwrap();
|
|
let local_a = line(&mut p, caller, client, "Caller client");
|
|
let local_b = line(&mut p, caller, provider, "Caller provider");
|
|
let use_id = add_occurrence(
|
|
&mut p,
|
|
caller,
|
|
"Reuse service conversation",
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(scenario),
|
|
bindings: vec![
|
|
LifelineBinding {
|
|
referenced: a,
|
|
local: local_a,
|
|
},
|
|
LifelineBinding {
|
|
referenced: b,
|
|
local: local_b,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
callers.push((caller, use_id));
|
|
}
|
|
valid(&p);
|
|
let request = {
|
|
let mut r =
|
|
TransitionRequest::new(direction, callers.iter().map(|(id, _)| *id).collect());
|
|
r.scope = DependencyScope::Required;
|
|
r
|
|
};
|
|
let plan = plan_transition(&p, &request).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let mapped = |id| plan.changes.iter().find(|c| c.source == id).unwrap().target;
|
|
let target = apply_transition(&p, &plan).unwrap();
|
|
valid(&target);
|
|
for (caller, use_id) in &callers {
|
|
assert!(
|
|
references::element_references(&target.elements[&mapped(*use_id)])
|
|
.contains(&mapped(scenario))
|
|
);
|
|
assert_eq!(
|
|
target.elements[&mapped(*caller)].layer(),
|
|
direction.target_layer()
|
|
);
|
|
assert!(
|
|
matches!(target.elements[&mapped(*use_id)].shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::ScenarioUse{scenario:Some(id),..},..})if *id==mapped(scenario))
|
|
);
|
|
}
|
|
let mut session = Session::new(p.clone());
|
|
session
|
|
.execute(Command::Rename(
|
|
scenario,
|
|
"Reviewed shared conversation".into(),
|
|
))
|
|
.unwrap();
|
|
for (_, use_id) in callers {
|
|
assert_eq!(session.project().elements[&use_id], p.elements[&use_id]);
|
|
}
|
|
session.undo();
|
|
assert_eq!(session.project(), &p);
|
|
let reverse = {
|
|
let mut r = TransitionRequest::new(
|
|
if direction == TransitionDirection::OaToSa {
|
|
TransitionDirection::SaToOa
|
|
} else {
|
|
TransitionDirection::OaToSa
|
|
},
|
|
vec![mapped(scenario)],
|
|
);
|
|
r.scope = DependencyScope::Required;
|
|
r
|
|
};
|
|
let repeat = plan_transition(&target, &reverse).unwrap();
|
|
assert!(repeat.conflicts.is_empty());
|
|
assert!(repeat.commands.is_empty());
|
|
}
|
|
}
|
|
#[test]
|
|
fn reuse_rejects_wrong_bindings_recursive_calls_and_wrong_viewpoints() {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
let caller = p
|
|
.add_element(Kind::Scenario, Layer::Sa, "Caller", None)
|
|
.unwrap();
|
|
let wrong_work = p
|
|
.add_element(Kind::Function, Layer::Sa, "Different function", None)
|
|
.unwrap();
|
|
let wrong = line(&mut p, caller, wrong_work, "Wrong role");
|
|
let use_id = add_occurrence(
|
|
&mut p,
|
|
caller,
|
|
"Bad role binding",
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(scenario),
|
|
bindings: vec![LifelineBinding {
|
|
referenced: a,
|
|
local: wrong,
|
|
}],
|
|
},
|
|
);
|
|
assert!(
|
|
validate_candidate(&p)
|
|
.unwrap_err()
|
|
.0
|
|
.contains("scenario_binding_identity")
|
|
);
|
|
let correct = match p.elements[&a].shared().unwrap() {
|
|
SharedBody::Lifeline {
|
|
represented: Some(id),
|
|
} => *id,
|
|
_ => unreachable!(),
|
|
};
|
|
*p.elements.get_mut(&wrong).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(correct),
|
|
};
|
|
valid(&p);
|
|
assert!(
|
|
validation::validate(&p)
|
|
.iter()
|
|
.any(|i| i.element == Some(use_id) && i.code == "missing_lifeline_binding")
|
|
);
|
|
let mut cycle = p.clone();
|
|
add_occurrence(
|
|
&mut cycle,
|
|
scenario,
|
|
"Recursive caller",
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(caller),
|
|
bindings: vec![],
|
|
},
|
|
);
|
|
assert!(
|
|
validation::validate(&cycle)
|
|
.iter()
|
|
.any(|i| i.code == "recursive_scenario_use" && i.severity == Severity::Error)
|
|
);
|
|
let mut wrong_context = p.clone();
|
|
let edited = edit_message(&wrong_context, use_id, |body| {
|
|
if let OccurrenceBody::ScenarioUse { bindings, .. } = body {
|
|
bindings[0].local = b;
|
|
}
|
|
});
|
|
wrong_context.elements.insert(use_id, edited);
|
|
assert!(
|
|
validation::validate(&wrong_context)
|
|
.iter()
|
|
.any(|i| i.code == "scenario_binding_context")
|
|
);
|
|
if let Some(SharedBody::Scenario { viewpoint, .. }) =
|
|
p.elements.get_mut(&caller).unwrap().shared_mut()
|
|
{
|
|
*viewpoint = ScenarioViewpoint::BoundaryInterface;
|
|
}
|
|
assert!(
|
|
validation::validate(&p)
|
|
.iter()
|
|
.any(|i| i.code == "scenario_use_viewpoint")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn create_delete_and_local_timer_messages_enforce_lifetime_rules() {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
exchange,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
let create = add_occurrence(
|
|
&mut p,
|
|
scenario,
|
|
"Create provider",
|
|
message(Some(exchange), a, Some(b), MessageKind::Create, None),
|
|
);
|
|
let steps = p.elements.get_mut(&scenario).unwrap().steps_mut().unwrap();
|
|
steps.pop();
|
|
steps.insert(0, create);
|
|
let timer = add_occurrence(
|
|
&mut p,
|
|
scenario,
|
|
"Wait for completion",
|
|
message(
|
|
None,
|
|
b,
|
|
None,
|
|
MessageKind::Timer {
|
|
expression: "after 5 s".into(),
|
|
},
|
|
None,
|
|
),
|
|
);
|
|
let delete = add_occurrence(
|
|
&mut p,
|
|
scenario,
|
|
"Delete provider",
|
|
message(Some(exchange), a, Some(b), MessageKind::Delete, None),
|
|
);
|
|
valid(&p);
|
|
let mut session = Session::new(p.clone());
|
|
for command in [
|
|
Command::MoveStep(scenario, 0, 2),
|
|
Command::MoveStep(scenario, 4, 1),
|
|
] {
|
|
assert!(
|
|
session
|
|
.execute(command)
|
|
.unwrap_err()
|
|
.0
|
|
.contains("lifeline_lifetime")
|
|
);
|
|
assert_eq!(session.project(), &p);
|
|
}
|
|
let bad = edit_message(&p, timer, |body| {
|
|
if let OccurrenceBody::Exchange { receiver, .. } = body {
|
|
*receiver = Some(a);
|
|
}
|
|
});
|
|
assert!(
|
|
session
|
|
.execute(Command::EditElement(timer, bad))
|
|
.unwrap_err()
|
|
.0
|
|
.contains("timer_contract")
|
|
);
|
|
let incomplete = edit_message(&p, timer, |body| {
|
|
if let OccurrenceBody::Exchange { message_kind, .. } = body {
|
|
*message_kind = MessageKind::Timer {
|
|
expression: String::new(),
|
|
};
|
|
}
|
|
});
|
|
session
|
|
.execute(Command::EditElement(timer, incomplete))
|
|
.unwrap();
|
|
assert!(
|
|
validation::validate(session.project())
|
|
.iter()
|
|
.any(|i| i.element == Some(timer) && i.code == "missing_timer_expression")
|
|
);
|
|
assert_eq!(session.project().elements[&delete], p.elements[&delete]);
|
|
}
|
|
|
|
#[test]
|
|
fn value_annotations_are_bound_to_carried_fields_and_all_references_follow_identity_changes() {
|
|
let Fixture { p, call, field, .. } = fixture(Layer::Oa);
|
|
valid(&p);
|
|
let mut duplicate = edit_message(&p, call, |body| {
|
|
if let OccurrenceBody::Exchange { values, .. } = body {
|
|
values.push(values[0].clone());
|
|
}
|
|
});
|
|
let mut session = Session::new(p.clone());
|
|
assert!(
|
|
session
|
|
.execute(Command::EditElement(call, duplicate.clone()))
|
|
.unwrap_err()
|
|
.0
|
|
.contains("duplicate_message_value")
|
|
);
|
|
let mut unrelated = p.clone();
|
|
let item = unrelated
|
|
.add_element(Kind::ExchangeItem, Layer::Oa, "Unrelated item", None)
|
|
.unwrap();
|
|
let other = unrelated
|
|
.add_element(Kind::DataField, Layer::Oa, "Unrelated field", Some(item))
|
|
.unwrap();
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = duplicate.shared_mut()
|
|
{
|
|
values.truncate(1);
|
|
values[0].field = other;
|
|
}
|
|
assert!(
|
|
execute_batch(&unrelated, &[Command::EditElement(call, duplicate)])
|
|
.unwrap_err()
|
|
.0
|
|
.contains("message_value_field")
|
|
);
|
|
for id in p.elements.keys().chain(p.relations.keys()).copied() {
|
|
let mut session = Session::new(p.clone());
|
|
let replacement = Id::new_v4();
|
|
session
|
|
.execute(Command::Reidentify(id, replacement))
|
|
.unwrap();
|
|
let renamed = session.project().clone();
|
|
assert!(
|
|
renamed
|
|
.elements
|
|
.values()
|
|
.all(|e| !references::element_references(e).contains(&id))
|
|
);
|
|
session.execute(Command::Delete(replacement)).unwrap();
|
|
valid(session.project());
|
|
session.undo();
|
|
assert_eq!(session.project(), &renamed);
|
|
session.undo();
|
|
assert_eq!(session.project(), &p);
|
|
}
|
|
session.execute(Command::Delete(field)).unwrap();
|
|
assert!(
|
|
matches!(session.project().elements[&call].shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::Exchange{values,..},..})if values.is_empty())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn loop_fragment_repetition_is_distinct_from_recursive_scenario_use() {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
call,
|
|
reply,
|
|
..
|
|
} = fixture(Layer::Oa);
|
|
let mut fragment = p
|
|
.make_element(Kind::Occurrence, Layer::Oa, "Retry service", Some(scenario))
|
|
.unwrap();
|
|
let fragment_id = fragment.id;
|
|
*fragment.shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Fragment {
|
|
operator: FragmentOperator::Loop,
|
|
branches: vec![],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
let mut branch = p
|
|
.make_element(
|
|
Kind::Occurrence,
|
|
Layer::Oa,
|
|
"While needed",
|
|
Some(fragment_id),
|
|
)
|
|
.unwrap();
|
|
let branch_id = branch.id;
|
|
*branch.shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Branch {
|
|
guard: "retry && count < 3".into(),
|
|
occurrences: vec![call, reply],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Fragment { branches, .. },
|
|
..
|
|
}) = fragment.shared_mut()
|
|
{
|
|
branches.push(branch_id);
|
|
}
|
|
p.elements.get_mut(&call).unwrap().parent = Some(branch_id);
|
|
p.elements.get_mut(&reply).unwrap().parent = Some(branch_id);
|
|
*p.elements.get_mut(&scenario).unwrap().steps_mut().unwrap() = vec![fragment_id];
|
|
p.elements.insert(fragment_id, fragment);
|
|
p.elements.insert(branch_id, branch);
|
|
valid(&p);
|
|
let exchange = p_exchange(&p, call);
|
|
let outside = add_occurrence(
|
|
&mut p,
|
|
scenario,
|
|
"Reply outside optional loop",
|
|
message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)),
|
|
);
|
|
assert!(
|
|
validation::validate(&p)
|
|
.iter()
|
|
.any(|i| i.element == Some(outside) && i.code == "reply_order")
|
|
);
|
|
}
|
|
fn p_exchange(p: &Project, id: Id) -> Id {
|
|
match p.elements[&id].shared().unwrap() {
|
|
SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
exchange: Some(id), ..
|
|
},
|
|
..
|
|
} => *id,
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn deleting_reused_scenarios_or_bound_lifelines_preserves_callers_and_undo() {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
let caller = p
|
|
.add_element(Kind::Scenario, Layer::Sa, "Caller", None)
|
|
.unwrap();
|
|
let client = match p.elements[&a].shared().unwrap() {
|
|
SharedBody::Lifeline {
|
|
represented: Some(id),
|
|
} => *id,
|
|
_ => unreachable!(),
|
|
};
|
|
let provider = match p.elements[&b].shared().unwrap() {
|
|
SharedBody::Lifeline {
|
|
represented: Some(id),
|
|
} => *id,
|
|
_ => unreachable!(),
|
|
};
|
|
let local_a = line(&mut p, caller, client, "Local client");
|
|
let local_b = line(&mut p, caller, provider, "Local provider");
|
|
let use_id = add_occurrence(
|
|
&mut p,
|
|
caller,
|
|
"Shared conversation",
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(scenario),
|
|
bindings: vec![
|
|
LifelineBinding {
|
|
referenced: a,
|
|
local: local_a,
|
|
},
|
|
LifelineBinding {
|
|
referenced: b,
|
|
local: local_b,
|
|
},
|
|
],
|
|
},
|
|
);
|
|
valid(&p);
|
|
for id in [scenario, a, b, local_a, local_b, use_id] {
|
|
let mut session = Session::new(p.clone());
|
|
let replacement = Id::new_v4();
|
|
session
|
|
.execute(Command::Reidentify(id, replacement))
|
|
.unwrap();
|
|
assert!(
|
|
session
|
|
.project()
|
|
.elements
|
|
.values()
|
|
.all(|e| !references::element_references(e).contains(&id))
|
|
);
|
|
let renamed = session.project().clone();
|
|
session.execute(Command::Delete(replacement)).unwrap();
|
|
valid(session.project());
|
|
assert!(session.project().elements.contains_key(&caller));
|
|
if id == scenario {
|
|
assert!(
|
|
matches!(session.project().elements[&use_id].shared(),Some(SharedBody::Occurrence{body:OccurrenceBody::ScenarioUse{scenario:None,bindings},..})if bindings.is_empty())
|
|
);
|
|
assert!(session.project().elements.contains_key(&local_a));
|
|
}
|
|
session.undo();
|
|
assert_eq!(session.project(), &renamed);
|
|
session.undo();
|
|
assert_eq!(session.project(), &p);
|
|
}
|
|
}
|
|
|
|
fn scenario_branch(p: &mut Project, owner: Id, operator: FragmentOperator) -> (Id, Id) {
|
|
let fragment = add_occurrence(
|
|
p,
|
|
owner,
|
|
"Conditional fragment",
|
|
OccurrenceBody::Fragment {
|
|
operator,
|
|
branches: vec![],
|
|
},
|
|
);
|
|
let branch = additional_branch(p, fragment);
|
|
(fragment, branch)
|
|
}
|
|
fn additional_branch(p: &mut Project, fragment: Id) -> Id {
|
|
let mut branch = p
|
|
.make_element(
|
|
Kind::Occurrence,
|
|
p.elements[&fragment].layer(),
|
|
"Operand",
|
|
Some(fragment),
|
|
)
|
|
.unwrap();
|
|
let id = branch.id;
|
|
*branch.shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Branch {
|
|
guard: "condition".into(),
|
|
occurrences: vec![],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
p.elements.insert(id, branch);
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Fragment { branches, .. },
|
|
..
|
|
}) = p.elements.get_mut(&fragment).unwrap().shared_mut()
|
|
{
|
|
branches.push(id);
|
|
}
|
|
id
|
|
}
|
|
|
|
#[test]
|
|
fn conditional_and_parallel_replies_cannot_answer_one_call_twice_but_alternatives_can() {
|
|
for operator in [FragmentOperator::Optional, FragmentOperator::Alternative] {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
call,
|
|
reply,
|
|
exchange,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
p.elements.remove(&reply);
|
|
p.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.retain(|id| *id != reply);
|
|
let (_, operand) = scenario_branch(&mut p, scenario, operator);
|
|
add_occurrence(
|
|
&mut p,
|
|
operand,
|
|
"Conditional return",
|
|
message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)),
|
|
);
|
|
valid(&p);
|
|
let before = p.clone();
|
|
add_occurrence(
|
|
&mut p,
|
|
scenario,
|
|
"Later return",
|
|
message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)),
|
|
);
|
|
let mut session = Session::new(before.clone());
|
|
assert!(
|
|
session
|
|
.execute(Command::Replace(p))
|
|
.unwrap_err()
|
|
.0
|
|
.contains("duplicate_reply")
|
|
);
|
|
assert_eq!(session.project(), &before);
|
|
}
|
|
for operator in [FragmentOperator::Alternative, FragmentOperator::Parallel] {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
call,
|
|
reply,
|
|
exchange,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
p.elements.remove(&reply);
|
|
p.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.retain(|id| *id != reply);
|
|
let (fragment, left) = scenario_branch(&mut p, scenario, operator);
|
|
let right = additional_branch(&mut p, fragment);
|
|
for operand in [left, right] {
|
|
add_occurrence(
|
|
&mut p,
|
|
operand,
|
|
"Branch return",
|
|
message(Some(exchange), b, Some(a), MessageKind::Reply, Some(call)),
|
|
);
|
|
}
|
|
if operator == FragmentOperator::Alternative {
|
|
valid(&p);
|
|
} else {
|
|
assert!(
|
|
validate_candidate(&p)
|
|
.unwrap_err()
|
|
.0
|
|
.contains("duplicate_reply")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn conditional_lifetime_changes_conflict_with_later_or_parallel_use_not_exclusive_use() {
|
|
for operator in [FragmentOperator::Optional, FragmentOperator::Alternative] {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
exchange,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
let (_, operand) = scenario_branch(&mut p, scenario, operator);
|
|
add_occurrence(
|
|
&mut p,
|
|
operand,
|
|
"Conditional deletion",
|
|
message(Some(exchange), a, Some(b), MessageKind::Delete, None),
|
|
);
|
|
valid(&p);
|
|
add_occurrence(
|
|
&mut p,
|
|
scenario,
|
|
"Use after fragment",
|
|
message(Some(exchange), a, Some(b), MessageKind::Asynchronous, None),
|
|
);
|
|
assert!(
|
|
validate_candidate(&p)
|
|
.unwrap_err()
|
|
.0
|
|
.contains("lifeline_lifetime")
|
|
);
|
|
}
|
|
for operator in [FragmentOperator::Alternative, FragmentOperator::Parallel] {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
exchange,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
let (fragment, left) = scenario_branch(&mut p, scenario, operator);
|
|
let right = additional_branch(&mut p, fragment);
|
|
add_occurrence(
|
|
&mut p,
|
|
left,
|
|
"Delete in one operand",
|
|
message(Some(exchange), a, Some(b), MessageKind::Delete, None),
|
|
);
|
|
add_occurrence(
|
|
&mut p,
|
|
right,
|
|
"Use in another operand",
|
|
message(Some(exchange), a, Some(b), MessageKind::Asynchronous, None),
|
|
);
|
|
if operator == FragmentOperator::Alternative {
|
|
valid(&p);
|
|
} else {
|
|
assert!(
|
|
validate_candidate(&p)
|
|
.unwrap_err()
|
|
.0
|
|
.contains("lifeline_lifetime")
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn role_lifelines_require_explicit_work_assignment_not_just_a_shared_realizing_entity() {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
exchange,
|
|
..
|
|
} = fixture(Layer::Oa);
|
|
let work = p.relations[&exchange].source();
|
|
let target = p.relations[&exchange].target();
|
|
let entity = p
|
|
.add_element(Kind::Entity, Layer::Oa, "Controller", None)
|
|
.unwrap();
|
|
let other = p
|
|
.add_element(Kind::Entity, Layer::Oa, "Recipient", None)
|
|
.unwrap();
|
|
let role = p
|
|
.add_element(Kind::Role, Layer::Oa, "Coordinator", None)
|
|
.unwrap();
|
|
let wrong_role = p
|
|
.add_element(Kind::Role, Layer::Oa, "Unassigned role", None)
|
|
.unwrap();
|
|
let allocation = p.relate(
|
|
RelationKind::Allocation,
|
|
"Controller performs",
|
|
entity,
|
|
work,
|
|
);
|
|
p.relate(
|
|
RelationKind::Allocation,
|
|
"Recipient performs",
|
|
other,
|
|
target,
|
|
);
|
|
p.relate(
|
|
RelationKind::RoleRealization,
|
|
"Controller realizes coordinator",
|
|
entity,
|
|
role,
|
|
);
|
|
p.relate(
|
|
RelationKind::RoleRealization,
|
|
"Controller also realizes unassigned role",
|
|
entity,
|
|
wrong_role,
|
|
);
|
|
p.relate(
|
|
RelationKind::RoleAssignment,
|
|
"Coordinator assignment",
|
|
role,
|
|
work,
|
|
);
|
|
if let Some(SharedBody::Scenario { viewpoint, .. }) =
|
|
p.elements.get_mut(&scenario).unwrap().shared_mut()
|
|
{
|
|
*viewpoint = ScenarioViewpoint::ParticipantInteraction;
|
|
}
|
|
*p.elements.get_mut(&a).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(role),
|
|
};
|
|
*p.elements.get_mut(&b).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(other),
|
|
};
|
|
valid(&p);
|
|
assert_eq!(
|
|
validation::lifeline_matches_endpoint(&p, a, work),
|
|
Some(true)
|
|
);
|
|
assert_eq!(
|
|
validation::semantic_representation_compatible(&p, wrong_role, work),
|
|
Some(false)
|
|
);
|
|
assert_eq!(
|
|
validation::semantic_representation_compatible(&p, work, work),
|
|
Some(true)
|
|
);
|
|
assert_eq!(
|
|
validation::semantic_representation_compatible(&p, target, work),
|
|
Some(false)
|
|
);
|
|
let port = Id::new_v4();
|
|
p.elements.insert(
|
|
port,
|
|
Element {
|
|
id: port,
|
|
alias: "legacy_controller_port".into(),
|
|
name: "Controller port".into(),
|
|
description: String::new(),
|
|
parent: Some(entity),
|
|
body: ElementBody::shared_in(
|
|
Layer::Oa,
|
|
SharedBody::Port {
|
|
contract: PortContract::Legacy,
|
|
legacy: Some(LegacyPortContract {
|
|
direction: PortDirection::InOut,
|
|
interface: None,
|
|
}),
|
|
},
|
|
),
|
|
},
|
|
);
|
|
assert_eq!(
|
|
validation::semantic_representation_compatible(&p, entity, port),
|
|
Some(true)
|
|
);
|
|
assert_eq!(
|
|
validation::semantic_representation_compatible(&p, role, port),
|
|
Some(true)
|
|
);
|
|
assert_eq!(
|
|
validation::semantic_representation_compatible(&p, other, port),
|
|
Some(false)
|
|
);
|
|
let mut wrong = p.clone();
|
|
*wrong.elements.get_mut(&a).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(wrong_role),
|
|
};
|
|
assert!(
|
|
validation::validate(&wrong)
|
|
.iter()
|
|
.any(|i| i.code == "scenario_endpoint" && i.severity == Severity::Error)
|
|
);
|
|
p.relations.remove(&allocation);
|
|
let additional = p
|
|
.add_element(Kind::Entity, Layer::Oa, "Alternative coordinator", None)
|
|
.unwrap();
|
|
p.relate(
|
|
RelationKind::RoleRealization,
|
|
"Another coordinator realization",
|
|
additional,
|
|
role,
|
|
);
|
|
assert_eq!(validation::effective_performer(&p, work), None);
|
|
assert_eq!(
|
|
validation::lifeline_matches_endpoint(&p, a, work),
|
|
Some(true)
|
|
);
|
|
assert_eq!(
|
|
validation::semantic_representation_compatible(&p, entity, work),
|
|
None
|
|
);
|
|
valid(&p);
|
|
}
|
|
|
|
#[test]
|
|
fn effective_message_endpoints_are_shared_for_forward_and_reverse_reply_choices() {
|
|
let Fixture {
|
|
mut p,
|
|
a,
|
|
b,
|
|
call,
|
|
reply,
|
|
exchange,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
let body = |p: &Project, id| match p.elements[&id].shared().unwrap() {
|
|
SharedBody::Occurrence { body, .. } => body.clone(),
|
|
_ => unreachable!(),
|
|
};
|
|
let (source, target) = p.relations[&exchange].body.endpoints();
|
|
assert_eq!(
|
|
validation::message_endpoints(&p, &body(&p, call)),
|
|
Some((source, target))
|
|
);
|
|
let reversed = validation::message_endpoints(&p, &body(&p, reply)).unwrap();
|
|
assert_eq!(reversed, (target, source));
|
|
assert_eq!(
|
|
validation::lifeline_matches_endpoint(&p, b, reversed.0),
|
|
Some(true)
|
|
);
|
|
assert_eq!(
|
|
validation::lifeline_matches_endpoint(&p, a, reversed.0),
|
|
Some(false)
|
|
);
|
|
let reverse = p.relate(
|
|
RelationKind::FunctionalExchange,
|
|
"Explicit reply exchange",
|
|
target,
|
|
source,
|
|
);
|
|
let explicit = message(Some(reverse), b, Some(a), MessageKind::Reply, Some(call));
|
|
assert_eq!(
|
|
validation::message_endpoints(&p, &explicit),
|
|
Some((target, source))
|
|
);
|
|
assert_eq!(
|
|
validation::message_endpoints(
|
|
&p,
|
|
&message(Some(exchange), b, Some(a), MessageKind::Reply, None)
|
|
),
|
|
None
|
|
);
|
|
assert_eq!(
|
|
validation::message_endpoints(
|
|
&p,
|
|
&message(
|
|
None,
|
|
b,
|
|
None,
|
|
MessageKind::Timer {
|
|
expression: "5 s".into()
|
|
},
|
|
None
|
|
)
|
|
),
|
|
None
|
|
);
|
|
}
|
|
|
|
fn move_into_operand(p: &mut Project, id: Id, operand: Id) {
|
|
let old = p.elements[&id].parent.unwrap();
|
|
p.elements
|
|
.get_mut(&old)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.retain(|step| *step != id);
|
|
p.elements.get_mut(&id).unwrap().parent = Some(operand);
|
|
p.elements
|
|
.get_mut(&operand)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(id);
|
|
}
|
|
|
|
#[test]
|
|
fn replies_repeated_without_their_call_warn_in_each_extra_loop_scope() {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
call,
|
|
reply,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
let (_, outer) = scenario_branch(&mut p, scenario, FragmentOperator::Loop);
|
|
move_into_operand(&mut p, reply, outer);
|
|
valid(&p);
|
|
assert!(
|
|
validation::validate(&p)
|
|
.iter()
|
|
.any(|i| i.element == Some(reply)
|
|
&& i.code == "reply_repetition"
|
|
&& i.severity == Severity::Warning)
|
|
);
|
|
// Moving both into the same loop gives each iteration its own call and reply.
|
|
move_into_operand(&mut p, call, outer);
|
|
p.elements
|
|
.get_mut(&outer)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.swap(0, 1);
|
|
valid(&p);
|
|
assert!(
|
|
!validation::validate(&p)
|
|
.iter()
|
|
.any(|i| i.code == "reply_repetition")
|
|
);
|
|
// An inner loop can still repeat the reply more often than the outer call.
|
|
let (_, inner) = scenario_branch(&mut p, outer, FragmentOperator::Loop);
|
|
move_into_operand(&mut p, reply, inner);
|
|
valid(&p);
|
|
assert_eq!(
|
|
validation::validate(&p)
|
|
.iter()
|
|
.filter(|i| i.code == "reply_repetition")
|
|
.count(),
|
|
1
|
|
);
|
|
move_into_operand(&mut p, call, inner);
|
|
p.elements
|
|
.get_mut(&inner)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.swap(0, 1);
|
|
valid(&p);
|
|
assert!(
|
|
!validation::validate(&p)
|
|
.iter()
|
|
.any(|i| i.code == "reply_repetition")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_lifecycle_requires_per_iteration_balance_without_prohibiting_balanced_loops() {
|
|
for starts_with_creation in [false, true] {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
call,
|
|
reply,
|
|
exchange,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
for id in [call, reply] {
|
|
p.elements.remove(&id);
|
|
}
|
|
p.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.clear();
|
|
let (_, operand) = scenario_branch(&mut p, scenario, FragmentOperator::Loop);
|
|
let lifecycle = if starts_with_creation {
|
|
add_occurrence(
|
|
&mut p,
|
|
operand,
|
|
"Create each iteration",
|
|
message(Some(exchange), a, Some(b), MessageKind::Create, None),
|
|
)
|
|
} else {
|
|
Id::nil()
|
|
};
|
|
add_occurrence(
|
|
&mut p,
|
|
operand,
|
|
"Use provider",
|
|
message(Some(exchange), a, Some(b), MessageKind::Asynchronous, None),
|
|
);
|
|
let lifecycle = if starts_with_creation {
|
|
lifecycle
|
|
} else {
|
|
add_occurrence(
|
|
&mut p,
|
|
operand,
|
|
"Delete each iteration",
|
|
message(Some(exchange), a, Some(b), MessageKind::Delete, None),
|
|
)
|
|
};
|
|
valid(&p);
|
|
assert!(
|
|
validation::validate(&p)
|
|
.iter()
|
|
.any(|i| i.element == Some(lifecycle)
|
|
&& i.code == "lifeline_repetition"
|
|
&& i.severity == Severity::Warning)
|
|
);
|
|
if starts_with_creation {
|
|
add_occurrence(
|
|
&mut p,
|
|
operand,
|
|
"Complete lifetime",
|
|
message(Some(exchange), a, Some(b), MessageKind::Delete, None),
|
|
);
|
|
} else {
|
|
add_occurrence(
|
|
&mut p,
|
|
operand,
|
|
"Start lifetime",
|
|
message(Some(exchange), a, Some(b), MessageKind::Create, None),
|
|
);
|
|
p.elements
|
|
.get_mut(&operand)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.rotate_right(1);
|
|
}
|
|
valid(&p);
|
|
assert!(
|
|
!validation::validate(&p)
|
|
.iter()
|
|
.any(|i| i.code == "lifeline_repetition")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn optional_cleanup_does_not_claim_a_repeated_lifetime_is_balanced() {
|
|
let Fixture {
|
|
mut p,
|
|
scenario,
|
|
a,
|
|
b,
|
|
call,
|
|
reply,
|
|
exchange,
|
|
..
|
|
} = fixture(Layer::Sa);
|
|
for id in [call, reply] {
|
|
p.elements.remove(&id);
|
|
}
|
|
p.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.clear();
|
|
let (_, iteration) = scenario_branch(&mut p, scenario, FragmentOperator::Loop);
|
|
let create = add_occurrence(
|
|
&mut p,
|
|
iteration,
|
|
"Create provider",
|
|
message(Some(exchange), a, Some(b), MessageKind::Create, None),
|
|
);
|
|
add_occurrence(
|
|
&mut p,
|
|
iteration,
|
|
"Use provider",
|
|
message(Some(exchange), a, Some(b), MessageKind::Asynchronous, None),
|
|
);
|
|
let (_, cleanup) = scenario_branch(&mut p, iteration, FragmentOperator::Optional);
|
|
add_occurrence(
|
|
&mut p,
|
|
cleanup,
|
|
"Optional deletion",
|
|
message(Some(exchange), a, Some(b), MessageKind::Delete, None),
|
|
);
|
|
valid(&p);
|
|
assert!(
|
|
validation::validate(&p)
|
|
.iter()
|
|
.any(|i| i.element == Some(create) && i.code == "lifeline_repetition")
|
|
);
|
|
}
|