Files
ARCADIA-Studio/experiments/source-authoring/tests/writeback.rs
T

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);
}