Add desktop reset history checks and native wiring fixtures
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
//! Native UI wiring fixture/oracle. Expected output comes from candidate Layout,
|
||||
//! not from an independent geometry algorithm. Run only after parent source freeze.
|
||||
use oa_core::*;
|
||||
use serde_json::{Value, json};
|
||||
use std::{collections::BTreeMap, error::Error, fs, path::PathBuf};
|
||||
|
||||
fn id(n: u128) -> Id {
|
||||
Id::from_u128(700_000 + n)
|
||||
}
|
||||
fn at(x: f32, y: f32) -> Position {
|
||||
Position { x, y }
|
||||
}
|
||||
|
||||
fn function(p: &mut Project, n: u128, name: &str, parent: Option<Id>) -> Id {
|
||||
let old = p
|
||||
.add_element(Kind::Function, Layer::Sa, name, parent)
|
||||
.unwrap();
|
||||
reidentify(p, old, id(n)).unwrap();
|
||||
id(n)
|
||||
}
|
||||
|
||||
fn place(p: &mut Project, d: Id, n: u128, element: Id, position: Position, size: Size) {
|
||||
let old = p.place(d, element, position);
|
||||
reidentify(p, old, id(n)).unwrap();
|
||||
let node = p
|
||||
.diagrams
|
||||
.get_mut(&d)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(n))
|
||||
.unwrap();
|
||||
node.alias = format!("placement_{n}");
|
||||
node.size = size;
|
||||
}
|
||||
|
||||
fn exchange(p: &mut Project, n: u128, name: &str, source: Id, target: Id) -> Id {
|
||||
let request = FunctionalExchangeRequest {
|
||||
id: id(n),
|
||||
alias: format!("exchange_{n}"),
|
||||
name: name.into(),
|
||||
description: String::new(),
|
||||
source,
|
||||
target,
|
||||
source_port: PortChoice::Create,
|
||||
target_port: PortChoice::Create,
|
||||
items: vec![],
|
||||
};
|
||||
let commands = ports::plan_functional_exchange(p, &request).unwrap();
|
||||
*p = execute_batch(p, &commands).unwrap();
|
||||
id(n)
|
||||
}
|
||||
|
||||
fn route(edge: Id, waypoints: Vec<Position>, fraction: f32, offset: Position) -> EdgeRoute {
|
||||
EdgeRoute {
|
||||
edge,
|
||||
source_attachment: None,
|
||||
target_attachment: None,
|
||||
waypoints,
|
||||
label: Some(EdgeLabel { fraction, offset }),
|
||||
}
|
||||
}
|
||||
|
||||
fn original() -> (Project, Id, Id, Value) {
|
||||
let mut p = Project::empty();
|
||||
p.id = id(0);
|
||||
p.alias = "arrange_native_fixture".into();
|
||||
p.name = "Arrange route reset trial".into();
|
||||
let old = p.ensure_diagram(Layer::Sa);
|
||||
let main = id(1);
|
||||
reidentify(&mut p, old, main).unwrap();
|
||||
{
|
||||
let d = p.diagrams.get_mut(&main).unwrap();
|
||||
d.alias = "main_dispatch".into();
|
||||
d.name = "Main dispatch".into();
|
||||
d.projection = ProjectionKind::FunctionalDataflow;
|
||||
}
|
||||
let group = function(&mut p, 10, "Prepare report", None);
|
||||
let send = function(&mut p, 11, "Send report", Some(group));
|
||||
let check = function(&mut p, 12, "Check report", Some(group));
|
||||
let monitor = function(&mut p, 13, "Monitor report", None);
|
||||
let archive = function(&mut p, 14, "Archive report", None);
|
||||
let card = Size {
|
||||
width: 224.,
|
||||
height: 94.,
|
||||
};
|
||||
place(
|
||||
&mut p,
|
||||
main,
|
||||
20,
|
||||
group,
|
||||
at(40., 80.),
|
||||
Size {
|
||||
width: 400.,
|
||||
height: 420.,
|
||||
},
|
||||
);
|
||||
for (n, element, x, y) in [
|
||||
(21, send, 80., 160.),
|
||||
(22, check, 80., 330.),
|
||||
(23, monitor, 740., 80.),
|
||||
(24, archive, 740., 380.),
|
||||
] {
|
||||
place(&mut p, main, n, element, at(x, y), card);
|
||||
}
|
||||
let authored = exchange(&mut p, 100, "Verified report", send, monitor);
|
||||
exchange(&mut p, 101, "Report for checking", send, check);
|
||||
exchange(&mut p, 102, "Checked report archive", check, archive);
|
||||
let hidden = exchange(
|
||||
&mut p,
|
||||
103,
|
||||
"Filtered archive acknowledgement",
|
||||
monitor,
|
||||
archive,
|
||||
);
|
||||
let RelationBody::FunctionalExchange {
|
||||
source_port,
|
||||
target_port,
|
||||
..
|
||||
} = p.relations[&authored].body
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
for (node, port, side, offset) in [
|
||||
(id(21), source_port, PortAnchorSide::Right, 0.25),
|
||||
(id(23), target_port, PortAnchorSide::Left, 0.75),
|
||||
] {
|
||||
p = prepare_command(
|
||||
&p,
|
||||
&Command::SetPortAnchor {
|
||||
diagram: main,
|
||||
node,
|
||||
anchor: PortAnchor { port, side, offset },
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.project;
|
||||
}
|
||||
let mut visible = route(
|
||||
authored,
|
||||
vec![at(610., 210.), at(610., 420.)],
|
||||
0.5,
|
||||
at(0., -32.),
|
||||
);
|
||||
// Real semantic ports remain authoritative; these stored preferences are dormant.
|
||||
visible.source_attachment = Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 0.5,
|
||||
});
|
||||
visible.target_attachment = Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.25,
|
||||
});
|
||||
let filtered = route(hidden, vec![at(1020., 270.)], 0.75, at(16., -16.));
|
||||
p.diagrams.get_mut(&main).unwrap().routes = vec![visible, filtered];
|
||||
p.diagrams.get_mut(&main).unwrap().view_options.hidden_edges = vec![hidden];
|
||||
let second = id(2);
|
||||
p.diagrams.insert(
|
||||
second,
|
||||
Diagram {
|
||||
id: second,
|
||||
alias: "review_sentinel".into(),
|
||||
name: "Review sentinel".into(),
|
||||
description:
|
||||
"Independent other-view route; Arrange Main dispatch must retain this entire view"
|
||||
.into(),
|
||||
layer: Layer::Sa,
|
||||
projection: ProjectionKind::FunctionalDataflow,
|
||||
view_options: Default::default(),
|
||||
nodes: BTreeMap::new(),
|
||||
layout: None,
|
||||
routes: vec![route(authored, vec![at(420., 40.)], 0.25, at(8., -8.))],
|
||||
},
|
||||
);
|
||||
place(&mut p, second, 30, send, at(40., 120.), card);
|
||||
place(&mut p, second, 31, monitor, at(660., 120.), card);
|
||||
validate_candidate(&p).unwrap();
|
||||
(
|
||||
p,
|
||||
main,
|
||||
second,
|
||||
json!({"authored_edge":authored,"authored_edge_name":"Verified report",
|
||||
"filtered_edge":hidden,"container_placement":id(20),"source_port":source_port,"target_port":target_port,
|
||||
"main_diagram":main,"second_diagram":second,"authored_pin_count":2,
|
||||
"source_anchor":{"side":"Right","offset":0.25},"target_anchor":{"side":"Left","offset":0.75},
|
||||
"dormant_source_attachment":{"side":"Bottom","offset":0.5},
|
||||
"dormant_target_attachment":{"side":"Top","offset":0.25}}),
|
||||
)
|
||||
}
|
||||
|
||||
fn json_bytes(p: &Project) -> Vec<u8> {
|
||||
let mut bytes = serde_json::to_vec_pretty(p).unwrap();
|
||||
if !bytes.ends_with(b"\n") {
|
||||
bytes.push(b'\n');
|
||||
}
|
||||
assert_eq!(persistence::decode(&bytes).unwrap(), *p);
|
||||
bytes
|
||||
}
|
||||
|
||||
fn source_bytes(p: &Project) -> Vec<u8> {
|
||||
let source = language::format(p);
|
||||
assert_eq!(language::parse(&source).unwrap(), *p);
|
||||
let mut bytes = source.into_bytes();
|
||||
if !bytes.ends_with(b"\n") {
|
||||
bytes.push(b'\n');
|
||||
}
|
||||
bytes
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let mut args = std::env::args_os().skip(1);
|
||||
let output = PathBuf::from(
|
||||
args.next()
|
||||
.ok_or("Usage: arrange_reset_native_probe NEW_DIRECTORY")?,
|
||||
);
|
||||
if args.next().is_some() || output.exists() {
|
||||
return Err("Require exactly one output directory that does not already exist".into());
|
||||
}
|
||||
let (before, main, second, identities) = original();
|
||||
let mut folded = before.clone();
|
||||
folded
|
||||
.diagrams
|
||||
.get_mut(&main)
|
||||
.unwrap()
|
||||
.nodes
|
||||
.get_mut(&id(20))
|
||||
.unwrap()
|
||||
.collapsed = true;
|
||||
let mut files = Vec::new();
|
||||
let mut cases = Vec::new();
|
||||
for (name, project) in [("expanded", before), ("folded", folded)] {
|
||||
validate_candidate(&project)?;
|
||||
// Explicitly candidate-derived UI oracle: this is not an independent layout proof.
|
||||
let expected = execute_batch(&project, &[Command::Layout(main)])?;
|
||||
assert_eq!(project.elements, expected.elements);
|
||||
assert_eq!(project.relations, expected.relations);
|
||||
assert_eq!(project.diagrams[&second], expected.diagrams[&second]);
|
||||
for (stage, p) in [("original", &project), ("expected-after", &expected)] {
|
||||
files.push((format!("{name}-{stage}.json"), json_bytes(p)));
|
||||
files.push((format!("{name}-{stage}.arcadia"), source_bytes(p)));
|
||||
}
|
||||
cases.push(json!({"name":name,"diagram":main,
|
||||
"original_json":format!("{name}-original.json"),
|
||||
"expected_json":format!("{name}-expected-after.json"),
|
||||
"original_source":format!("{name}-original.arcadia"),
|
||||
"expected_source":format!("{name}-expected-after.arcadia"),
|
||||
"original_routes":project.diagrams[&main].routes,"expected_routes":expected.diagrams[&main].routes,
|
||||
"expected_origin":"Candidate Command::Layout; UI-wiring oracle only"}));
|
||||
}
|
||||
let manifest = json!({"schema":1,"cases":cases,"identities":identities,
|
||||
"definition":"Source-authored five-Function/two-view fixture; original geometry never generated by Layout.",
|
||||
"comparison":"Decode saved JSON with production persistence and compare complete Project to immutable original/expected file. Prefer direct serializer bytes for byte checks; do not reserialize Project through generic JSON Value as a float oracle.",
|
||||
"performed":"Generation and core checks only when this program is actually run; no native UI results recorded here."});
|
||||
files.push((
|
||||
"manifest.json".into(),
|
||||
serde_json::to_vec_pretty(&manifest)?,
|
||||
));
|
||||
fs::create_dir(&output)?;
|
||||
for (name, bytes) in files {
|
||||
fs::write(output.join(name), bytes)?;
|
||||
}
|
||||
println!("{}", output.join("manifest.json").display());
|
||||
Ok(())
|
||||
}
|
||||
@@ -1692,3 +1692,161 @@ fn adding_bend_between_close_ports_stays_outside_owner_clearance() {
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
fn arrange_reset_fixture() -> (Project, Id, Id) {
|
||||
let mut project = Project::empty();
|
||||
let source = project.add(Kind::Activity, "Send report", 20., 70.);
|
||||
let target = project.add(Kind::Activity, "Receive report", 650., 200.);
|
||||
let edge = project.relate(RelationKind::Interaction, "Report", source, target);
|
||||
let diagram = project.ensure_diagram(Layer::Oa);
|
||||
project
|
||||
.diagrams
|
||||
.get_mut(&diagram)
|
||||
.unwrap()
|
||||
.routes
|
||||
.push(EdgeRoute {
|
||||
edge,
|
||||
source_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Bottom,
|
||||
offset: 0.25,
|
||||
}),
|
||||
target_attachment: Some(EdgeAttachment {
|
||||
side: PortAnchorSide::Top,
|
||||
offset: 0.75,
|
||||
}),
|
||||
waypoints: vec![Position { x: 400., y: 500. }],
|
||||
label: Some(EdgeLabel {
|
||||
fraction: 0.25,
|
||||
offset: Position { x: 11., y: 7. },
|
||||
}),
|
||||
});
|
||||
(project, diagram, source)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrange_reset_document_preserves_multifile_comments_and_exact_undo_redo_reopen() {
|
||||
use crate::document::Document;
|
||||
use oa_core::source_project::{self, ProjectLimits};
|
||||
let (project, diagram, _) = arrange_reset_fixture();
|
||||
let mut bundle = source_project::format_bundle(&project);
|
||||
assert!(
|
||||
bundle.files.len() > 1,
|
||||
"This must exercise a real include closure"
|
||||
);
|
||||
for (path, text) in &mut bundle.files {
|
||||
*text = format!("// Keep exact authored module: {path}\r\n{text}");
|
||||
}
|
||||
let mut document = Document::from_bundle(bundle, ProjectLimits::default()).unwrap();
|
||||
document.mark_saved(document.snapshot());
|
||||
let before = document.snapshot();
|
||||
let original_route = before.decoded.project.diagrams[&diagram].routes[0].clone();
|
||||
document.execute(Command::Layout(diagram)).unwrap();
|
||||
let after = document.snapshot();
|
||||
let routes = &after.decoded.project.diagrams[&diagram].routes;
|
||||
let mut expected_route = original_route;
|
||||
expected_route.waypoints.clear();
|
||||
expected_route.label = None;
|
||||
assert_eq!(routes, &vec![expected_route]);
|
||||
let mut outside = after.decoded.project.clone();
|
||||
outside
|
||||
.diagrams
|
||||
.insert(diagram, before.decoded.project.diagrams[&diagram].clone());
|
||||
assert_eq!(outside, before.decoded.project);
|
||||
let affected: std::collections::BTreeSet<_> = std::iter::once(diagram)
|
||||
.chain(
|
||||
before.decoded.project.diagrams[&diagram]
|
||||
.nodes
|
||||
.keys()
|
||||
.copied(),
|
||||
)
|
||||
.map(|id| document.compiled().index.declarations[&id].path.clone())
|
||||
.collect();
|
||||
for (path, text) in &before.bundle.files {
|
||||
assert!(
|
||||
after.bundle.files[path].contains(&format!("// Keep exact authored module: {path}"))
|
||||
);
|
||||
if !affected.contains(path) {
|
||||
assert_eq!(
|
||||
&after.bundle.files[path], text,
|
||||
"Unrelated accepted bytes: {path}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(document.undo());
|
||||
assert_eq!(document.bundle(), &before.bundle);
|
||||
assert_eq!(document.project(), &before.decoded.project);
|
||||
assert!(!document.dirty());
|
||||
assert!(document.redo());
|
||||
assert_eq!(document.bundle(), &after.bundle);
|
||||
assert_eq!(document.project(), &after.decoded.project);
|
||||
let reopened = Document::from_bundle(after.bundle.clone(), ProjectLimits::default()).unwrap();
|
||||
assert_eq!(reopened.bundle(), &after.bundle);
|
||||
assert_eq!(reopened.project(), &after.decoded.project);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrange_reset_pending_valid_property_has_separate_apply_and_layout_history() {
|
||||
let (project, diagram, source) = arrange_reset_fixture();
|
||||
let mut control = App::with_project(project.clone());
|
||||
send(&mut control, Message::Select(source));
|
||||
send(
|
||||
&mut control,
|
||||
Message::Field("/name".into(), "Revised sender".into()),
|
||||
);
|
||||
send(&mut control, Message::Apply);
|
||||
let applied_project = control.project().clone();
|
||||
let applied_bundle = control.document.bundle().clone();
|
||||
assert_eq!(applied_project.elements[&source].name, "Revised sender");
|
||||
assert_eq!(
|
||||
applied_project.diagrams[&diagram].routes,
|
||||
project.diagrams[&diagram].routes
|
||||
);
|
||||
let mut app = App::with_project(project);
|
||||
let before_project = app.project().clone();
|
||||
let before_bundle = app.document.bundle().clone();
|
||||
send(&mut app, Message::Select(source));
|
||||
send(
|
||||
&mut app,
|
||||
Message::Field("/name".into(), "Revised sender".into()),
|
||||
);
|
||||
send(&mut app, Message::Command(Command::Layout(diagram)));
|
||||
assert_eq!(app.project().elements[&source].name, "Revised sender");
|
||||
assert!(
|
||||
app.project().diagrams[&diagram].routes[0]
|
||||
.waypoints
|
||||
.is_empty()
|
||||
);
|
||||
assert!(app.project().diagrams[&diagram].routes[0].label.is_none());
|
||||
let arranged_project = app.project().clone();
|
||||
let arranged_bundle = app.document.bundle().clone();
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &applied_project);
|
||||
assert_eq!(app.document.bundle(), &applied_bundle);
|
||||
send(&mut app, Message::Undo);
|
||||
assert_eq!(app.project(), &before_project);
|
||||
assert_eq!(app.document.bundle(), &before_bundle);
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &applied_project);
|
||||
assert_eq!(app.document.bundle(), &applied_bundle);
|
||||
send(&mut app, Message::Redo);
|
||||
assert_eq!(app.project(), &arranged_project);
|
||||
assert_eq!(app.document.bundle(), &arranged_bundle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrange_reset_invalid_property_blocks_layout_without_losing_draft_or_accepted_bytes() {
|
||||
let (project, diagram, _) = arrange_reset_fixture();
|
||||
let node = *project.diagrams[&diagram].nodes.keys().next().unwrap();
|
||||
let mut app = App::with_project(project);
|
||||
send(&mut app, Message::Select(node));
|
||||
let before_project = app.project().clone();
|
||||
let before_bundle = app.document.bundle().clone();
|
||||
let revision = app.document.revision();
|
||||
send(&mut app, Message::Field("/position/x".into(), "-".into()));
|
||||
send(&mut app, Message::Command(Command::Layout(diagram)));
|
||||
assert_eq!(app.project(), &before_project);
|
||||
assert_eq!(app.document.bundle(), &before_bundle);
|
||||
assert_eq!(app.document.revision(), revision);
|
||||
assert!(!app.document.can_undo());
|
||||
assert_eq!(app.draft.as_ref().unwrap().inputs["/position/x"].raw, "-");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user