Files
ARCADIA-Studio/crates/oa-core/tests/release_scenarios.rs
T
Codex e2ecfea69b
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Checkpoint source 7 workflows and in-progress diagram authoring redesign
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
2026-09-10 10:06:55 -07:00

1377 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 {
operation: None,
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")
);
}