875 lines
27 KiB
Rust
875 lines
27 KiB
Rust
//! Direct core fixture/oracle helper; no native input or accepted-file mutation.
|
|
use oa_core::{transitions::*, view_options::ViewOptions, *};
|
|
use std::{
|
|
collections::{BTreeMap, BTreeSet},
|
|
fs,
|
|
path::Path,
|
|
};
|
|
|
|
fn id(n: u32) -> Id {
|
|
format!("c6060906-0000-4000-8000-{n:012x}").parse().unwrap()
|
|
}
|
|
fn element(
|
|
p: &mut Project,
|
|
n: u32,
|
|
kind: Kind,
|
|
layer: Layer,
|
|
name: &str,
|
|
parent: Option<Id>,
|
|
) -> Id {
|
|
let key = id(n);
|
|
p.elements.insert(
|
|
key,
|
|
Element {
|
|
id: key,
|
|
alias: format!("cm_{n}"),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
parent,
|
|
body: if kind == Kind::Port {
|
|
ElementBody::shared_in(
|
|
layer,
|
|
SharedBody::Port {
|
|
contract: PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
},
|
|
legacy: None,
|
|
},
|
|
)
|
|
} else {
|
|
ElementBody::new(kind, layer).unwrap()
|
|
},
|
|
},
|
|
);
|
|
key
|
|
}
|
|
fn relation(p: &mut Project, n: u32, kind: RelationKind, name: &str, source: Id, target: Id) -> Id {
|
|
let key = id(n);
|
|
p.relations.insert(
|
|
key,
|
|
Relation {
|
|
id: key,
|
|
alias: format!("cm_{n}"),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
body: RelationBody::new(kind, source, target),
|
|
},
|
|
);
|
|
key
|
|
}
|
|
fn trace(p: &mut Project, n: u32, oa: Id, sa: Id, kind: TraceKind, name: &str) {
|
|
p.traces.insert(
|
|
id(n),
|
|
Trace {
|
|
id: id(n),
|
|
alias: format!("cm_{n}"),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
oa,
|
|
sa,
|
|
kind,
|
|
baseline: None,
|
|
},
|
|
);
|
|
}
|
|
fn diagram(p: &mut Project, n: u32, layer: Layer, name: &str, rows: &[(Id, f32, f32, f32, f32)]) {
|
|
let nodes = rows
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, &(element, x, y, width, height))| {
|
|
let key = id(n + 10 + i as u32);
|
|
(
|
|
key,
|
|
NodePlacement {
|
|
id: key,
|
|
alias: format!("cm_view_{}", n + i as u32),
|
|
element,
|
|
position: Position { x, y },
|
|
size: Size { width, height },
|
|
collapsed: false,
|
|
port_anchors: vec![],
|
|
},
|
|
)
|
|
})
|
|
.collect();
|
|
p.diagrams.insert(
|
|
id(n),
|
|
Diagram {
|
|
id: id(n),
|
|
alias: format!("cm_diagram_{n}"),
|
|
name: name.into(),
|
|
description: String::new(),
|
|
layer,
|
|
projection: ProjectionKind::Overview,
|
|
view_options: ViewOptions::default(),
|
|
nodes,
|
|
routes: vec![],
|
|
},
|
|
);
|
|
}
|
|
fn port(p: &mut Project, n: u32, owner: Id, name: &str) -> Id {
|
|
let key = element(p, n, Kind::Port, Layer::Sa, name, Some(owner));
|
|
if let Some(SharedBody::Port { contract, .. }) = p.elements.get_mut(&key).unwrap().shared_mut()
|
|
{
|
|
*contract = PortContract::Component {
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
};
|
|
} else {
|
|
panic!("port");
|
|
}
|
|
key
|
|
}
|
|
fn base() -> Project {
|
|
let mut p = Project::empty();
|
|
p.id = id(1);
|
|
p.alias = "communication_native".into();
|
|
p.name = "Rescue communication engineering".into();
|
|
let a = element(&mut p, 10, Kind::Entity, Layer::Oa, "Rescue team", None);
|
|
let b = element(&mut p, 11, Kind::Actor, Layer::Oa, "Control centre", None);
|
|
let x = element(
|
|
&mut p,
|
|
12,
|
|
Kind::Activity,
|
|
Layer::Oa,
|
|
"Report rescue status",
|
|
None,
|
|
);
|
|
let y = element(
|
|
&mut p,
|
|
13,
|
|
Kind::Activity,
|
|
Layer::Oa,
|
|
"Assess rescue status",
|
|
None,
|
|
);
|
|
relation(
|
|
&mut p,
|
|
20,
|
|
RelationKind::Allocation,
|
|
"Team performs reporting",
|
|
a,
|
|
x,
|
|
);
|
|
relation(
|
|
&mut p,
|
|
21,
|
|
RelationKind::Allocation,
|
|
"Control performs assessment",
|
|
b,
|
|
y,
|
|
);
|
|
relation(&mut p, 22, RelationKind::Interaction, "Rescue status", x, y);
|
|
let sa = element(&mut p, 30, Kind::System, Layer::Sa, "Rescue radio", None);
|
|
let sb = element(
|
|
&mut p,
|
|
31,
|
|
Kind::SystemActor,
|
|
Layer::Sa,
|
|
"Control radio",
|
|
None,
|
|
);
|
|
port(&mut p, 32, sa, "Existing team socket");
|
|
port(&mut p, 33, sb, "Existing control socket");
|
|
trace(
|
|
&mut p,
|
|
40,
|
|
a,
|
|
sa,
|
|
TraceKind::Participant,
|
|
"Team radio correspondence",
|
|
);
|
|
trace(
|
|
&mut p,
|
|
41,
|
|
b,
|
|
sb,
|
|
TraceKind::Participant,
|
|
"Control radio correspondence",
|
|
);
|
|
diagram(
|
|
&mut p,
|
|
1000,
|
|
Layer::Oa,
|
|
"Draw operational communication",
|
|
&[
|
|
(a, 80., 70., 250., 160.),
|
|
(b, 550., 70., 250., 160.),
|
|
(x, 80., 330., 250., 110.),
|
|
(y, 550., 330., 250., 110.),
|
|
],
|
|
);
|
|
diagram(
|
|
&mut p,
|
|
1100,
|
|
Layer::Sa,
|
|
"Explicit system endpoints",
|
|
&[(sa, 80., 140., 250., 170.), (sb, 550., 140., 250., 170.)],
|
|
);
|
|
valid(&p);
|
|
p
|
|
}
|
|
fn valid(p: &Project) {
|
|
validate_candidate(p).unwrap();
|
|
assert_eq!(language::parse(&language::format(p)).unwrap(), *p);
|
|
assert_eq!(
|
|
persistence::decode(&serde_json::to_vec(p).unwrap()).unwrap(),
|
|
*p
|
|
);
|
|
}
|
|
fn forward(p: &Project, mean: Id, create: Option<Id>) -> TransitionRequest {
|
|
let mut r = TransitionRequest::new(TransitionDirection::OaToSa, vec![mean]);
|
|
r.mappings = vec![
|
|
TransitionMapping {
|
|
source: id(10),
|
|
target: Some(id(30)),
|
|
target_kind: None,
|
|
},
|
|
TransitionMapping {
|
|
source: id(11),
|
|
target: Some(id(31)),
|
|
target_kind: None,
|
|
},
|
|
];
|
|
r.resolutions = r
|
|
.mappings
|
|
.iter()
|
|
.map(|m| FieldResolution {
|
|
source: m.source,
|
|
field: TransitionField::Name,
|
|
decision: FieldDecision::KeepTarget,
|
|
})
|
|
.collect();
|
|
let target_port = if let Some(key) = create {
|
|
let name = format!("{} · target port", p.named(mean));
|
|
CommunicationPortChoice::Create {
|
|
id: key,
|
|
alias: p.fresh_alias(&name, "port", key),
|
|
name,
|
|
description: String::new(),
|
|
kind: ComponentPortKind::Standard,
|
|
orientation: ComponentPortOrientation::Unset,
|
|
}
|
|
} else {
|
|
CommunicationPortChoice::Reuse { port: id(33) }
|
|
};
|
|
r.communication_endpoints
|
|
.push(CommunicationEndpointDecision {
|
|
source: mean,
|
|
source_port: CommunicationPortChoice::Reuse { port: id(32) },
|
|
target_port,
|
|
});
|
|
r
|
|
}
|
|
fn apply(p: &Project, r: &TransitionRequest) -> Project {
|
|
let plan = plan_transition(p, r).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let mut q = apply_transition(p, &plan).unwrap();
|
|
// Desktop Workflow::Transition::Apply adds presentation after the core
|
|
// transition. Predict its policy from the plan and prior diagram, never
|
|
// adopt placement coordinates/size/alias from the observed native result.
|
|
let diagram = r
|
|
.destination_diagram
|
|
.or_else(|| {
|
|
q.diagrams
|
|
.values()
|
|
.find(|d| d.layer == r.direction.target_layer())
|
|
.map(|d| d.id)
|
|
})
|
|
.expect("These fixtures provide an existing destination-layer diagram");
|
|
for change in &plan.changes {
|
|
if q.elements.contains_key(&change.target) {
|
|
let count = q.diagrams[&diagram].nodes.len();
|
|
q.place(
|
|
diagram,
|
|
change.target,
|
|
Position {
|
|
x: 60. + (count % 3) as f32 * 280.,
|
|
y: 60. + (count / 3) as f32 * 150.,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
valid(&q);
|
|
q
|
|
}
|
|
// Separate policy oracle: the explicit scenario root opens in Behavior, so no
|
|
// architecture placement is requested. Keep apply() above for the legacy binary.
|
|
fn apply_reverse_behavior(p: &Project, r: &TransitionRequest) -> Project {
|
|
assert_eq!(r.sources, vec![id(200)]);
|
|
assert_eq!(p.elements[&id(200)].kind(), Kind::Scenario);
|
|
assert!(r.destination_diagram.is_none());
|
|
let plan = plan_transition(p, r).unwrap();
|
|
assert!(plan.conflicts.is_empty(), "{:?}", plan.conflicts);
|
|
let q = apply_transition(p, &plan).unwrap();
|
|
assert_eq!(
|
|
q.diagrams, p.diagrams,
|
|
"Behavior must preserve every saved view"
|
|
);
|
|
reverse_assertions(p, &q);
|
|
valid(&q);
|
|
q
|
|
}
|
|
|
|
fn generate_reverse_behavior(before: &Path, out: &Path) {
|
|
let disk =
|
|
source_files::load_project(before, source_project::ProjectLimits::default()).unwrap();
|
|
let compiled = source_project::compile_bundle(
|
|
&disk.reachable_bundle(),
|
|
source_project::ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
let p = &compiled.decoded.project;
|
|
let request = reverse(p);
|
|
let q = deterministic(p, apply_reverse_behavior(p, &request));
|
|
let updated =
|
|
source_project::update_bundle(&compiled, &q, source_project::ProjectLimits::default())
|
|
.unwrap();
|
|
fs::create_dir_all(out).unwrap();
|
|
write_bundle(out, &updated.bundle);
|
|
fs::write(
|
|
out.join("expected.json"),
|
|
serde_json::to_vec_pretty(&q).unwrap(),
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
out.join("request.json"),
|
|
serde_json::to_vec_pretty(&request).unwrap(),
|
|
)
|
|
.unwrap();
|
|
println!("PASS generate-reverse-behavior: core candidate, all saved views unchanged");
|
|
}
|
|
// Only actual identity fields and references are remapped, never names/text/data.
|
|
fn remap(mut p: Project, mappings: &BTreeMap<Id, Id>) -> Project {
|
|
for (&old, &new) in mappings {
|
|
if old == new {
|
|
continue;
|
|
}
|
|
assert!(!p.contains(new));
|
|
references::remap_references(&mut p, old, new);
|
|
if let Some(mut e) = p.elements.remove(&old) {
|
|
e.id = new;
|
|
p.elements.insert(new, e);
|
|
}
|
|
if let Some(mut e) = p.relations.remove(&old) {
|
|
e.id = new;
|
|
p.relations.insert(new, e);
|
|
}
|
|
if let Some(mut e) = p.traces.remove(&old) {
|
|
e.id = new;
|
|
p.traces.insert(new, e);
|
|
}
|
|
if let Some(mut e) = p.artifact_links.remove(&old) {
|
|
e.id = new;
|
|
p.artifact_links.insert(new, e);
|
|
}
|
|
if let Some(mut e) = p.diagrams.remove(&old) {
|
|
e.id = new;
|
|
p.diagrams.insert(new, e);
|
|
}
|
|
for d in p.diagrams.values_mut() {
|
|
if let Some(mut e) = d.nodes.remove(&old) {
|
|
e.id = new;
|
|
d.nodes.insert(new, e);
|
|
}
|
|
}
|
|
}
|
|
valid(&p);
|
|
p
|
|
}
|
|
fn deterministic(base: &Project, q: Project) -> Project {
|
|
let mut fresh: Vec<_> = q
|
|
.identities()
|
|
.into_iter()
|
|
.filter(|(key, _)| !base.contains(*key))
|
|
.collect();
|
|
fresh.sort_by(|a, b| a.1.cmp(&b.1));
|
|
let mut next = 8000;
|
|
let map = fresh
|
|
.into_iter()
|
|
.map(|(old, _)| {
|
|
while base.contains(id(next)) {
|
|
next += 1;
|
|
}
|
|
let new = id(next);
|
|
next += 1;
|
|
(old, new)
|
|
})
|
|
.collect();
|
|
remap(q, &map)
|
|
}
|
|
fn emit(dir: &Path, name: &str, p: &Project) {
|
|
valid(p);
|
|
fs::write(dir.join(format!("{name}.arcadia")), language::format(p)).unwrap();
|
|
fs::write(
|
|
dir.join(format!("{name}.json")),
|
|
serde_json::to_vec_pretty(p).unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
fn emit_after(dir: &Path, name: &str, base: &Project, after: &Project) {
|
|
let entry = "entry.arcadia".to_string();
|
|
let bundle = source_project::SourceBundle {
|
|
entry: entry.clone(),
|
|
files: BTreeMap::from([(entry.clone(), language::format(base))]),
|
|
};
|
|
let compiled =
|
|
source_project::compile_bundle(&bundle, source_project::ProjectLimits::default()).unwrap();
|
|
let updated =
|
|
source_project::update_bundle(&compiled, after, source_project::ProjectLimits::default())
|
|
.unwrap();
|
|
let output = dir.join(name);
|
|
write_bundle(&output, &updated.bundle);
|
|
fs::write(
|
|
output.join("expected.json"),
|
|
serde_json::to_vec_pretty(after).unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
fn write_bundle(dir: &Path, bundle: &source_project::SourceBundle) {
|
|
fs::create_dir_all(dir).unwrap();
|
|
for (name, text) in &bundle.files {
|
|
let path = dir.join(name);
|
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
fs::write(path, text).unwrap();
|
|
}
|
|
}
|
|
fn scenario(p: &mut Project, boundary: Id) -> Id {
|
|
let s = element(
|
|
p,
|
|
200,
|
|
Kind::Scenario,
|
|
Layer::Sa,
|
|
"Reverse rescue request",
|
|
None,
|
|
);
|
|
if let Some(SharedBody::Scenario { viewpoint, .. }) =
|
|
p.elements.get_mut(&s).unwrap().shared_mut()
|
|
{
|
|
*viewpoint = ScenarioViewpoint::BoundaryInterface;
|
|
}
|
|
let a = element(
|
|
p,
|
|
201,
|
|
Kind::Lifeline,
|
|
Layer::Sa,
|
|
"Team radio lifeline",
|
|
Some(s),
|
|
);
|
|
let b = element(
|
|
p,
|
|
202,
|
|
Kind::Lifeline,
|
|
Layer::Sa,
|
|
"Control radio lifeline",
|
|
Some(s),
|
|
);
|
|
for (line, owner) in [(a, id(30)), (b, id(31))] {
|
|
*p.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(owner),
|
|
};
|
|
}
|
|
for (n, name, sender, receiver, kind, reply_to) in [
|
|
(203, "Request rescue report", b, a, MessageKind::Call, None),
|
|
(
|
|
204,
|
|
"Reply with rescue report",
|
|
a,
|
|
b,
|
|
MessageKind::Reply,
|
|
Some(id(203)),
|
|
),
|
|
] {
|
|
let e = element(p, n, Kind::Occurrence, Layer::Sa, name, Some(s));
|
|
*p.elements.get_mut(&e).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
exchange: Some(boundary),
|
|
sender: Some(sender),
|
|
receiver: Some(receiver),
|
|
message_kind: kind,
|
|
reply_to,
|
|
values: vec![],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
}
|
|
*p.elements.get_mut(&s).unwrap().steps_mut().unwrap() = vec![id(203), id(204)];
|
|
valid(p);
|
|
s
|
|
}
|
|
fn reverse(p: &Project) -> TransitionRequest {
|
|
let mean = id(50);
|
|
let boundary = p.traces.values().find(|t| t.oa == mean).unwrap().sa;
|
|
let mut r = TransitionRequest::new(TransitionDirection::SaToOa, vec![id(200)]);
|
|
r.scope = DependencyScope::Subtree;
|
|
r.mappings = [(boundary, mean), (id(30), id(10)), (id(31), id(11))]
|
|
.into_iter()
|
|
.map(|(source, target)| TransitionMapping {
|
|
source,
|
|
target: Some(target),
|
|
target_kind: None,
|
|
})
|
|
.collect();
|
|
r.resolutions = r
|
|
.mappings
|
|
.iter()
|
|
.map(|m| FieldResolution {
|
|
source: m.source,
|
|
field: TransitionField::Name,
|
|
decision: FieldDecision::KeepTarget,
|
|
})
|
|
.collect();
|
|
r
|
|
}
|
|
fn reverse_assertions(p: &Project, q: &Project) {
|
|
use validation::MessageEndpointResolution as R;
|
|
let mapped = |source| q.traces.values().find(|t| t.sa == source).unwrap().oa;
|
|
let body = |id| match q.elements[&id].shared().unwrap() {
|
|
SharedBody::Occurrence { body, .. } => body,
|
|
_ => panic!(),
|
|
};
|
|
let call = mapped(id(203));
|
|
let reply = mapped(id(204));
|
|
assert_eq!(
|
|
validation::resolve_message_endpoints(q, body(call)),
|
|
R::Resolved {
|
|
source: id(11),
|
|
target: id(10)
|
|
}
|
|
);
|
|
assert_eq!(
|
|
validation::resolve_message_endpoints(q, body(reply)),
|
|
R::Resolved {
|
|
source: id(10),
|
|
target: id(11)
|
|
}
|
|
);
|
|
assert!(matches!(body(reply),OccurrenceBody::Exchange{reply_to:Some(id),..} if *id==call));
|
|
for key in [id(200), id(201), id(202), id(203), id(204)] {
|
|
assert_eq!(p.elements[&key], q.elements[&key]);
|
|
}
|
|
assert_eq!(
|
|
q.elements
|
|
.values()
|
|
.filter(|e| e.layer() == Layer::Oa && e.kind() == Kind::Port)
|
|
.count(),
|
|
0
|
|
);
|
|
}
|
|
fn generate(dir: &Path) {
|
|
fs::create_dir_all(dir).unwrap();
|
|
let p = base();
|
|
emit(dir, "cm-draw", &p);
|
|
let mut ready = p.clone();
|
|
relation(
|
|
&mut ready,
|
|
50,
|
|
RelationKind::CommunicationMean,
|
|
"Rescue radio channel",
|
|
id(10),
|
|
id(11),
|
|
);
|
|
valid(&ready);
|
|
emit(dir, "cm-transition-ready", &ready);
|
|
for (label, created) in [("reuse", None), ("create", Some(id(60)))] {
|
|
let r = forward(&ready, id(50), created);
|
|
let q = apply(&ready, &r);
|
|
let q = deterministic(&ready, q);
|
|
emit_after(dir, &format!("cm-{label}.after-template"), &ready, &q);
|
|
fs::write(
|
|
dir.join(format!("cm-{label}.request.json")),
|
|
serde_json::to_vec_pretty(&r).unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
let mut p = deterministic(&ready, apply(&ready, &forward(&ready, id(50), None)));
|
|
let boundary = p.traces.values().find(|t| t.oa == id(50)).unwrap().sa;
|
|
scenario(&mut p, boundary);
|
|
emit(dir, "cm-reverse-scenario", &p);
|
|
let r = reverse(&p);
|
|
let q = apply(&p, &r);
|
|
reverse_assertions(&p, &q);
|
|
let q = deterministic(&p, q);
|
|
emit_after(dir, "cm-reverse-scenario.after-template", &p, &q);
|
|
fs::write(
|
|
dir.join("cm-reverse-scenario.request.json"),
|
|
serde_json::to_vec_pretty(&r).unwrap(),
|
|
)
|
|
.unwrap();
|
|
let manifest = serde_json::json!({"schema":6,"native_status":"pending","project":id(1),"oa_participants":[id(10),id(11)],"oa_activities":[id(12),id(13)],"interaction":id(22),"sa_participants":[id(30),id(31)],"existing_ports":[id(32),id(33)],"participant_traces":[id(40),id(41)],"draw_diagram":id(1000),"system_diagram":id(1100),"template_mean":id(50),"reverse_scenario":id(200),"reverse_call":id(203),"correlated_reply":id(204),"templates":"Generated target/trace UUIDs are reference examples, not native exact expectations. Use check with native before/after sources."});
|
|
fs::write(
|
|
dir.join("manifest.json"),
|
|
serde_json::to_vec_pretty(&manifest).unwrap(),
|
|
)
|
|
.unwrap();
|
|
}
|
|
fn check_draw(
|
|
before: &source_project::CompiledSourceProject,
|
|
actual: &source_project::CompiledSourceProject,
|
|
out: &Path,
|
|
) {
|
|
let p = &before.decoded.project;
|
|
assert!(
|
|
!p.relations
|
|
.values()
|
|
.any(|r| r.kind() == RelationKind::CommunicationMean)
|
|
);
|
|
let means: Vec<_> = actual
|
|
.decoded
|
|
.project
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.kind() == RelationKind::CommunicationMean)
|
|
.collect();
|
|
assert_eq!(
|
|
means.len(),
|
|
1,
|
|
"Drawing creates exactly one communication means"
|
|
);
|
|
let identity = means[0].id;
|
|
assert!(!p.contains(identity));
|
|
let name = format!(
|
|
"Communication means: {} — {}",
|
|
p.named(id(10)),
|
|
p.named(id(11))
|
|
);
|
|
let drawn = Relation {
|
|
id: identity,
|
|
alias: p.fresh_alias(&name, "relation", identity),
|
|
name,
|
|
description: String::new(),
|
|
body: RelationBody::CommunicationMean {
|
|
source: id(10),
|
|
target: id(11),
|
|
oriented: false,
|
|
items: vec![],
|
|
},
|
|
};
|
|
let create = Command::CreateRelation(drawn.clone());
|
|
let after_draw = execute_batch(p, std::slice::from_ref(&create)).unwrap();
|
|
let draw_source = source_project::update_bundle(
|
|
before,
|
|
&after_draw,
|
|
source_project::ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
let mut renamed = drawn;
|
|
renamed.name = "Rescue radio channel".into();
|
|
let rename = Command::EditRelation(identity, renamed);
|
|
let expected = execute_batch(&after_draw, std::slice::from_ref(&rename)).unwrap();
|
|
let expected_source = source_project::update_bundle(
|
|
&draw_source,
|
|
&expected,
|
|
source_project::ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(
|
|
expected, actual.decoded.project,
|
|
"Draw+rename full model differs; only actual new relation UUID was supplied"
|
|
);
|
|
let mut actual_files = actual.bundle.files.clone();
|
|
let entry = actual_files.remove(&actual.bundle.entry).unwrap();
|
|
assert!(
|
|
actual_files
|
|
.insert(expected_source.bundle.entry.clone(), entry)
|
|
.is_none()
|
|
);
|
|
assert_eq!(
|
|
expected_source.bundle.files, actual_files,
|
|
"Draw+rename complete source bytes differ (entry filename only aligned)"
|
|
);
|
|
write_bundle(out, &expected_source.bundle);
|
|
fs::write(
|
|
out.join("expected-draw.json"),
|
|
serde_json::to_vec_pretty(&expected).unwrap(),
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
out.join("draw-rename-commands.json"),
|
|
serde_json::to_vec_pretty(&[create, rename]).unwrap(),
|
|
)
|
|
.unwrap();
|
|
println!(
|
|
"PASS draw: typed CreateRelation then EditRelation, exact full model/source, only new relation UUID {identity}"
|
|
);
|
|
}
|
|
fn check(mode: &str, before: &Path, after: &Path, out: &Path) {
|
|
let before_disk =
|
|
source_files::load_project(before, source_project::ProjectLimits::default()).unwrap();
|
|
let actual_disk =
|
|
source_files::load_project(after, source_project::ProjectLimits::default()).unwrap();
|
|
let compiled = source_project::compile_bundle(
|
|
&before_disk.reachable_bundle(),
|
|
source_project::ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
let actual_compiled = source_project::compile_bundle(
|
|
&actual_disk.reachable_bundle(),
|
|
source_project::ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
let p = compiled.decoded.project.clone();
|
|
let actual = actual_compiled.decoded.project.clone();
|
|
valid(&p);
|
|
valid(&actual);
|
|
if mode == "draw" {
|
|
return check_draw(&compiled, &actual_compiled, out);
|
|
}
|
|
let r = if matches!(mode, "reverse" | "reverse-behavior") {
|
|
reverse(&p)
|
|
} else {
|
|
let means: Vec<_> = p
|
|
.relations
|
|
.values()
|
|
.filter(|r| r.kind() == RelationKind::CommunicationMean)
|
|
.collect();
|
|
assert_eq!(means.len(), 1);
|
|
let mean = means[0].id;
|
|
let created = if mode == "create" {
|
|
let trace = actual.traces.values().find(|t| t.oa == mean).unwrap();
|
|
let key = actual.relations[&trace.sa].target();
|
|
assert!(!p.contains(key));
|
|
Some(key)
|
|
} else {
|
|
assert_eq!(mode, "reuse");
|
|
None
|
|
};
|
|
forward(&p, mean, created)
|
|
};
|
|
let mut expected = if mode == "reverse-behavior" {
|
|
apply_reverse_behavior(&p, &r)
|
|
} else {
|
|
apply(&p, &r)
|
|
};
|
|
let mut map = BTreeMap::new();
|
|
let mut actual_used = BTreeSet::new();
|
|
for t in expected
|
|
.traces
|
|
.values()
|
|
.filter(|t| !p.traces.contains_key(&t.id))
|
|
{
|
|
let source = if r.direction == TransitionDirection::OaToSa {
|
|
t.oa
|
|
} else {
|
|
t.sa
|
|
};
|
|
let counterparts: Vec<_> = actual
|
|
.traces
|
|
.values()
|
|
.filter(|a| {
|
|
if r.direction == TransitionDirection::OaToSa {
|
|
a.oa == source
|
|
} else {
|
|
a.sa == source
|
|
}
|
|
})
|
|
.collect();
|
|
assert_eq!(
|
|
counterparts.len(),
|
|
1,
|
|
"each explicit source has one counterpart"
|
|
);
|
|
let a = counterparts[0];
|
|
assert!(!p.contains(a.id));
|
|
assert!(actual_used.insert(a.id));
|
|
map.insert(t.id, a.id);
|
|
let (old, new) = if r.direction == TransitionDirection::OaToSa {
|
|
(t.sa, a.sa)
|
|
} else {
|
|
(t.oa, a.oa)
|
|
};
|
|
if !p.contains(old) {
|
|
assert!(!p.contains(new));
|
|
assert!(actual_used.insert(new));
|
|
map.insert(old, new);
|
|
}
|
|
}
|
|
// Match fresh presentation identities by exact destination diagram + mapped
|
|
// element. Every other placement property remains independently expected.
|
|
let mut placement_count = 0;
|
|
for (diagram, view) in &expected.diagrams {
|
|
for node in view.nodes.values().filter(|node| !p.contains(node.id)) {
|
|
let mapped_element = map.get(&node.element).copied().unwrap_or(node.element);
|
|
let actual_nodes: Vec<_> = actual
|
|
.diagrams
|
|
.get(diagram)
|
|
.expect("Expected destination diagram remains")
|
|
.nodes
|
|
.values()
|
|
.filter(|n| n.element == mapped_element)
|
|
.collect();
|
|
assert_eq!(
|
|
actual_nodes.len(),
|
|
1,
|
|
"Each generated placement must identify its exact diagram and mapped element"
|
|
);
|
|
let actual_id = actual_nodes[0].id;
|
|
assert!(!p.contains(actual_id));
|
|
assert!(actual_used.insert(actual_id));
|
|
map.insert(node.id, actual_id);
|
|
placement_count += 1;
|
|
}
|
|
}
|
|
expected = remap(expected, &map);
|
|
if matches!(mode, "reverse" | "reverse-behavior") {
|
|
reverse_assertions(&p, &expected);
|
|
}
|
|
assert_eq!(
|
|
expected, actual,
|
|
"full model oracle differs after UUID-only normalization"
|
|
);
|
|
let updated = source_project::update_bundle(
|
|
&compiled,
|
|
&expected,
|
|
source_project::ProjectLimits::default(),
|
|
)
|
|
.unwrap();
|
|
let mut actual_files = actual_compiled.bundle.files.clone();
|
|
let actual_entry = actual_files.remove(&actual_compiled.bundle.entry).unwrap();
|
|
assert!(
|
|
actual_files
|
|
.insert(updated.bundle.entry.clone(), actual_entry)
|
|
.is_none()
|
|
);
|
|
assert_eq!(
|
|
updated.bundle.files, actual_files,
|
|
"complete exact source bundle differs (only entry filename normalized for Save As)"
|
|
);
|
|
fs::create_dir_all(out).unwrap();
|
|
write_bundle(out, &updated.bundle);
|
|
fs::write(
|
|
out.join("expected-native.json"),
|
|
serde_json::to_vec_pretty(&expected).unwrap(),
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
out.join("native-request.json"),
|
|
serde_json::to_vec_pretty(&r).unwrap(),
|
|
)
|
|
.unwrap();
|
|
println!(
|
|
"PASS {mode}: complete model and exact source; {} generated UUID substitutions only ({placement_count} placement identities)",
|
|
map.len()
|
|
);
|
|
}
|
|
fn main() {
|
|
let args: Vec<_> = std::env::args().collect();
|
|
match args.get(1).map(String::as_str) {
|
|
Some("generate") if args.len() == 3 => generate(Path::new(&args[2])),
|
|
Some("generate-reverse-behavior") if args.len() == 4 => {
|
|
generate_reverse_behavior(Path::new(&args[2]), Path::new(&args[3]))
|
|
}
|
|
Some("check") if args.len() == 6 => check(
|
|
&args[2],
|
|
Path::new(&args[3]),
|
|
Path::new(&args[4]),
|
|
Path::new(&args[5]),
|
|
),
|
|
_ => panic!(
|
|
"generate DIRECTORY | generate-reverse-behavior BEFORE.arcadia OUTPUT_DIRECTORY | check draw|reuse|create|reverse|reverse-behavior BEFORE.arcadia AFTER.arcadia OUTPUT_DIRECTORY"
|
|
),
|
|
}
|
|
}
|