Add private consumed rename emission with bounded owned output

This commit is contained in:
2026-09-24 18:07:16 -07:00
parent fa7a980418
commit 19a4082117
5 changed files with 890 additions and 1 deletions
@@ -660,3 +660,70 @@ fn ordinary_position_key_copy_is_counted_at_existing_postparse_text_phase() {
"Source8 index/output byte limit exceeded.",
);
}
#[test]
fn emission_refuses_changed_carried_even_zero_and_preserves_noop_guards() {
for retained in [false, true] {
let held = owner(empty(), profile(100, 4096));
let seed =
parse_with_profile(&held.bundle, ProjectLimits::default(), profile(100, 4096)).unwrap();
for changed in [false, true] {
let input = if retained {
held.input()
} else {
CarriedInput::borrowed(&held.bundle, &seed)
};
let parsed = parse_carried(input).unwrap();
let original_guard = parsed.carried.as_ref().unwrap().slots();
let resolved = project::resolve(parsed).unwrap();
let source = resolved.index().unwrap().index_ordinary().unwrap();
let mut desired = source.project().clone();
if changed {
desired.alias = "next".into();
}
let plan = source.plan_rename(&desired).unwrap();
if changed {
let error = match plan.emit_changed_unverified(&held.bundle) {
Ok(_) => panic!("carried changed emission must refuse"),
Err(e) => e,
};
diagnostic(
&error.diagnostics[0],
"main.arcadia",
0,
0,
"Changed emission requires original legacy syntax.",
);
} else {
let source = plan.into_unchanged(&held.bundle).unwrap();
assert!(std::ptr::eq(source.bundle(), &held.bundle));
// The intact owner can be consumed through another no-op plan;
// no production getter exposes or replaces its guard.
let source = source
.plan_rename(&desired)
.unwrap()
.into_unchanged(&held.bundle)
.unwrap();
assert!(std::ptr::eq(source.bundle(), &held.bundle));
let mut changed_desired = desired.clone();
changed_desired.alias = "next".into();
assert!(
source
.plan_rename(&changed_desired)
.unwrap()
.emit_changed_unverified(&held.bundle)
.is_err()
);
}
assert_eq!(
original_guard,
if retained {
held.retained
} else {
Slots::default()
}
);
assert_eq!(held.bundle.files["main.arcadia"], doc(""));
}
}
}
@@ -1,4 +1,5 @@
//! Complete-target rename planning only; no output emission or admission.
//! Complete-target rename planning and private unverified emission; no admission.
pub(in crate::language::analysis_source) mod emission;
use super::*;
use crate::source_project::SourceBundle;
use lower::Target;
@@ -0,0 +1,152 @@
//! Owned output only. No parser, extraction or accepted-result conversion.
use super::*;
use crate::language::analysis_source::budget::Limits;
use crate::source_project::ProjectLimits;
pub(in crate::language::analysis_source) struct PendingRename<'t> {
bundle: SourceBundle,
limits: Limits,
project_limits: ProjectLimits,
retained: Slots,
expression_nodes: usize,
desired: &'t Project,
}
impl<'s, 't> RenamePlan<'s, 't> {
pub fn into_unchanged(
self,
current: &SourceBundle,
) -> Result<AuthoredMixedProject<'s>, Failure> {
self.validate_current(current)?;
self.validate()?;
if !self.names.is_empty() {
return Err(self.failure(None, error(Span::default(), "Rename plan changes names.")));
}
Ok(self.source)
}
pub fn emit_changed_unverified(
mut self,
current: &SourceBundle,
) -> Result<PendingRename<'t>, Failure> {
self.validate_current(current)?;
self.validate()?;
if self.names.is_empty() {
return Err(self.failure(None, error(Span::default(), "Rename plan is unchanged.")));
}
if self.source.indexed.resolved.syntax.carried.is_some() {
return Err(self.failure(
None,
error(
Span::default(),
"Changed emission requires original legacy syntax.",
),
));
}
let total = self.output_bytes()?;
let retained =
output_slots(self.source.bundle(), total).map_err(|e| self.failure(None, e))?;
let resolved = &mut self.source.indexed.resolved;
resolved
.storage
.reserve(&resolved.syntax.budget, retained, Span::default())
.map_err(|e| self.failure(None, e))?;
// The complete output reservation precedes every owned byte and entry.
let mut files = BTreeMap::new();
let mut produced = 0;
for (file, path, original) in self.source.files() {
let result = (|| {
let edits = || {
self.edits
.range((file, 0, 0)..=(file, usize::MAX, usize::MAX))
};
let mut removed = 0;
let mut replacement = 0;
for (&(_, start, end), target) in edits() {
let at = Span { start, end };
removed = add(
removed,
end.checked_sub(start)
.ok_or_else(|| error(at, "Invalid rename token size."))?,
at,
)?;
replacement = add(replacement, self.names[target].len(), at)?;
}
let size = add(
original.len().checked_sub(removed).ok_or_else(|| {
error(Span::default(), "Rename removals exceed original file.")
})?,
replacement,
Span::default(),
)?;
let mut output = String::with_capacity(size);
let mut cursor = 0;
for (&(_, start, end), target) in edits() {
let at = Span { start, end };
output.push_str(
original
.get(cursor..start)
.ok_or_else(|| error(at, "Invalid rename output range."))?,
);
output.push_str(self.names[target]);
cursor = end;
}
output.push_str(
original
.get(cursor..)
.ok_or_else(|| error(Span::default(), "Invalid rename output tail."))?,
);
if output.len() != size {
return Err(error(Span::default(), "Rename output length mismatch."));
}
produced = add(produced, size, Span::default())?;
Ok(output)
})();
let output = result.map_err(|e| self.failure(Some(file), e))?;
files.insert(path.to_owned(), output);
}
if files.len() != retained.entries || produced != total {
return Err(self.failure(None, error(Span::default(), "Incomplete rename output.")));
}
let bundle = SourceBundle {
entry: self.source.bundle().entry.clone(),
files,
};
let limits = self.source.indexed.resolved.syntax.budget.limits();
let project_limits = self.source.indexed.resolved.syntax.limits;
let expression_nodes = self.source.indexed.resolved.expressions.len();
let Self {
source,
desired,
names,
edits,
} = self;
drop(names);
drop(edits);
drop(source);
Ok(PendingRename {
bundle,
limits,
project_limits,
retained,
expression_nodes,
desired,
})
}
}
fn output_slots(bundle: &SourceBundle, total: usize) -> Result<Slots, TypedError> {
let mut text = add(total, bundle.entry.len(), Span::default())?;
for path in bundle.files.keys() {
text = add(text, path.len(), Span::default())?;
}
Ok(Slots {
entries: bundle.files.len(),
occurrences: 0,
text,
})
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,646 @@
use super::*;
use crate::Layer;
use crate::language::ParseLimits;
use crate::language::analysis_source::mixed::{parse_with_profile, project};
use project::tests::{accepted, id, rich_bundle, rich_model, single};
fn authored(bundle: &SourceBundle) -> AuthoredMixedProject<'_> {
accepted(bundle).index().unwrap().index_ordinary().unwrap()
}
fn profile(entries: usize, bytes: usize) -> Limits {
Limits {
entries,
bytes,
..Limits::source8(ParseLimits::UNLIMITED)
}
}
fn limited(
bundle: &SourceBundle,
limits: Limits,
policy: ProjectLimits,
) -> AuthoredMixedProject<'_> {
project::resolve(parse_with_profile(bundle, policy, limits).unwrap())
.unwrap()
.index()
.unwrap()
.index_ordinary()
.unwrap()
}
fn chain() -> SourceBundle {
let mut b = single("include \"part.arcadia\";");
b.files.insert(
"part.arcadia".into(),
"include \"last.arcadia\";\r\n".into(),
);
b.files.insert("last.arcadia".into(), "//é\r\n".into());
b
}
fn failure<T>(result: Result<T, Failure>, text: &str) -> Failure {
let e = match result {
Ok(_) => panic!("unexpected success"),
Err(e) => e,
};
assert_eq!(e.diagnostics.len(), 1);
assert!(e.diagnostics[0].diagnostic.message.contains(text), "{e:?}");
e
}
fn same_policy(output: &PendingRename<'_>, limits: Limits, policy: ProjectLimits) {
let a = output.limits;
assert_eq!(
[a.bytes, a.tokens, a.entries, a.references, a.frames],
[
limits.bytes,
limits.tokens,
limits.entries,
limits.references,
limits.frames
]
);
assert_eq!(a.numeric_units, limits.numeric_units);
let a = output.project_limits;
assert_eq!(
[
a.max_files,
a.max_include_depth,
a.max_include_edges,
a.model.max_source_bytes,
a.model.max_graph_objects,
a.model.max_records,
a.model.max_step_references
],
[
policy.max_files,
policy.max_include_depth,
policy.max_include_edges,
policy.model.max_source_bytes,
policy.model.max_graph_objects,
policy.model.max_records,
policy.model.max_step_references
]
);
}
const NEXT: &str =
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\n\n}\n";
const NEXT_CHAIN: &str = "arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\ninclude \"part.arcadia\";\n}\n";
#[test]
fn exact_real_single_and_chain_overlap_caps_and_complete_owned_bytes() {
for (bundle, entries, bytes, before, output_entries, output_text, expected) in [
(
single(""),
17,
114,
Slots {
entries: 16,
occurrences: 0,
text: 9,
},
1,
105,
NEXT,
),
(
chain(),
29,
216,
Slots {
entries: 26,
occurrences: 0,
text: 33,
},
3,
183,
NEXT_CHAIN,
),
] {
for (entry_cap, byte_cap, success) in [
(entries, bytes, true),
(entries - 1, bytes, false),
(entries, bytes - 1, false),
] {
let limits = profile(entry_cap, byte_cap);
let policy = ProjectLimits::default();
let source = limited(&bundle, limits, policy);
let mut desired = source.project().clone();
desired.alias = "next".into();
let plan = source.plan_rename(&desired).unwrap();
assert_eq!(plan.source.indexed.resolved.storage.live, before);
let result = plan.emit_changed_unverified(&bundle);
if success {
let output = result.unwrap();
assert_eq!(
output.retained,
Slots {
entries: output_entries,
occurrences: 0,
text: output_text
}
);
assert_eq!(output.bundle.entry, "main.arcadia");
let mut expected_files = BTreeMap::from([("main.arcadia".into(), expected.into())]);
if output_entries == 3 {
expected_files.insert(
"part.arcadia".into(),
"include \"last.arcadia\";\r\n".into(),
);
expected_files.insert("last.arcadia".into(), "//é\r\n".into());
}
assert_eq!(output.bundle.files, expected_files);
assert_eq!(output.expression_nodes, 0);
assert!(std::ptr::eq(output.desired, &desired));
same_policy(&output, limits, policy);
} else {
let e = failure(result, "limit exceeded");
assert_eq!(e.diagnostics[0].path, "main.arcadia");
assert_eq!(e.diagnostics[0].diagnostic.span, Span::default());
}
}
}
}
#[test]
fn noop_returns_identical_owner_below_output_overlap_and_releases_desired_borrow() {
for (bundle, entries, bytes) in [(single(""), 16, 78), (chain(), 26, 132)] {
let source = limited(&bundle, profile(entries, bytes), ProjectLimits::default());
let storage = source.indexed.resolved.storage.live;
let usage = source.indexed.resolved.syntax.budget.usage;
let numeric = source.indexed.resolved.syntax.budget.numeric_units();
let names = source.indexed.data.names.values().next().unwrap().as_ptr();
let warnings = source.warnings().to_vec();
let result = {
let desired = source.project().clone();
source
.plan_rename(&desired)
.unwrap()
.into_unchanged(&bundle)
.unwrap()
};
assert!(std::ptr::eq(result.bundle(), &bundle));
assert_eq!(
result.indexed.data.names.values().next().unwrap().as_ptr(),
names
);
assert_eq!(result.indexed.resolved.storage.live, storage);
assert_eq!(result.indexed.resolved.syntax.budget.usage, usage);
assert_eq!(
result.indexed.resolved.syntax.budget.numeric_units(),
numeric
);
assert_eq!(result.warnings(), warnings);
}
}
#[test]
fn wrong_branches_and_complete_stale_current_refuse_before_output() {
let bundle = chain();
let original = authored(&bundle).project().clone();
let mut desired = original.clone();
desired.alias = "next".into();
failure(
authored(&bundle)
.plan_rename(&original)
.unwrap()
.emit_changed_unverified(&bundle),
"unchanged",
);
failure(
authored(&bundle)
.plan_rename(&desired)
.unwrap()
.into_unchanged(&bundle),
"changes names",
);
for case in 0..6 {
let mut current = bundle.clone();
match case {
0 => current.entry = "last.arcadia".into(),
1 => current.files.get_mut("last.arcadia").unwrap().push(' '),
2 => current.files.get_mut("main.arcadia").unwrap().push(' '),
3 => {
current.files.remove("last.arcadia");
}
4 => {
current.files.insert("extra.arcadia".into(), String::new());
}
_ => {
let text = current.files.remove("last.arcadia").unwrap();
current.files.insert("renamed.arcadia".into(), text);
}
}
failure(
authored(&bundle)
.plan_rename(&original)
.unwrap()
.into_unchanged(&current),
"Current complete bundle",
);
failure(
authored(&bundle)
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&current),
"Current complete bundle",
);
}
let current = bundle.clone();
assert_eq!(
authored(&bundle)
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&current)
.unwrap()
.bundle
.files["main.arcadia"],
NEXT_CHAIN
);
}
#[test]
fn existing_edit_validation_runs_before_both_transitions() {
let bundle = single("");
let original = authored(&bundle).project().clone();
let mut desired = original.clone();
desired.alias = "next".into();
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
plan.edits.clear();
failure(
plan.emit_changed_unverified(&bundle),
"missing a required target token",
);
let mut plan = authored(&bundle).plan_rename(&original).unwrap();
plan.edits.insert((0, 19, 20), Target::Global(id(1)));
failure(plan.into_unchanged(&bundle), "unchanged name");
let mut plan = authored(&bundle).plan_rename(&desired).unwrap();
plan.edits.clear();
plan.edits
.insert((0, usize::MAX, usize::MAX), Target::Global(id(1)));
failure(plan.emit_changed_unverified(&bundle), "nonempty");
// Arithmetic-only fault injection, not a real bundle large enough to overflow.
assert!(output_slots(&bundle, usize::MAX).is_err());
}
#[test]
fn exact_all_policy_fields_and_pending_outlives_original_bundle() {
let desired = {
let b = single("");
let mut p = authored(&b).project().clone();
p.alias = "next".into();
p
};
let limits = Limits {
bytes: 2048,
tokens: 101,
entries: 99,
references: 31,
frames: 17,
numeric_units: 8192,
};
let policy = ProjectLimits {
max_files: 7,
max_include_depth: 3,
max_include_edges: 5,
model: ParseLimits {
max_source_bytes: 3000,
max_graph_objects: 67,
max_records: 123,
max_step_references: 37,
},
};
let pending = {
let b = single("");
limited(&b, limits, policy)
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&b)
.unwrap()
};
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
same_policy(&pending, limits, policy);
}
#[test]
fn scoped_small_stack_changed_unchanged_and_failure_drop() {
let bundle = single("");
let original = authored(&bundle).project().clone();
let mut changed = original.clone();
changed.alias = "next".into();
std::thread::scope(|scope| {
for mode in 0..3 {
let plan = limited(
&bundle,
profile(17, if mode == 2 { 113 } else { 114 }),
ProjectLimits::default(),
)
.plan_rename(if mode == 1 { &original } else { &changed })
.unwrap();
let b = &bundle;
std::thread::Builder::new()
.stack_size(128 * 1024)
.spawn_scoped(scope, move || {
if mode == 1 {
let owner = plan.into_unchanged(b).unwrap();
assert!(std::ptr::eq(owner.bundle(), b));
drop(owner);
} else if mode == 0 {
let pending = plan.emit_changed_unverified(b).unwrap();
assert_eq!(pending.bundle.files["main.arcadia"], NEXT);
drop(pending);
} else {
drop(failure(plan.emit_changed_unverified(b), "limit exceeded"));
}
})
.unwrap()
.join()
.unwrap();
}
});
for (expression, result, nodes) in [
(
format!("{}1{}", "abs(".repeat(127), ")".repeat(127)),
"Number",
128,
),
(
format!("[{}]", vec!["1"; 128].join(", ")),
"List(Number)",
129,
),
] {
let body = format!(
"calculation c \"C\" id \"00000000-0000-0000-0000-000000000002\" {{ result {result}; body {expression}; }}"
);
let bundle = single(&body);
let before = bundle.clone();
let expected = format!(
"arcadia 8;\nproject next \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\n{body}\n}}\n"
);
for changed in [false, true] {
// Complete prerequisites stay outside the bounded emitter worker.
let source = authored(&bundle);
assert_eq!(source.indexed.resolved.expressions.len(), nodes);
let mut desired = source.project().clone();
if changed {
desired.alias = "next".into();
}
let plan = source.plan_rename(&desired).unwrap();
std::thread::scope(|scope| {
let bundle = &bundle;
let expected = &expected;
std::thread::Builder::new()
.stack_size(128 * 1024)
.spawn_scoped(scope, move || {
if changed {
let pending = plan.emit_changed_unverified(bundle).unwrap();
assert_eq!(pending.expression_nodes, nodes);
assert_eq!(&pending.bundle.files["main.arcadia"], expected);
drop(pending);
} else {
let owner = plan.into_unchanged(bundle).unwrap();
assert!(std::ptr::eq(owner.bundle(), bundle));
assert_eq!(owner.indexed.resolved.expressions.len(), nodes);
drop(owner);
}
})
.unwrap()
.join()
.unwrap();
});
assert_eq!(bundle, before);
}
}
}
fn rich_desired(layer: Layer) -> Project {
let mut p = rich_model(layer);
p.alias = "w".into();
p.elements.get_mut(&id(2)).unwrap().alias = "p".into();
p.elements.get_mut(&id(3)).unwrap().alias = "n".into();
let d = p.diagrams.get_mut(&id(5)).unwrap();
d.alias = "z".into();
d.nodes.get_mut(&id(4)).unwrap().alias = "v".into();
for (n, name) in [(10, "g"), (11, "f")] {
let def = p.definitions.get_mut(&id(n)).unwrap();
def.alias = name.into();
let crate::analysis::DefinitionBody::Calculation(c) = &mut def.body else {
panic!("calculation")
};
c.parameters[0].name = "y".into();
c.parameters[1].name = "x".into();
}
if layer == Layer::Sa {
p.elements.get_mut(&id(6)).unwrap().alias = "u".into();
}
p
}
// Independently authored entire expected files: no edit iterator or output text
// contributes to these strings. The LF/CRLF transform applies to whole literals.
const RICH_A: &str = "calculation g \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y + $x;\n}\n";
const RICH_B: &str = "calculation f \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput y id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput x id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $y - $x;\n}\n";
fn rich_expected(layer: Layer) -> SourceBundle {
let (property, work, diagram, extra) = if layer == Layer::Oa {
("oa_property", "oa_activity", "", "")
} else {
(
"sa_property",
"sa_function",
"layer Sa;",
"sa_system u \"System\" id \"00000000-0000-0000-0000-000000000006\" {}",
)
};
SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([
("a.arcadia".into(), RICH_A.into()),
("b.arcadia".into(), RICH_B.into()),
(
"main.arcadia".into(),
format!(
"arcadia 8;\nproject w \"Project\" id \"00000000-0000-0000-0000-000000000001\" {{\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode v id \"00000000-0000-0000-0000-000000000004\" {{ diagram @z; element @n; }}\n{property} n \"Value\" id \"00000000-0000-0000-0000-000000000003\" {{ subject @p; type Number; base @g(x: 2, y: @f(y: 3, x: 4)) + @g(y: 5, x: 6); }}\n{work} p \"Work\" id \"00000000-0000-0000-0000-000000000002\" {{}}\ndiagram z \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" {{ {diagram} }}\n{extra}\n}}\n"
),
),
]),
}
}
#[test]
fn whole_rich_oa_sa_scoped_global_swaps_and_unicode_crlf_bytes() {
for (layer, bytes, unicode_bytes) in [(Layer::Oa, 900, 943), (Layer::Sa, 974, 1017)] {
for unicode in [false, true] {
let mut bundle = rich_bundle(layer);
let mut expected = rich_expected(layer);
if unicode {
for text in bundle.files.values_mut() {
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
}
for text in expected.files.values_mut() {
*text = format!("// 雨\r\n{}", text.replace('\n', "\r\n"));
}
}
let desired = rich_desired(layer);
let source = authored(&bundle);
let original_project = source.project().clone();
let pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&bundle)
.unwrap();
assert_eq!(pending.bundle, expected);
assert_eq!(pending.expression_nodes, 15);
let count = if unicode { unicode_bytes } else { bytes };
assert_eq!(
pending.retained,
Slots {
entries: 3,
occurrences: 0,
text: count + 42
}
);
assert_eq!(authored(&bundle).project(), &original_project);
assert_eq!(desired, rich_desired(layer));
}
}
}
#[test]
fn four_file_cross_family_swap_preserves_entire_untouched_file_and_comments() {
let bundle=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
("a.arcadia".into(),"calculation f \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
("b.arcadia".into(),"// f and w are comments\r\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @f(); }\r\n}\r\n".into()),
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
])};
let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
("a.arcadia".into(),"calculation w \"F\" id \"00000000-0000-0000-0000-000000000010\" { result Number; body 1; }\r\n".into()),
("b.arcadia".into(),"// f and w are comments\r\noa_activity f \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\r\n".into()),
("main.arcadia".into(),"// 雨\r\narcadia 8;\r\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\r\ninclude \"a.arcadia\"; include \"b.arcadia\"; include \"untouched.arcadia\";\r\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @f; type Number; base @w(); }\r\n}\r\n".into()),
("untouched.arcadia".into(),"// 雪 untouched f w\r\n".into()),
])};
let source = authored(&bundle);
let mut desired = source.project().clone();
desired.definitions.get_mut(&id(10)).unwrap().alias = "w".into();
desired.elements.get_mut(&id(2)).unwrap().alias = "f".into();
let pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&bundle)
.unwrap();
assert_eq!(pending.bundle, expected);
assert_eq!(
pending.retained,
Slots {
entries: 4,
occurrences: 0,
text: 526
}
);
assert_eq!(pending.expression_nodes, 2);
}
const CATEGORY_EXPECTED: &str = r#"arcadia 8;
project a "Authored @p child" id "00000000-0000-0000-0000-000000000001" {
description "Do not index @child, @link or uuid in prose 雨";
oa_activity p "Parent child" id "00000000-0000-0000-0000-000000000002" {
oa_activity implicit "Explicit child" id "00000000-0000-0000-0000-000000000003" { parent @ // @noise child
p; }
oa_activity child "Implicit child" id "00000000-0000-0000-0000-000000000004" { description "child @a"; }
}
sa_function link "Selected" id "00000000-0000-0000-0000-000000000005" {}
interaction s "Link" id "00000000-0000-0000-0000-000000000006" { source @implicit; target @child; }
diagram note "View" id "00000000-0000-0000-0000-000000000007" {
routes [{ edge @s; }];
view_options { hidden_edges [@s]; };
}
artifact d "Evidence" id "00000000-0000-0000-0000-000000000008" { owner @s; uri "docs/@child.md"; }
trace q "Review" id "00000000-0000-0000-0000-000000000009" {
oa @implicit; sa @link; kind Manual;
baseline {
oa { name "child"; description "@child"; };
sa { name "s"; description "@s"; };
semantic { facets [{ Field { Type { oa { Value { Named uuid "00000000-0000-0000-0000-000000000003"; }; }; sa { Value Boolean; }; }; }; }]; };
};
}
oa_process t "Caller" id "00000000-0000-0000-0000-000000000010" { steps [@sub, @left]; }
oa_process use "Callee" id "00000000-0000-0000-0000-000000000011" { steps [@right]; }
oa_occurrence sub "Call" id "00000000-0000-0000-0000-000000000012" { parent @t; body { SequenceReference { sequence @use; }; }; }
oa_occurrence right "Left" id "00000000-0000-0000-0000-000000000013" { parent @use; body { Action { action @implicit; lifeline none; }; }; }
oa_occurrence left "Right" id "00000000-0000-0000-0000-000000000014" { parent @t; body { Action { action @child; lifeline none; }; }; }
contextual_link ctx "Context" id "00000000-0000-0000-0000-000000000015" { owner @t; source { references [@sub]; occurrence @right; }; target { references []; occurrence @left; }; purpose { Precedence { guard "@child"; }; }; }
}
"#;
#[test]
fn complete_categories_preserve_trivia_implicit_parent_and_historical_uuid_bytes() {
let bundle = SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([(
"main.arcadia".into(),
include_str!("../../tests/fixtures/categories.arcadia").into(),
)]),
};
let source = authored(&bundle);
let mut desired = source.project().clone();
let replacements = [
"a", "p", "implicit", "child", "link", "s", "note", "d", "q", "t", "use", "sub", "right",
"left", "ctx",
];
desired.alias = "a".into();
for e in desired.elements.values_mut() {
e.alias = replacements[(1..=15).find(|n| id(*n) == e.id).unwrap() - 1].into();
}
for r in desired.relations.values_mut() {
r.alias = replacements[(1..=15).find(|n| id(*n) == r.id).unwrap() - 1].into();
}
desired.diagrams.get_mut(&id(7)).unwrap().alias = "note".into();
desired.artifact_links.get_mut(&id(8)).unwrap().alias = "d".into();
desired.traces.get_mut(&id(9)).unwrap().alias = "q".into();
let pending = source
.plan_rename(&desired)
.unwrap()
.emit_changed_unverified(&bundle)
.unwrap();
let expected = SourceBundle {
entry: "main.arcadia".into(),
files: BTreeMap::from([(
"main.arcadia".into(),
format!(
"{}\r\n",
CATEGORY_EXPECTED
.trim_end_matches('\n')
.replace("@noise child\n", "@noise child\r\n")
),
)]),
};
assert_eq!(pending.bundle, expected);
assert_eq!(
pending.retained,
Slots {
entries: 1,
occurrences: 0,
text: 2169
}
);
assert_eq!(pending.expression_nodes, 0);
}
#[test]
fn combined_growth_shrinkage_emits_original_coordinates_and_complete_untouched_file() {
let mut bundle = rich_bundle(Layer::Oa);
*bundle.files.get_mut("main.arcadia").unwrap() =
bundle.files["main.arcadia"].replace("node n ", "node nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn ");
let source = authored(&bundle);
let mut desired = source.project().clone();
desired
.diagrams
.get_mut(&id(5))
.unwrap()
.nodes
.get_mut(&id(4))
.unwrap()
.alias = "n".into();
desired.definitions.get_mut(&id(10)).unwrap().alias = "renamed_f".into();
let expected=SourceBundle {entry:"main.arcadia".into(),files:BTreeMap::from([
("a.arcadia".into(),"calculation renamed_f \"F\" id \"00000000-0000-0000-0000-000000000010\" {\ninput x id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput y id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $x + $y;\n}\n".into()),
("b.arcadia".into(),"calculation g \"G\" id \"00000000-0000-0000-0000-000000000011\" {\ninput x id \"00000000-0000-0000-0000-000000000001\" : Number;\ninput y id \"00000000-0000-0000-0000-000000000012\" : Number;\nresult Number; body $x - $y;\n}\n".into()),
("main.arcadia".into(),"arcadia 8;\nproject p \"Project\" id \"00000000-0000-0000-0000-000000000001\" {\ninclude \"b.arcadia\"; include \"a.arcadia\";\nnode n id \"00000000-0000-0000-0000-000000000004\" { diagram @d; element @v; }\noa_property v \"Value\" id \"00000000-0000-0000-0000-000000000003\" { subject @w; type Number; base @renamed_f(y: 2, x: @g(x: 3, y: 4)) + @renamed_f(x: 5, y: 6); }\noa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {}\ndiagram d \"Diagram\" id \"00000000-0000-0000-0000-000000000005\" { }\n\n}\n".into()),
])};
let plan = source.plan_rename(&desired).unwrap();
assert_eq!(plan.output_bytes().unwrap(), 924);
let pending = plan.emit_changed_unverified(&bundle).unwrap();
assert_eq!(pending.bundle, expected);
assert_eq!(
pending.retained,
Slots {
entries: 3,
occurrences: 0,
text: 966
}
);
}