Add private consumed rename emission with bounded owned output
This commit is contained in:
@@ -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;
|
||||
|
||||
+152
@@ -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;
|
||||
+646
@@ -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(¤t),
|
||||
"Current complete bundle",
|
||||
);
|
||||
failure(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(¤t),
|
||||
"Current complete bundle",
|
||||
);
|
||||
}
|
||||
let current = bundle.clone();
|
||||
assert_eq!(
|
||||
authored(&bundle)
|
||||
.plan_rename(&desired)
|
||||
.unwrap()
|
||||
.emit_changed_unverified(¤t)
|
||||
.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
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user