457 lines
17 KiB
Rust
457 lines
17 KiB
Rust
use oa_core::{
|
|
Id, PortContract, Project, SharedBody,
|
|
language::{self, TokenClass},
|
|
source_project::SourceBundle,
|
|
};
|
|
use source_authoring_experiment::{compile, writeback::writeback};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn id(project: &Project, alias: &str) -> Id {
|
|
project
|
|
.identities()
|
|
.into_iter()
|
|
.find(|(_, a)| *a == alias)
|
|
.unwrap()
|
|
.0
|
|
}
|
|
fn fragments(source: &str, path: &str, extras: BTreeMap<String, String>) -> SourceBundle {
|
|
let start = source.find('{').unwrap() + 1;
|
|
let end = source.rfind('}').unwrap();
|
|
let mut files = extras;
|
|
files.insert(path.into(), source[start..end].into());
|
|
let includes = files
|
|
.keys()
|
|
.map(|path| format!(" include \"{path}\";\n"))
|
|
.collect::<String>();
|
|
files.insert(
|
|
"project.arcadia".into(),
|
|
format!("{}\n{includes}}}\n", &source[..start]),
|
|
);
|
|
SourceBundle {
|
|
entry: "project.arcadia".into(),
|
|
files,
|
|
}
|
|
}
|
|
fn sa() -> SourceBundle {
|
|
let source=include_str!("../fixtures/sa-b.arcadia")
|
|
.replace(" sa_function send_report", " // Keep reporting allocation explicit; this note belongs to the function.\n sa_function send_report")
|
|
.replace(" output report_out", " // This note belongs to the payload port.\n output report_out")
|
|
.replace(" sa_function assess", " // Assessment is independently owned.\n sa_function assess")
|
|
.replace(" output report_out \"Report output\" id \"00000000-0000-4000-8000-000000000208\" items [@report_data];", " output report_out \"Report output\" id \"00000000-0000-4000-8000-000000000208\" items [@report_data]; // Primary payload\n output diagnostic_out \"Diagnostic output\" id \"00000000-0000-4000-8000-000000000222\" items [\n @report_data // Keep this contract rationale\n ];");
|
|
fragments(&source,"domain/system.arcadia",BTreeMap::from([
|
|
("domain/other.arcadia".into(),"// Other analysis aggregate\nsa_function alternate \"Alternate analysis\" id \"00000000-0000-4000-8000-000000000223\" {\n description \"A separate analysis context\";\n}\nsa_exchange_item location_data \"Location data\" id \"00000000-0000-4000-8000-000000000224\" {}\n".into()),
|
|
("views/reporting.arcadia".into(),r#"// View rationale survives a geometry edit.
|
|
diagram reporting_view "Reporting view" id "00000000-0000-4000-8000-000000000220" { layer Sa; projection FunctionalDataflow; }
|
|
node send_placement id "00000000-0000-4000-8000-000000000221" {
|
|
diagram @reporting_view;
|
|
element @send_report;
|
|
position { y 32.5; // Y rationale
|
|
x -0.0; }; // Preserve the author's field order
|
|
size { width 200.0; height 100.0; };
|
|
collapsed false;
|
|
}
|
|
"#.into()),
|
|
]))
|
|
}
|
|
fn scenario() -> SourceBundle {
|
|
let source=include_str!("../fixtures/scenario-b.arcadia")
|
|
.replace(" oa_occurrence initial_report", " // Initial request retains its call identity.\n oa_occurrence initial_report")
|
|
.replace(" oa_occurrence accepted", " // Acknowledgement rationale.\n oa_occurrence accepted")
|
|
.replace(" oa_occurrence update_report", " // Newer position update is its own occurrence.\n oa_occurrence update_report")
|
|
.replace(" precondition", " // This precondition travels with the update.\n precondition");
|
|
fragments(
|
|
&source,
|
|
"behavior/reports.arcadia",
|
|
BTreeMap::from([(
|
|
"unrelated.arcadia".into(),
|
|
"// This file intentionally contains only narrative comments.\n".into(),
|
|
)]),
|
|
)
|
|
}
|
|
fn comment_strings(bundle: &SourceBundle) -> Vec<String> {
|
|
let mut comments = bundle
|
|
.files
|
|
.values()
|
|
.flat_map(|source| {
|
|
language::tokenize(source)
|
|
.into_iter()
|
|
.filter(|token| token.class == TokenClass::Comment)
|
|
.map(|token| source[token.span.start..token.span.end].to_owned())
|
|
})
|
|
.collect::<Vec<_>>();
|
|
comments.sort();
|
|
comments
|
|
}
|
|
fn verify(bundle: &SourceBundle, expected: &Project) -> SourceBundle {
|
|
let original = bundle.clone();
|
|
let output = writeback(bundle, expected).unwrap();
|
|
assert_eq!(*bundle, original, "Caller input changed");
|
|
assert_eq!(compile(&output).unwrap(), *expected);
|
|
assert_eq!(comment_strings(&output), comment_strings(bundle));
|
|
assert_eq!(
|
|
writeback(&output, expected).unwrap(),
|
|
output,
|
|
"Repeated writeback churned source"
|
|
);
|
|
output
|
|
}
|
|
fn changed_files(before: &SourceBundle, after: &SourceBundle) -> Vec<String> {
|
|
before
|
|
.files
|
|
.iter()
|
|
.filter(|(path, text)| after.files.get(*path) != Some(*text))
|
|
.map(|(path, _)| path.clone())
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn no_op_and_owner_rename_preserve_authored_structure_comments_and_other_files() {
|
|
let mut bundle = sa();
|
|
// Equivalent but noncanonical lexical forms must survive an unrelated edit.
|
|
*bundle.files.get_mut("domain/other.arcadia").unwrap() = bundle.files["domain/other.arcadia"]
|
|
.replace("Alternate analysis", r"Alternate \u0061nalysis");
|
|
let old = compile(&bundle).unwrap();
|
|
assert_eq!(writeback(&bundle, &old).unwrap(), bundle);
|
|
let mut candidate = old.clone();
|
|
candidate
|
|
.elements
|
|
.get_mut(&id(&old, "manage"))
|
|
.unwrap()
|
|
.name = "Manage emergency reporting".into();
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(changed_files(&bundle, &output), ["domain/system.arcadia"]);
|
|
assert_eq!(
|
|
output.files["domain/system.arcadia"],
|
|
bundle.files["domain/system.arcadia"]
|
|
.replace("\"Manage reporting\"", "\"Manage emergency reporting\"")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn owner_alias_rename_updates_shorthand_and_cross_file_references_only() {
|
|
let bundle = sa();
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
candidate
|
|
.elements
|
|
.get_mut(&id(&old, "send_report"))
|
|
.unwrap()
|
|
.alias = "transmit_report".into();
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(
|
|
changed_files(&bundle, &output),
|
|
["domain/system.arcadia", "views/reporting.arcadia"]
|
|
);
|
|
for path in &changed_files(&bundle, &output) {
|
|
assert_eq!(
|
|
output.files[path],
|
|
bundle.files[path].replace("send_report", "transmit_report")
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn simultaneous_alias_swap_preserves_identity_and_does_not_rewrite_comments_or_strings() {
|
|
let mut bundle = sa();
|
|
*bundle.files.get_mut("domain/other.arcadia").unwrap() = bundle.files["domain/other.arcadia"]
|
|
.replace(
|
|
"A separate analysis context",
|
|
"@send_report and @assess are prose, not references",
|
|
);
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
candidate
|
|
.elements
|
|
.get_mut(&id(&old, "send_report"))
|
|
.unwrap()
|
|
.alias = "assess".into();
|
|
candidate
|
|
.elements
|
|
.get_mut(&id(&old, "assess"))
|
|
.unwrap()
|
|
.alias = "send_report".into();
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(
|
|
output.files["domain/other.arcadia"],
|
|
bundle.files["domain/other.arcadia"]
|
|
);
|
|
assert_eq!(old.relations, candidate.relations);
|
|
}
|
|
|
|
#[test]
|
|
fn raw_component_contract_edit_preserves_nested_comments_and_authored_field_order() {
|
|
let mut bundle = sa();
|
|
*bundle.files.get_mut("domain/system.arcadia").unwrap() = bundle.files["domain/system.arcadia"]
|
|
.replace(
|
|
"kind Flow; orientation Out;",
|
|
"orientation Out; // Keep orientation reasoning\nkind Flow;",
|
|
);
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
if let Some(SharedBody::Port {
|
|
contract: PortContract::Component { orientation, .. },
|
|
..
|
|
}) = candidate
|
|
.elements
|
|
.get_mut(&id(&old, "vessel_out"))
|
|
.unwrap()
|
|
.shared_mut()
|
|
{
|
|
*orientation = oa_core::ComponentPortOrientation::InOut;
|
|
}
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(changed_files(&bundle, &output), ["domain/system.arcadia"]);
|
|
assert_eq!(
|
|
output.files["domain/system.arcadia"],
|
|
bundle.files["domain/system.arcadia"].replace("orientation Out;", "orientation InOut;")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reparent_and_rename_in_one_model_transaction_absorb_only_the_moved_subtree_edits() {
|
|
let bundle = sa();
|
|
let old = compile(&bundle).unwrap();
|
|
let mut moved = old.elements[&id(&old, "send_report")].clone();
|
|
moved.name = "Transmit emergency report".into();
|
|
moved.parent = Some(id(&old, "alternate"));
|
|
let candidate =
|
|
oa_core::execute_batch(&old, &[oa_core::Command::EditElement(moved.id, moved)]).unwrap();
|
|
let output = verify(&bundle, &candidate);
|
|
assert!(output.files["domain/other.arcadia"].contains("\"Transmit emergency report\""));
|
|
assert_eq!(
|
|
output.files["views/reporting.arcadia"],
|
|
bundle.files["views/reporting.arcadia"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn function_port_direction_and_item_addition_keep_shorthand_and_inline_rationale() {
|
|
let bundle = sa();
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
let port = id(&old, "diagnostic_out");
|
|
let payload = id(&old, "report_data");
|
|
let location = id(&old, "location_data");
|
|
if let Some(SharedBody::Port { contract, .. }) =
|
|
candidate.elements.get_mut(&port).unwrap().shared_mut()
|
|
{
|
|
*contract = PortContract::FunctionInput {
|
|
incoming_items: vec![payload, location],
|
|
};
|
|
}
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(changed_files(&bundle, &output), ["domain/system.arcadia"]);
|
|
let text = &output.files["domain/system.arcadia"];
|
|
assert!(text.contains("input diagnostic_out"));
|
|
assert!(!text.contains("contract { FunctionInput"));
|
|
assert!(text.contains("@report_data, // Keep this contract rationale"));
|
|
assert!(text.contains("@location_data"));
|
|
let untouched_prefix = bundle.files["domain/system.arcadia"]
|
|
.split("output diagnostic_out")
|
|
.next()
|
|
.unwrap();
|
|
assert!(text.starts_with(untouched_prefix));
|
|
}
|
|
|
|
#[test]
|
|
fn widening_an_input_contract_does_not_reformat_its_existing_exchange() {
|
|
let bundle = sa();
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
// Increasing the accepted input contract does not change exchange semantics.
|
|
let input = id(&old, "report_in");
|
|
let payload = id(&old, "report_data");
|
|
let location = id(&old, "location_data");
|
|
if let Some(SharedBody::Port { contract, .. }) =
|
|
candidate.elements.get_mut(&input).unwrap().shared_mut()
|
|
{
|
|
*contract = PortContract::FunctionInput {
|
|
incoming_items: vec![payload, location],
|
|
};
|
|
}
|
|
let output = verify(&bundle, &candidate);
|
|
let before_exchange = bundle.files["domain/system.arcadia"]
|
|
.lines()
|
|
.find(|line| line.contains("functional_exchange report_flow"))
|
|
.unwrap();
|
|
assert!(
|
|
output.files["domain/system.arcadia"]
|
|
.lines()
|
|
.any(|line| line == before_exchange)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn reordering_occurrences_moves_their_rationale_and_retains_step_blocks() {
|
|
let bundle = scenario();
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
candidate
|
|
.elements
|
|
.get_mut(&id(&old, "reports"))
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.rotate_right(1);
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(
|
|
changed_files(&bundle, &output),
|
|
["behavior/reports.arcadia"]
|
|
);
|
|
let source = &output.files["behavior/reports.arcadia"];
|
|
assert!(source.contains("steps {"));
|
|
assert!(!source.contains("steps ["));
|
|
assert!(
|
|
source.find("// Newer position update").unwrap()
|
|
< source.find("// Initial request").unwrap()
|
|
);
|
|
assert!(
|
|
source.find("// This precondition travels").unwrap()
|
|
> source.find("oa_occurrence update_report").unwrap()
|
|
);
|
|
assert_eq!(old.relations, candidate.relations);
|
|
}
|
|
|
|
#[test]
|
|
fn adding_an_occurrence_uses_its_new_identity_and_preserves_existing_blocks() {
|
|
let bundle = scenario();
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
let owner = id(&old, "reports");
|
|
let mut occurrence = candidate.elements[&id(&old, "update_report")].clone();
|
|
occurrence.id = "00000000-0000-4000-8000-000000000316".parse().unwrap();
|
|
occurrence.alias = "further_report".into();
|
|
occurrence.name = "Further report".into();
|
|
candidate
|
|
.elements
|
|
.get_mut(&owner)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.push(occurrence.id);
|
|
candidate.elements.insert(occurrence.id, occurrence);
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(
|
|
changed_files(&bundle, &output),
|
|
["behavior/reports.arcadia"]
|
|
);
|
|
let source = &output.files["behavior/reports.arcadia"];
|
|
assert!(source.contains("oa_occurrence further_report"));
|
|
assert!(!source.contains("parent @reports"));
|
|
assert!(source.contains("steps {"));
|
|
let original = &bundle.files["behavior/reports.arcadia"];
|
|
let before_close = original.rfind(" };").unwrap();
|
|
assert!(source.starts_with(&original[..before_close]));
|
|
}
|
|
|
|
#[test]
|
|
fn semantic_reparent_moves_nested_function_ports_and_comments_across_files() {
|
|
let bundle = sa();
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
candidate
|
|
.elements
|
|
.get_mut(&id(&old, "send_report"))
|
|
.unwrap()
|
|
.parent = Some(id(&old, "alternate"));
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(
|
|
changed_files(&bundle, &output),
|
|
["domain/other.arcadia", "domain/system.arcadia"]
|
|
);
|
|
assert!(!output.files["domain/system.arcadia"].contains("sa_function send_report"));
|
|
assert!(
|
|
output.files["domain/other.arcadia"].contains(" // Keep reporting allocation explicit")
|
|
);
|
|
assert!(output.files["domain/other.arcadia"].contains(" sa_function send_report"));
|
|
assert!(output.files["domain/other.arcadia"].contains(" output report_out"));
|
|
assert_eq!(
|
|
old.relations, candidate.relations,
|
|
"Reparenting changed explicit allocations"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn diagram_geometry_and_existing_a_fields_preserve_comments_and_field_order() {
|
|
let bundle = sa();
|
|
let old = compile(&bundle).unwrap();
|
|
let diagram = id(&old, "reporting_view");
|
|
let node = id(&old, "send_placement");
|
|
let candidate = oa_core::execute_batch(
|
|
&old,
|
|
&[oa_core::Command::MoveNode {
|
|
diagram,
|
|
node,
|
|
position: oa_core::Position { x: 72.25, y: 99.5 },
|
|
with_descendants: false,
|
|
}],
|
|
)
|
|
.unwrap();
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(changed_files(&bundle, &output), ["views/reporting.arcadia"]);
|
|
assert_eq!(
|
|
output.files["views/reporting.arcadia"],
|
|
bundle.files["views/reporting.arcadia"]
|
|
.replace("y 32.5", "y 99.5")
|
|
.replace("x -0.0", "x 72.25")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn raw_a_parent_changes_preserve_file_organization_and_declaration_comments() {
|
|
let source = include_str!("../fixtures/oa-a.arcadia").replace(
|
|
" oa_activity report",
|
|
" // Reparent the activity, keep this explanation.\n oa_activity report",
|
|
);
|
|
let bundle = fragments(&source, "operations.arcadia", BTreeMap::new());
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
candidate
|
|
.elements
|
|
.get_mut(&id(&old, "report"))
|
|
.unwrap()
|
|
.parent = None;
|
|
let output = verify(&bundle, &candidate);
|
|
assert_eq!(
|
|
output.files["operations.arcadia"],
|
|
bundle.files["operations.arcadia"].replacen("parent @respond", "parent none", 1)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ambiguous_comment_ownership_and_invalid_candidates_fail_without_mutation() {
|
|
let mut bundle = scenario();
|
|
*bundle.files.get_mut("behavior/reports.arcadia").unwrap()=bundle.files["behavior/reports.arcadia"].replace(" // Acknowledgement rationale.","\n // Unattached group rationale.\n\n // Acknowledgement rationale.");
|
|
let original = bundle.clone();
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
candidate
|
|
.elements
|
|
.get_mut(&id(&old, "reports"))
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.rotate_right(1);
|
|
assert!(
|
|
writeback(&bundle, &candidate)
|
|
.unwrap_err()
|
|
.contains("Unattached comments")
|
|
);
|
|
assert_eq!(bundle, original);
|
|
let bundle = sa();
|
|
let original = bundle.clone();
|
|
let old = compile(&bundle).unwrap();
|
|
let mut candidate = old.clone();
|
|
candidate
|
|
.elements
|
|
.get_mut(&id(&old, "send_report"))
|
|
.unwrap()
|
|
.parent = Some(id(&old, "center"));
|
|
assert!(
|
|
writeback(&bundle, &candidate)
|
|
.unwrap_err()
|
|
.contains("Invalid candidate")
|
|
);
|
|
assert_eq!(bundle, original);
|
|
}
|