1205 lines
44 KiB
Rust
1205 lines
44 KiB
Rust
use oa_core::{language, source_project::*, *};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn bundle(entry: &str, fragments: &[(&str, &str)]) -> SourceBundle {
|
|
let mut files = BTreeMap::from([("project.arcadia".into(), entry.into())]);
|
|
files.extend(
|
|
fragments
|
|
.iter()
|
|
.map(|(path, text)| ((*path).into(), (*text).into())),
|
|
);
|
|
SourceBundle {
|
|
entry: "project.arcadia".into(),
|
|
files,
|
|
}
|
|
}
|
|
|
|
fn compile(bundle: &SourceBundle) -> CompiledSourceProject {
|
|
compile_bundle(bundle, ProjectLimits::default()).unwrap()
|
|
}
|
|
|
|
fn id(project: &Project, alias: &str) -> Id {
|
|
project
|
|
.identities()
|
|
.into_iter()
|
|
.find(|(_, candidate)| *candidate == alias)
|
|
.unwrap()
|
|
.0
|
|
}
|
|
|
|
#[test]
|
|
fn route_edits_keep_diagram_file_ownership_and_comments_with_edge_identity() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
|
&[
|
|
(
|
|
"work.arcadia",
|
|
"oa_activity a \"A\" {}\noa_activity b \"B\" {}\ninteraction first \"First\" { source @a; target @b; }\ninteraction second \"Second\" { source @b; target @a; }\n",
|
|
),
|
|
(
|
|
"views.arcadia",
|
|
r#"// Diagram routing decisions stay here.
|
|
diagram review "Review" {
|
|
routes [
|
|
// First edge's detour, including unusual numeric spelling.
|
|
{ edge @ // stable edge
|
|
first; waypoints [{ x 4.20e2; y // clearance
|
|
260; }];
|
|
label { fraction // near target
|
|
0.7; offset { x 0; y -16; }; }; },
|
|
// Second edge stays automatic.
|
|
{ edge @second; }
|
|
];
|
|
}
|
|
"#,
|
|
),
|
|
],
|
|
);
|
|
let original = compile(&input);
|
|
let mut changed = original.decoded.project.clone();
|
|
let diagram = id(&changed, "review");
|
|
let first = id(&changed, "first");
|
|
let routes = &mut changed.diagrams.get_mut(&diagram).unwrap().routes;
|
|
routes[0].waypoints[0].y = 300.;
|
|
routes[0].label.as_mut().unwrap().fraction = 0.6;
|
|
routes.swap(0, 1);
|
|
changed.relations.get_mut(&first).unwrap().alias = "detour".into();
|
|
let updated = update_bundle(&original, &changed, ProjectLimits::default()).unwrap();
|
|
assert_eq!(updated.decoded.project, changed);
|
|
assert_eq!(updated.index.declarations[&diagram].path, "views.arcadia");
|
|
assert_eq!(updated.bundle.files.len(), original.bundle.files.len());
|
|
let text = &updated.bundle.files["views.arcadia"];
|
|
assert!(
|
|
text.contains("edge @ // stable edge\n detour;"),
|
|
"{text}"
|
|
);
|
|
assert!(text.contains("x 4.20e2; y // clearance\n 300"), "{text}");
|
|
assert!(text.contains("fraction // near target\n 0.6"), "{text}");
|
|
assert!(
|
|
text.find("// Second edge stays automatic.").unwrap()
|
|
< text.find("// First edge's detour").unwrap(),
|
|
"{text}"
|
|
);
|
|
assert!(
|
|
text.find("// First edge's detour").unwrap()
|
|
< text.find("edge @ // stable edge\n detour").unwrap(),
|
|
"{text}"
|
|
);
|
|
let mut old_comments = comments(&original.bundle.files["views.arcadia"]);
|
|
let mut new_comments = comments(text);
|
|
old_comments.sort();
|
|
new_comments.sort();
|
|
assert_eq!(new_comments, old_comments);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_cross_file_route_references_and_geometry_locate_the_authored_view() {
|
|
let mut input = bundle(
|
|
"arcadia 5; project p \"Routing\" { include \"views.arcadia\"; include \"work.arcadia\"; }",
|
|
&[
|
|
(
|
|
"work.arcadia",
|
|
"oa_activity a \"A\" {}\noa_activity b \"B\" {}\ninteraction link \"Link\" { source @a; target @b; }\n",
|
|
),
|
|
(
|
|
"views.arcadia",
|
|
"// authored view\ndiagram review \"Review\" {\n routes [{ edge @absent; }];\n}\n",
|
|
),
|
|
],
|
|
);
|
|
let errors = compile_bundle(&input, ProjectLimits::default()).unwrap_err();
|
|
assert!(
|
|
errors.iter().any(|error| error.path == "views.arcadia"
|
|
&& error.diagnostic.line == 3
|
|
&& error.diagnostic.message.contains("absent")),
|
|
"{errors:?}"
|
|
);
|
|
input.files.get_mut("views.arcadia").unwrap().clone_from(&
|
|
"// authored view\ndiagram review \"Review\" {\n routes [{ edge @link; label { fraction -0.1; offset { x 0; y 0; }; }; }];\n}\n".into());
|
|
let errors = compile_bundle(&input, ProjectLimits::default()).unwrap_err();
|
|
assert!(
|
|
errors.iter().any(|error| error.path == "views.arcadia"
|
|
&& error.diagnostic.line == 3
|
|
&& error.diagnostic.field.as_deref() == Some("routes")),
|
|
"{errors:?}"
|
|
);
|
|
}
|
|
|
|
fn typed() -> SourceBundle {
|
|
bundle(
|
|
"arcadia 5; project test_model \"航海\" {\n include \"exchanges.arcadia\";\n include \"work/send.arcadia\";\n include \"work/receive.arcadia\";\n include \"types.arcadia\";\n}",
|
|
&[
|
|
(
|
|
"exchanges.arcadia",
|
|
"// Boundary-independent functional exchange\nfunctional_exchange message \"Message\" { source @send; target @receive; source_port @output; target_port @input; items [@data]; }\n",
|
|
),
|
|
(
|
|
"work/send.arcadia",
|
|
"// keep this explanation\nsa_function send \"Send\" {}\nsa_port output \"Output\" { parent @send; contract { FunctionOutput { outgoing_items [@data]; }; }; }\n",
|
|
),
|
|
(
|
|
"work/receive.arcadia",
|
|
"sa_function receive \"Receive\" {}\nsa_port input \"Input\" { parent @receive; contract { FunctionInput { incoming_items [@data]; }; }; }\n",
|
|
),
|
|
("types.arcadia", "sa_exchange_item data \"Data\" {}\n"),
|
|
],
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_typed_ports_global_forward_references_and_stable_ids() {
|
|
let input = typed();
|
|
let original = input.clone();
|
|
let result = compile(&input);
|
|
assert_eq!(input, original, "compilation must not mutate caller drafts");
|
|
assert_ne!(result.bundle, input);
|
|
let again = compile(&result.bundle);
|
|
assert_eq!(again.decoded.project, result.decoded.project);
|
|
assert_eq!(again.bundle, result.bundle);
|
|
assert_eq!(again.index, result.index);
|
|
assert!(result.decoded.migration.is_none());
|
|
let project = &result.decoded.project;
|
|
assert_eq!(
|
|
project.relations[&id(project, "message")].source(),
|
|
id(project, "send")
|
|
);
|
|
let port = id(project, "output");
|
|
assert_eq!(result.index.declarations[&port].path, "work/send.arcadia");
|
|
let field = &result.index.fields[&(port, "contract".into())];
|
|
assert_eq!(
|
|
&result.bundle.files[&field.path][field.span.start..field.span.end],
|
|
"{ FunctionOutput { outgoing_items [@data]; }; }"
|
|
);
|
|
assert_eq!(
|
|
language::parse(&language::format(project)).unwrap(),
|
|
*project
|
|
);
|
|
assert_eq!(
|
|
persistence::decode(&serde_json::to_vec(project).unwrap()).unwrap(),
|
|
*project
|
|
);
|
|
assert!(result.bundle.files["work/send.arcadia"].starts_with("// keep this explanation\n"));
|
|
let send = id(project, "send");
|
|
let warning = result
|
|
.diagnostics
|
|
.iter()
|
|
.find(|issue| {
|
|
issue.diagnostic.object_id == Some(send) && issue.diagnostic.code == "unallocated_leaf"
|
|
})
|
|
.unwrap();
|
|
assert_eq!(warning.path, "work/send.arcadia");
|
|
assert_eq!(warning.diagnostic.line, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn advisory_lint_locations_refer_to_raw_drafts_before_identity_stabilization() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"work.arcadia\"; }",
|
|
&[(
|
|
"work.arcadia",
|
|
"sa_function first \"First\" {}\nsa_function second \"Second\" {}\n",
|
|
)],
|
|
);
|
|
let before = input.clone();
|
|
let raw = &input.files["work.arcadia"];
|
|
let warnings = lint_bundle(&input, ProjectLimits::default());
|
|
let second = warnings
|
|
.iter()
|
|
.find(|d| {
|
|
d.path == "work.arcadia"
|
|
&& d.diagnostic.code == "unallocated_leaf"
|
|
&& d.diagnostic.line == 2
|
|
})
|
|
.unwrap();
|
|
assert_eq!(
|
|
second.diagnostic.span.start,
|
|
raw.find("sa_function second").unwrap()
|
|
);
|
|
assert!(raw[second.diagnostic.span.start..second.diagnostic.span.end].contains("second"));
|
|
assert_eq!(input, before);
|
|
let compiled = compile(&input);
|
|
assert_ne!(compiled.bundle, input);
|
|
assert!(compiled.diagnostics.iter().any(|d| d.path == "work.arcadia"
|
|
&& d.diagnostic.code == "unallocated_leaf"
|
|
&& d.diagnostic.line == 2
|
|
&& d.diagnostic.span.start > second.diagnostic.span.start));
|
|
}
|
|
|
|
#[test]
|
|
fn source_errors_keep_exact_fragment_unicode_positions() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"child.arcadia\"; }",
|
|
&[(
|
|
"child.arcadia",
|
|
"// Пример\nsa_function child \"航海\" { parent @missing; }",
|
|
)],
|
|
);
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
let text = &input.files["child.arcadia"];
|
|
assert_eq!(error.path, "child.arcadia");
|
|
assert_eq!(error.diagnostic.line, 2);
|
|
let offset = text.find("@missing").unwrap();
|
|
assert_eq!(error.diagnostic.span.start, offset);
|
|
assert_eq!(
|
|
error.diagnostic.column,
|
|
text[..offset].rsplit('\n').next().unwrap().chars().count() + 1
|
|
);
|
|
assert!(error.diagnostic.message.contains("@missing"));
|
|
let cyclic_parent = SourceBundle {
|
|
files: input
|
|
.files
|
|
.iter()
|
|
.map(|(path, text)| (path.clone(), text.replace("@missing", "@child")))
|
|
.collect(),
|
|
..input
|
|
};
|
|
let error = compile_bundle(&cyclic_parent, ProjectLimits::default()).unwrap_err();
|
|
let parent = error
|
|
.iter()
|
|
.find(|issue| issue.diagnostic.code == "invalid_parent")
|
|
.unwrap();
|
|
assert_eq!(parent.path, "child.arcadia");
|
|
assert_eq!(parent.diagnostic.span.start, offset);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
|
|
for (left, right) in [
|
|
("oa_activity same \"A\" {}", "oa_activity same \"B\" {}"),
|
|
(
|
|
"oa_activity first \"A\" id \"00000000-0000-4000-8000-000000000001\" {}",
|
|
"oa_activity second \"B\" id \"00000000-0000-4000-8000-000000000001\" {}",
|
|
),
|
|
] {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
|
&[("a.arcadia", left), ("b.arcadia", right)],
|
|
);
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
assert_eq!(error.diagnostic.code, "duplicate_declaration");
|
|
assert_eq!(error.path, "b.arcadia");
|
|
assert_eq!(error.related[0].path, "a.arcadia");
|
|
}
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"child.arcadia\"; }",
|
|
&[("child.arcadia", "oa_activity p \"Collision\" {}")],
|
|
);
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
assert_eq!(error.related[0].path, "project.arcadia");
|
|
}
|
|
|
|
#[test]
|
|
fn includes_are_once_only_and_cycles_missing_and_unreachable_files_are_located() {
|
|
let mut input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"sub/a.arcadia\"; include \"sub/b.arcadia\"; }",
|
|
&[
|
|
("sub/a.arcadia", "include \"../shared.arcadia\";"),
|
|
("sub/b.arcadia", "include \"../shared.arcadia\";"),
|
|
("shared.arcadia", "oa_activity shared_work \"Shared\" {}"),
|
|
],
|
|
);
|
|
assert_eq!(compile(&input).decoded.project.elements.len(), 1);
|
|
input
|
|
.files
|
|
.insert("shared.arcadia".into(), "include \"sub/a.arcadia\";".into());
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
assert_eq!(error.path, "shared.arcadia");
|
|
assert_eq!(error.diagnostic.code, "include_cycle");
|
|
assert!(
|
|
error
|
|
.diagnostic
|
|
.message
|
|
.contains("sub/a.arcadia -> shared.arcadia -> sub/a.arcadia")
|
|
);
|
|
input.files.remove("shared.arcadia");
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
assert_eq!(error.path, "sub/a.arcadia");
|
|
assert_eq!(error.diagnostic.code, "missing_include");
|
|
input.files.insert("shared.arcadia".into(), String::new());
|
|
input.files.insert("unused.arcadia".into(), String::new());
|
|
assert_eq!(
|
|
compile_bundle(&input, ProjectLimits::default()).unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"unreachable_file"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn portable_paths_reject_escapes_devices_urls_and_case_collisions() {
|
|
for path in [
|
|
"../escape.arcadia",
|
|
"/abs.arcadia",
|
|
"C:/drive.arcadia",
|
|
"https://host/a.arcadia",
|
|
"a\\b.arcadia",
|
|
"a//b.arcadia",
|
|
"con.arcadia",
|
|
"dir./a.arcadia",
|
|
"a.txt",
|
|
] {
|
|
assert!(resolve_include("project.arcadia", path).is_err(), "{path}");
|
|
}
|
|
assert_eq!(
|
|
resolve_include("sub/a.arcadia", "../b.arcadia").unwrap(),
|
|
"b.arcadia"
|
|
);
|
|
assert!(resolve_include("../bad.arcadia", "a.arcadia").is_err());
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" {}",
|
|
&[("A.arcadia", ""), ("a.arcadia", "")],
|
|
);
|
|
assert_eq!(
|
|
compile_bundle(&input, ProjectLimits::default()).unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"duplicate_path"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn project_admission_is_aggregate_and_precedes_global_resolution() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
|
&[
|
|
("a.arcadia", "oa_activity one \"One\" { parent @missing; }"),
|
|
("b.arcadia", "oa_activity two \"Two\" {}"),
|
|
],
|
|
);
|
|
let cases = [
|
|
ProjectLimits {
|
|
max_files: 2,
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
max_include_depth: 0,
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
max_include_edges: 1,
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_graph_objects: 1,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_records: 2,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_source_bytes: input.files.values().map(String::len).sum::<usize>() - 1,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
},
|
|
];
|
|
for limits in cases {
|
|
let error = compile_bundle(&input, limits).unwrap_err();
|
|
assert!(error[0].diagnostic.code.ends_with("limit"), "{error:?}");
|
|
assert!(!error[0].diagnostic.message.contains("@missing"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn canonical_export_groups_semantic_roots_ports_and_placements_and_is_deterministic() {
|
|
let mut project = compile(&typed()).decoded.project;
|
|
let child = project
|
|
.add_element(
|
|
Kind::Function,
|
|
Layer::Sa,
|
|
"Child",
|
|
Some(id(&project, "send")),
|
|
)
|
|
.unwrap();
|
|
let diagram = project.ensure_diagram(Layer::Sa);
|
|
let node = project.place(diagram, child, Position { x: 40., y: 80. });
|
|
let output = format_bundle(&project);
|
|
assert_eq!(format_bundle(&project), output);
|
|
let result = compile(&output);
|
|
assert_eq!(result.decoded.project, project);
|
|
assert!(
|
|
result.index.declarations[&child]
|
|
.path
|
|
.contains("function_send.arcadia")
|
|
);
|
|
assert_eq!(
|
|
result.index.declarations[&child].path,
|
|
result.index.declarations[&id(&project, "output")].path
|
|
);
|
|
assert_eq!(
|
|
result.index.declarations[&node].path,
|
|
result.index.declarations[&diagram].path
|
|
);
|
|
let work = &output.files[&result.index.declarations[&child].path];
|
|
assert!(
|
|
work.find("sa_function send ").unwrap()
|
|
< work
|
|
.find(&format!("sa_function {} ", project.elements[&child].alias))
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn moving_declarations_between_files_changes_no_semantics_or_ids() {
|
|
let compiled = compile(&typed());
|
|
let mut moved = compiled.bundle.clone();
|
|
let port = id(&compiled.decoded.project, "output");
|
|
let location = &compiled.index.declarations[&port];
|
|
let declaration =
|
|
moved.files[&location.path][location.span.start..location.span.end].to_owned();
|
|
moved
|
|
.files
|
|
.get_mut(&location.path)
|
|
.unwrap()
|
|
.replace_range(location.span.start..location.span.end, "");
|
|
moved
|
|
.files
|
|
.get_mut("exchanges.arcadia")
|
|
.unwrap()
|
|
.push_str(&format!("\n{declaration}\n"));
|
|
let moved = compile(&moved);
|
|
assert_eq!(moved.decoded.project, compiled.decoded.project);
|
|
assert_eq!(moved.index.declarations[&port].path, "exchanges.arcadia");
|
|
}
|
|
|
|
#[test]
|
|
fn model_updates_rewrite_cross_file_alias_uses_preserve_ownership_and_untouched_comments() {
|
|
let compiled = compile(&typed());
|
|
let mut project = compiled.decoded.project.clone();
|
|
let send = id(&project, "send");
|
|
project.elements.get_mut(&send).unwrap().alias = "transmit".into();
|
|
project.elements.get_mut(&send).unwrap().name = "Transmit data".into();
|
|
project.description = "Documented project".into();
|
|
let result = update_bundle(&compiled, &project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(result.decoded.project, project);
|
|
assert_eq!(result.index.declarations[&send].path, "work/send.arcadia");
|
|
assert!(result.bundle.files["exchanges.arcadia"].contains("source @transmit;"));
|
|
assert!(result.bundle.files["work/send.arcadia"].contains("parent @transmit;"));
|
|
assert!(result.bundle.files["work/send.arcadia"].starts_with("// keep this explanation\n"));
|
|
assert_eq!(
|
|
result.bundle.files["types.arcadia"],
|
|
compiled.bundle.files["types.arcadia"]
|
|
);
|
|
assert_eq!(
|
|
result.bundle.files["work/receive.arcadia"],
|
|
compiled.bundle.files["work/receive.arcadia"]
|
|
);
|
|
assert_eq!(
|
|
update_bundle(&result, &project, ProjectLimits::default())
|
|
.unwrap()
|
|
.bundle,
|
|
result.bundle
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn graphical_move_updates_only_its_owned_view_document() {
|
|
let mut project = compile(&typed()).decoded.project;
|
|
let diagram = project.ensure_diagram(Layer::Sa);
|
|
let send = id(&project, "send");
|
|
let node = project.place(diagram, send, Position { x: 0., y: 0. });
|
|
let current = compile(&format_bundle(&project));
|
|
project
|
|
.diagrams
|
|
.get_mut(&diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&node)
|
|
.unwrap()
|
|
.position = Position { x: 142., y: 83. };
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
let changed: Vec<_> = result
|
|
.bundle
|
|
.files
|
|
.iter()
|
|
.filter(|(path, text)| current.bundle.files[*path] != **text)
|
|
.map(|(path, _)| path.clone())
|
|
.collect();
|
|
assert_eq!(
|
|
changed,
|
|
vec![current.index.declarations[&node].path.clone()]
|
|
);
|
|
assert_eq!(result.decoded.project, project);
|
|
}
|
|
|
|
#[test]
|
|
fn new_children_inherit_owner_file_new_roots_add_include_and_deletion_keeps_files() {
|
|
let current = compile(&typed());
|
|
let mut project = current.decoded.project.clone();
|
|
let send = id(&project, "send");
|
|
let child = project
|
|
.add_element(Kind::Function, Layer::Sa, "Nested work", Some(send))
|
|
.unwrap();
|
|
let root = project
|
|
.add_element(Kind::Function, Layer::Sa, "Independent work", None)
|
|
.unwrap();
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(result.index.declarations[&child].path, "work/send.arcadia");
|
|
let root_file = result.index.declarations[&root].path.clone();
|
|
assert!(root_file.contains("independent_work"));
|
|
assert!(result.bundle.files["project.arcadia"].contains(&format!("include \"{root_file}\";")));
|
|
project.elements.remove(&root);
|
|
let deleted = update_bundle(&result, &project, ProjectLimits::default()).unwrap();
|
|
assert!(deleted.bundle.files.contains_key(&root_file));
|
|
assert!(!deleted.index.declarations.contains_key(&root));
|
|
}
|
|
|
|
#[test]
|
|
fn failed_update_preserves_candidate_and_rejects_stale_index() {
|
|
let current = compile(&typed());
|
|
let original = current.bundle.clone();
|
|
let mut project = current.decoded.project.clone();
|
|
project
|
|
.elements
|
|
.get_mut(&id(¤t.decoded.project, "send"))
|
|
.unwrap()
|
|
.parent = Some(Id::new_v4());
|
|
assert!(update_bundle(¤t, &project, ProjectLimits::default()).is_err());
|
|
assert_eq!(current.bundle, original);
|
|
let mut stale = current.clone();
|
|
stale
|
|
.bundle
|
|
.files
|
|
.get_mut("work/send.arcadia")
|
|
.unwrap()
|
|
.insert_str(0, "// external edit\n");
|
|
assert_eq!(
|
|
update_bundle(&stale, ¤t.decoded.project, ProjectLimits::default()).unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"stale_source_index"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn single_string_and_legacy_apis_remain_explicit_and_filesystem_free() {
|
|
let source = "arcadia 5; project p \"P\" { include \"missing.arcadia\"; }";
|
|
assert!(
|
|
language::parse(source).unwrap_err()[0]
|
|
.message
|
|
.contains("requires a source bundle")
|
|
);
|
|
let old = "arcadia 2; project p \"Legacy\" { oa_activity work \"Work\" {} }";
|
|
let decoded = language::parse_with_report(old).unwrap();
|
|
assert!(decoded.migration.is_some());
|
|
assert!(compile_bundle(&bundle(old, &[]), ProjectLimits::default()).is_err());
|
|
assert_eq!(
|
|
compile(&format_bundle(&decoded.project)).decoded.project,
|
|
decoded.project
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exported_case_colliding_root_aliases_are_portable_and_large_root_sets_are_bounded() {
|
|
let mut project = Project::empty();
|
|
let a = project
|
|
.add_element(Kind::Activity, Layer::Oa, "Upper", None)
|
|
.unwrap();
|
|
let b = project
|
|
.add_element(Kind::Activity, Layer::Oa, "Lower", None)
|
|
.unwrap();
|
|
project.elements.get_mut(&a).unwrap().alias = "Work".into();
|
|
project.elements.get_mut(&b).unwrap().alias = "work".into();
|
|
let result = compile(&format_bundle(&project));
|
|
assert_ne!(
|
|
result.index.declarations[&a].path.to_lowercase(),
|
|
result.index.declarations[&b].path.to_lowercase()
|
|
);
|
|
for n in 0..260 {
|
|
project
|
|
.add_element(Kind::Activity, Layer::Oa, &format!("Activity {n}"), None)
|
|
.unwrap();
|
|
}
|
|
let generated = format_bundle(&project);
|
|
assert!(generated.files.len() <= ProjectLimits::default().max_files);
|
|
assert_eq!(compile(&generated).decoded.project, project);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_demo_preserves_behavior_artifacts_traces_and_every_placement() {
|
|
let project = Project::demo();
|
|
let generated = format_bundle(&project);
|
|
let result = compile(&generated);
|
|
assert_eq!(result.decoded.project, project);
|
|
for element in project
|
|
.elements
|
|
.values()
|
|
.filter(|e| matches!(e.kind(), Kind::Scenario | Kind::StateMachine))
|
|
{
|
|
assert!(
|
|
result.index.declarations[&element.id]
|
|
.path
|
|
.contains("/behavior/")
|
|
);
|
|
for child in project
|
|
.elements
|
|
.values()
|
|
.filter(|child| child.parent == Some(element.id))
|
|
{
|
|
assert_eq!(
|
|
result.index.declarations[&child.id].path,
|
|
result.index.declarations[&element.id].path
|
|
);
|
|
}
|
|
}
|
|
let json = serde_json::to_string(&generated).unwrap();
|
|
assert_eq!(
|
|
serde_json::from_str::<SourceBundle>(&json).unwrap(),
|
|
generated
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn json_bundle_rejects_duplicate_file_keys_and_unknown_fields() {
|
|
assert!(serde_json::from_str::<SourceBundle>(r#"{"entry":"project.arcadia","files":{"project.arcadia":"first","project.arcadia":"second"}}"#).unwrap_err().to_string().contains("Duplicate source path"));
|
|
assert!(
|
|
serde_json::from_str::<SourceBundle>(
|
|
r#"{"entry":"project.arcadia","files":{},"root":"C:/host"}"#
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn generated_id_insertion_handles_adjacent_header_tokens_and_remains_exactly_indexed() {
|
|
let input = bundle(
|
|
"arcadia 5;project p\"P\"{include \"child.arcadia\";}",
|
|
&[(
|
|
"child.arcadia",
|
|
"oa_activity a\"A\"{}diagram d\"D\"{}node n{diagram @d;element @a;}",
|
|
)],
|
|
);
|
|
let result = compile(&input);
|
|
for (id, location) in &result.index.declarations {
|
|
assert!(
|
|
result.bundle.files[&location.path][location.span.start..location.span.end]
|
|
.contains(&id.to_string())
|
|
);
|
|
}
|
|
assert_eq!(
|
|
compile(&result.bundle).decoded.project,
|
|
result.decoded.project
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_include_edges_count_toward_admission_and_ordered_references_are_aggregate() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"a.arcadia\"; include \"a.arcadia\"; }",
|
|
&[("a.arcadia", "")],
|
|
);
|
|
assert_eq!(
|
|
compile_bundle(
|
|
&input,
|
|
ProjectLimits {
|
|
max_include_edges: 1,
|
|
..ProjectLimits::default()
|
|
}
|
|
)
|
|
.unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"project_limit"
|
|
);
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
|
&[
|
|
(
|
|
"a.arcadia",
|
|
"oa_process one \"One\" { steps [@step_one]; } oa_occurrence step_one \"Step one\" { parent @one; body { Control { kind Choice; }; }; }",
|
|
),
|
|
(
|
|
"b.arcadia",
|
|
"oa_process two \"Two\" { steps [@step_two]; } oa_occurrence step_two \"Step two\" { parent @two; body { Control { kind Choice; }; }; }",
|
|
),
|
|
],
|
|
);
|
|
assert_eq!(
|
|
compile_bundle(
|
|
&input,
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_step_references: 1,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
}
|
|
)
|
|
.unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"model_limit"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn include_inspection_reuses_strict_entry_fragment_syntax_and_real_spans() {
|
|
let source = "// include \"fake.arcadia\";\noa_activity work \"航海\" { parent @external_work; description \"include ignored\"; }\ninclude \"../shared.arcadia\";";
|
|
let includes =
|
|
inspect_includes("sub/work.arcadia", source, false, ProjectLimits::default()).unwrap();
|
|
assert_eq!(includes.len(), 1);
|
|
assert_eq!(includes[0].target, "shared.arcadia");
|
|
assert_eq!(includes[0].location.path, "sub/work.arcadia");
|
|
let span = includes[0].location.span;
|
|
assert_eq!(
|
|
&source[span.start..span.end],
|
|
"include \"../shared.arcadia\";"
|
|
);
|
|
let entry = "arcadia 5; project p \"P\" { include \"sub/work.arcadia\"; }";
|
|
assert_eq!(
|
|
inspect_includes("project.arcadia", entry, true, ProjectLimits::default()).unwrap()[0]
|
|
.target,
|
|
"sub/work.arcadia"
|
|
);
|
|
assert!(inspect_includes("project.arcadia", entry, false, ProjectLimits::default()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn include_inspection_returns_located_failure_without_partial_discovery_and_is_bounded() {
|
|
let source = "include \"first.arcadia\";\noa_activity invalid \"航海\" { parent @; }";
|
|
let errors =
|
|
inspect_includes("broken.arcadia", source, false, ProjectLimits::default()).unwrap_err();
|
|
assert_eq!(errors[0].path, "broken.arcadia");
|
|
assert_eq!(errors[0].diagnostic.line, 2);
|
|
assert_eq!(
|
|
errors[0].diagnostic.span.start,
|
|
source.find("@;").unwrap() + 1
|
|
);
|
|
assert_eq!(
|
|
inspect_includes(
|
|
"broken.arcadia",
|
|
source,
|
|
false,
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_source_bytes: 10,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
}
|
|
)
|
|
.unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"source_limit"
|
|
);
|
|
let source = "include \"first.arcadia\"; include \"second.arcadia\";";
|
|
let errors = inspect_includes(
|
|
"project.arcadia",
|
|
source,
|
|
false,
|
|
ProjectLimits {
|
|
max_include_edges: 1,
|
|
..ProjectLimits::default()
|
|
},
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(
|
|
errors[0].diagnostic.span.start,
|
|
source.find("include \"second").unwrap()
|
|
);
|
|
assert_eq!(errors[0].diagnostic.code, "project_limit");
|
|
}
|
|
|
|
fn comments(source: &str) -> Vec<String> {
|
|
language::tokenize(source)
|
|
.into_iter()
|
|
.filter(|token| token.class == language::TokenClass::Comment)
|
|
.map(|token| source[token.span.start..token.span.end].to_owned())
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_header_guard_and_project_edits_preserve_exact_crlf_trivia_and_escape_spelling() {
|
|
let entry = "arcadia 5;\r\nproject p // project title\r\n \"Original project\" {\r\n description // project reason\r\n \"Before\";\r\n include \"behavior.arcadia\";\r\n}\r\n";
|
|
let source = "oa_state_machine machine \"Machine\" {}\r\noa_region region_one \"Region\" { parent @machine; }\r\noa_state first \"First\" { parent @region_one; }\r\noa_state second \"Second\" { parent @region_one; }\r\nstate_transition route // route title\r\n \"Original transition\" {\r\n\tguard // feasibility\r\n\t \"old guard\"; // retain explanation\r\n\tdescription \"Keep\\u0020spelling\";\r\n\tsource @ // endpoint proof\r\n first; target @second;\r\n}\r\n";
|
|
let current = compile(&bundle(entry, &[("behavior.arcadia", source)]));
|
|
let mut project = current.decoded.project.clone();
|
|
project.name = "Renamed project".into();
|
|
project.description = "After".into();
|
|
let route = project
|
|
.relations
|
|
.get_mut(&id(¤t.decoded.project, "route"))
|
|
.unwrap();
|
|
route.name = "Renamed transition".into();
|
|
if let RelationBody::StateTransition { guard, .. } = &mut route.body {
|
|
*guard = "new guard".into();
|
|
} else {
|
|
panic!("transition");
|
|
}
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(
|
|
result.bundle.files["project.arcadia"],
|
|
current.bundle.files["project.arcadia"]
|
|
.replace("\"Original project\"", "\"Renamed project\"")
|
|
.replace("\"Before\"", "\"After\"")
|
|
);
|
|
assert_eq!(
|
|
result.bundle.files["behavior.arcadia"],
|
|
current.bundle.files["behavior.arcadia"]
|
|
.replace("\"Original transition\"", "\"Renamed transition\"")
|
|
.replace("\"old guard\"", "\"new guard\"")
|
|
);
|
|
assert_eq!(result.decoded.project, project);
|
|
}
|
|
|
|
#[test]
|
|
fn alias_updates_touch_only_header_and_reference_tokens_even_with_comments_after_at() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"work.arcadia\"; }",
|
|
&[(
|
|
"work.arcadia",
|
|
"oa_activity root_work // root rationale\n \"Root\\u0020name\" { description \"root_work is prose\"; }\noa_activity child \"Child\" { parent @ // parent proof\n root_work; }\n",
|
|
)],
|
|
);
|
|
let current = compile(&input);
|
|
let mut project = current.decoded.project.clone();
|
|
project
|
|
.elements
|
|
.get_mut(&id(¤t.decoded.project, "root_work"))
|
|
.unwrap()
|
|
.alias = "renamed_work".into();
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
let expected = current.bundle.files["work.arcadia"]
|
|
.replace("oa_activity root_work ", "oa_activity renamed_work ")
|
|
.replace("\n root_work;", "\n renamed_work;");
|
|
assert_eq!(result.bundle.files["work.arcadia"], expected);
|
|
}
|
|
|
|
#[test]
|
|
fn omitted_defaults_stay_omitted_and_new_fields_do_not_reformat_surviving_fields() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"work.arcadia\"; }",
|
|
&[(
|
|
"work.arcadia",
|
|
"oa_activity work \"Work\" {\r\n\t// owner rationale\r\n\tparent none ; // preserve spacing\r\n}\r\n",
|
|
)],
|
|
);
|
|
let current = compile(&input);
|
|
let mut project = current.decoded.project.clone();
|
|
project
|
|
.elements
|
|
.get_mut(&id(¤t.decoded.project, "work"))
|
|
.unwrap()
|
|
.description = "New documentation".into();
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
let before = ¤t.bundle.files["work.arcadia"];
|
|
let after = &result.bundle.files["work.arcadia"];
|
|
assert_eq!(
|
|
after.replace("\tdescription \"New documentation\";\r\n", ""),
|
|
*before
|
|
);
|
|
assert_eq!(comments(after), comments(before));
|
|
}
|
|
|
|
#[test]
|
|
fn kind_change_removes_field_syntax_but_keeps_its_comments_and_other_fields() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"item.arcadia\"; }",
|
|
&[(
|
|
"item.arcadia",
|
|
"oa_exchange_item item \"Item\" {\n description \"Keep\\u0020this\" ; // description note\n kind // classification rationale\n Information ;\n parent none;\n}\n",
|
|
)],
|
|
);
|
|
let current = compile(&input);
|
|
let mut project = current.decoded.project.clone();
|
|
project
|
|
.elements
|
|
.get_mut(&id(¤t.decoded.project, "item"))
|
|
.unwrap()
|
|
.body = ElementBody::new(Kind::Entity, Layer::Oa).unwrap();
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
let after = &result.bundle.files["item.arcadia"];
|
|
assert!(after.contains("description \"Keep\\u0020this\" ; // description note"));
|
|
assert!(after.contains("parent none;"));
|
|
assert_eq!(
|
|
comments(after),
|
|
comments(¤t.bundle.files["item.arcadia"])
|
|
);
|
|
assert!(!significant_words(after).contains(&"Information".into()));
|
|
}
|
|
|
|
fn significant_words(source: &str) -> Vec<String> {
|
|
language::tokenize(source)
|
|
.into_iter()
|
|
.filter(|token| {
|
|
!matches!(
|
|
token.class,
|
|
language::TokenClass::Whitespace | language::TokenClass::Comment
|
|
)
|
|
})
|
|
.map(|token| source[token.span.start..token.span.end].to_owned())
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn constructor_tag_changes_retain_shared_nested_fields_and_removed_field_comments() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"event.arcadia\"; }",
|
|
&[(
|
|
"event.arcadia",
|
|
"oa_event timer \"Timer\" { body { Time // time rationale\n { expression // expression rationale\n \"before\" ; kind After; // clock rationale\n }; }; }\n",
|
|
)],
|
|
);
|
|
let current = compile(&input);
|
|
let mut project = current.decoded.project.clone();
|
|
let timer = project
|
|
.elements
|
|
.get_mut(&id(¤t.decoded.project, "timer"))
|
|
.unwrap();
|
|
if let Some(SharedBody::Event { body }) = timer.shared_mut() {
|
|
*body = EventBody::Change {
|
|
expression: "after".into(),
|
|
};
|
|
} else {
|
|
panic!("event");
|
|
}
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
let after = &result.bundle.files["event.arcadia"];
|
|
assert!(after.contains("Change // time rationale"));
|
|
assert!(after.contains("expression // expression rationale\n \"after\" ;"));
|
|
assert_eq!(
|
|
comments(after),
|
|
comments(¤t.bundle.files["event.arcadia"])
|
|
);
|
|
assert!(!significant_words(after).contains(&"After".into()));
|
|
}
|
|
|
|
#[test]
|
|
fn scalar_record_constructor_changes_keep_comments_inside_removed_values() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"ports.arcadia\"; }",
|
|
&[(
|
|
"ports.arcadia",
|
|
"sa_function work \"Work\" {}\nsa_port input \"Input\" { parent @work; contract Legacy; // contract rationale\n legacy { direction In; // direction rationale\n interface none; // interface rationale\n }; }\n",
|
|
)],
|
|
);
|
|
let current = compile(&input);
|
|
let mut project = current.decoded.project.clone();
|
|
let port = project
|
|
.elements
|
|
.get_mut(&id(¤t.decoded.project, "input"))
|
|
.unwrap();
|
|
if let Some(SharedBody::Port { contract, legacy }) = port.shared_mut() {
|
|
*contract = PortContract::FunctionInput {
|
|
incoming_items: vec![],
|
|
};
|
|
*legacy = None;
|
|
} else {
|
|
panic!("port");
|
|
}
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(
|
|
comments(&result.bundle.files["ports.arcadia"]),
|
|
comments(¤t.bundle.files["ports.arcadia"])
|
|
);
|
|
assert_eq!(result.decoded.project, project);
|
|
}
|
|
|
|
#[test]
|
|
fn reference_list_reordering_carries_item_comments_and_alias_spelling_with_identity() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"types.arcadia\"; }",
|
|
&[(
|
|
"types.arcadia",
|
|
"sa_exchange_item a \"A\" {} sa_exchange_item b \"B\" {}\nsa_event service \"Service\" { body { Signal { items [\n // before A\n @ // A reference rationale\n a // after A\n ,\n // before B\n @b // after B\n ]; }; }; }\n",
|
|
)],
|
|
);
|
|
let current = compile(&input);
|
|
let mut project = current.decoded.project.clone();
|
|
let a = id(&project, "a");
|
|
let b = id(&project, "b");
|
|
let service = id(&project, "service");
|
|
project.elements.get_mut(&a).unwrap().alias = "renamed_a".into();
|
|
if let Some(SharedBody::Event {
|
|
body: EventBody::Signal { items },
|
|
}) = project.elements.get_mut(&service).unwrap().shared_mut()
|
|
{
|
|
*items = vec![b, a];
|
|
} else {
|
|
panic!("interface");
|
|
}
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
let after = &result.bundle.files["types.arcadia"];
|
|
assert!(after.find("// before B").unwrap() < after.find("// before A").unwrap());
|
|
assert!(after.contains("@ // A reference rationale\n renamed_a // after A"));
|
|
let mut old_comments = comments(¤t.bundle.files["types.arcadia"]);
|
|
let mut new_comments = comments(after);
|
|
old_comments.sort();
|
|
new_comments.sort();
|
|
assert_eq!(old_comments, new_comments);
|
|
assert_eq!(result.decoded.project, project);
|
|
}
|
|
|
|
#[test]
|
|
fn removed_list_items_and_declarations_keep_comments_as_enclosing_annotations() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"types.arcadia\"; }",
|
|
&[(
|
|
"types.arcadia",
|
|
"sa_exchange_item a \"A\" { // declaration explanation\n } sa_exchange_item b \"B\" {}\nsa_event service \"Service\" { body { Signal { items [\n // explanation for removed A\n @a,\n // explanation for retained B\n @b ]; }; }; }\n",
|
|
)],
|
|
);
|
|
let current = compile(&input);
|
|
let mut project = current.decoded.project.clone();
|
|
let a = id(&project, "a");
|
|
let b = id(&project, "b");
|
|
let service = id(&project, "service");
|
|
project.elements.remove(&a);
|
|
if let Some(SharedBody::Event {
|
|
body: EventBody::Signal { items },
|
|
}) = project.elements.get_mut(&service).unwrap().shared_mut()
|
|
{
|
|
*items = vec![b];
|
|
} else {
|
|
panic!("interface");
|
|
}
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
let mut before = comments(¤t.bundle.files["types.arcadia"]);
|
|
let mut after = comments(&result.bundle.files["types.arcadia"]);
|
|
before.sort();
|
|
after.sort();
|
|
assert_eq!(before, after);
|
|
assert_eq!(result.decoded.project, project);
|
|
}
|
|
|
|
#[test]
|
|
fn nested_message_value_edits_preserve_field_comments_and_reordering_uses_field_identity() {
|
|
let source = r#"oa_activity send "Send" {} oa_activity receive "Receive" {}
|
|
oa_exchange_item data "Data" {}
|
|
oa_data_field first_value "First" { parent @data; }
|
|
oa_data_field second_value "Second" { parent @data; }
|
|
interaction transfer "Transfer" { source @send; target @receive; items [@data]; }
|
|
oa_scenario conversation "Conversation" { viewpoint OperationalActivity; steps [@message]; }
|
|
oa_lifeline sender_line "Sender" { parent @conversation; represented @send; }
|
|
oa_lifeline receiver_line "Receiver" { parent @conversation; represented @receive; }
|
|
oa_occurrence message "Message" { parent @conversation; body { Exchange {
|
|
exchange @transfer; sender @sender_line; receiver @receiver_line;
|
|
values [
|
|
// first annotation
|
|
{ field @ // first field proof
|
|
first_value; expression // first expression proof
|
|
"first expression"; },
|
|
// second annotation
|
|
{ expression "second expression"; field @second_value; }
|
|
];
|
|
}; }; }
|
|
"#;
|
|
let current = compile(&bundle(
|
|
"arcadia 5; project p \"P\" { include \"conversation.arcadia\"; }",
|
|
&[("conversation.arcadia", source)],
|
|
));
|
|
let mut project = current.decoded.project.clone();
|
|
let message = id(&project, "message");
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = project.elements.get_mut(&message).unwrap().shared_mut()
|
|
{
|
|
values[0].expression = "changed expression".into();
|
|
} else {
|
|
panic!("message");
|
|
}
|
|
let changed = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(
|
|
changed.bundle.files["conversation.arcadia"],
|
|
current.bundle.files["conversation.arcadia"]
|
|
.replace("\"first expression\"", "\"changed expression\"")
|
|
);
|
|
let first = id(&project, "first_value");
|
|
project.elements.get_mut(&first).unwrap().alias = "renamed_value".into();
|
|
if let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = project.elements.get_mut(&message).unwrap().shared_mut()
|
|
{
|
|
values.reverse();
|
|
}
|
|
let reordered = update_bundle(&changed, &project, ProjectLimits::default()).unwrap();
|
|
let after = &reordered.bundle.files["conversation.arcadia"];
|
|
assert!(after.contains("field @ // first field proof\n renamed_value; expression // first expression proof\n \"changed expression\";"));
|
|
assert!(
|
|
after.find("// second annotation").unwrap() < after.find("// first annotation").unwrap()
|
|
);
|
|
let mut old_comments = comments(¤t.bundle.files["conversation.arcadia"]);
|
|
let mut new_comments = comments(after);
|
|
old_comments.sort();
|
|
new_comments.sort();
|
|
assert_eq!(old_comments, new_comments);
|
|
for (id, location) in ¤t.index.declarations {
|
|
assert_eq!(reordered.index.declarations[id].path, location.path);
|
|
}
|
|
assert_eq!(reordered.decoded.project, project);
|
|
}
|
|
|
|
#[test]
|
|
fn added_list_items_empty_lists_and_literal_changes_preserve_existing_annotations() {
|
|
let input = bundle(
|
|
"arcadia 5; project p \"P\" { include \"types.arcadia\"; }",
|
|
&[(
|
|
"types.arcadia",
|
|
"oa_data_type values \"Values\" { definition { Enumeration { literals [ // enumeration rationale\n ]; }; }; }\n",
|
|
)],
|
|
);
|
|
let empty = compile(&input);
|
|
let mut project = empty.decoded.project.clone();
|
|
let values = id(&project, "values");
|
|
if let Some(SharedBody::DataType { definition, .. }) =
|
|
project.elements.get_mut(&values).unwrap().shared_mut()
|
|
{
|
|
*definition = DataTypeDefinition::Enumeration {
|
|
literals: vec!["Alpha".into(), "Beta".into()],
|
|
};
|
|
} else {
|
|
panic!("enumeration");
|
|
}
|
|
let added = update_bundle(&empty, &project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(
|
|
comments(&added.bundle.files["types.arcadia"]),
|
|
comments(&empty.bundle.files["types.arcadia"])
|
|
);
|
|
let mut annotated = added.bundle.clone();
|
|
annotated
|
|
.files
|
|
.get_mut("types.arcadia")
|
|
.unwrap()
|
|
.clone_from(
|
|
&added.bundle.files["types.arcadia"]
|
|
.replace("\"Alpha\"", "\"Alpha\" // alpha explanation\n")
|
|
.replace("\"Beta\"", "\"Beta\" // beta explanation\n"),
|
|
);
|
|
let annotated = compile(&annotated);
|
|
if let Some(SharedBody::DataType { definition, .. }) =
|
|
project.elements.get_mut(&values).unwrap().shared_mut()
|
|
{
|
|
*definition = DataTypeDefinition::Enumeration {
|
|
literals: vec!["Beta".into(), "Gamma".into()],
|
|
};
|
|
}
|
|
let changed = update_bundle(&annotated, &project, ProjectLimits::default()).unwrap();
|
|
let mut before = comments(&annotated.bundle.files["types.arcadia"]);
|
|
let mut after = comments(&changed.bundle.files["types.arcadia"]);
|
|
before.sort();
|
|
after.sort();
|
|
assert_eq!(before, after);
|
|
assert_eq!(changed.decoded.project, project);
|
|
}
|