689 lines
24 KiB
Rust
689 lines
24 KiB
Rust
use oa_core::{language, source_project::*, *};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn bundle(entry: &str, fragments: &[(&str, &str)]) -> SourceBundle {
|
|
let mut files = BTreeMap::from([("project.arcadia".into(), entry.into())]);
|
|
files.extend(
|
|
fragments
|
|
.iter()
|
|
.map(|(path, text)| ((*path).into(), (*text).into())),
|
|
);
|
|
SourceBundle {
|
|
entry: "project.arcadia".into(),
|
|
files,
|
|
}
|
|
}
|
|
|
|
fn compile(bundle: &SourceBundle) -> CompiledSourceProject {
|
|
compile_bundle(bundle, ProjectLimits::default()).unwrap()
|
|
}
|
|
|
|
fn id(project: &Project, alias: &str) -> Id {
|
|
project
|
|
.identities()
|
|
.into_iter()
|
|
.find(|(_, candidate)| *candidate == alias)
|
|
.unwrap()
|
|
.0
|
|
}
|
|
|
|
fn typed() -> SourceBundle {
|
|
bundle(
|
|
"arcadia 3; project test_model \"航海\" {\n include \"exchanges.arcadia\";\n include \"work/send.arcadia\";\n include \"work/receive.arcadia\";\n include \"types.arcadia\";\n}",
|
|
&[
|
|
(
|
|
"exchanges.arcadia",
|
|
"// Boundary-independent functional exchange\nfunctional_exchange message \"Message\" { source @send; target @receive; source_port @output; target_port @input; items [@data]; }\n",
|
|
),
|
|
(
|
|
"work/send.arcadia",
|
|
"// keep this explanation\nsa_function send \"Send\" {}\nsa_port output \"Output\" { parent @send; contract { FunctionOutput { outgoing_items [@data]; }; }; }\n",
|
|
),
|
|
(
|
|
"work/receive.arcadia",
|
|
"sa_function receive \"Receive\" {}\nsa_port input \"Input\" { parent @receive; contract { FunctionInput { incoming_items [@data]; }; }; }\n",
|
|
),
|
|
("types.arcadia", "sa_exchange_item data \"Data\" {}\n"),
|
|
],
|
|
)
|
|
}
|
|
|
|
#[test]
|
|
fn cross_file_typed_ports_global_forward_references_and_stable_ids() {
|
|
let input = typed();
|
|
let original = input.clone();
|
|
let result = compile(&input);
|
|
assert_eq!(input, original, "compilation must not mutate caller drafts");
|
|
assert_ne!(result.bundle, input);
|
|
let again = compile(&result.bundle);
|
|
assert_eq!(again.decoded.project, result.decoded.project);
|
|
assert_eq!(again.bundle, result.bundle);
|
|
assert_eq!(again.index, result.index);
|
|
assert!(result.decoded.migration.is_none());
|
|
let project = &result.decoded.project;
|
|
assert_eq!(
|
|
project.relations[&id(project, "message")].source(),
|
|
id(project, "send")
|
|
);
|
|
let port = id(project, "output");
|
|
assert_eq!(result.index.declarations[&port].path, "work/send.arcadia");
|
|
let field = &result.index.fields[&(port, "contract".into())];
|
|
assert_eq!(
|
|
&result.bundle.files[&field.path][field.span.start..field.span.end],
|
|
"{ FunctionOutput { outgoing_items [@data]; }; }"
|
|
);
|
|
assert_eq!(
|
|
language::parse(&language::format(project)).unwrap(),
|
|
*project
|
|
);
|
|
assert_eq!(
|
|
persistence::decode(&serde_json::to_vec(project).unwrap()).unwrap(),
|
|
*project
|
|
);
|
|
assert!(result.bundle.files["work/send.arcadia"].starts_with("// keep this explanation\n"));
|
|
let send = id(project, "send");
|
|
let warning = result
|
|
.diagnostics
|
|
.iter()
|
|
.find(|issue| {
|
|
issue.diagnostic.object_id == Some(send) && issue.diagnostic.code == "unallocated_leaf"
|
|
})
|
|
.unwrap();
|
|
assert_eq!(warning.path, "work/send.arcadia");
|
|
assert_eq!(warning.diagnostic.line, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn source_errors_keep_exact_fragment_unicode_positions() {
|
|
let input = bundle(
|
|
"arcadia 3; project p \"P\" { include \"child.arcadia\"; }",
|
|
&[(
|
|
"child.arcadia",
|
|
"// Пример\nsa_function child \"航海\" { parent @missing; }",
|
|
)],
|
|
);
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
let text = &input.files["child.arcadia"];
|
|
assert_eq!(error.path, "child.arcadia");
|
|
assert_eq!(error.diagnostic.line, 2);
|
|
let offset = text.find("@missing").unwrap();
|
|
assert_eq!(error.diagnostic.span.start, offset);
|
|
assert_eq!(
|
|
error.diagnostic.column,
|
|
text[..offset].rsplit('\n').next().unwrap().chars().count() + 1
|
|
);
|
|
assert!(error.diagnostic.message.contains("@missing"));
|
|
let cyclic_parent = SourceBundle {
|
|
files: input
|
|
.files
|
|
.iter()
|
|
.map(|(path, text)| (path.clone(), text.replace("@missing", "@child")))
|
|
.collect(),
|
|
..input
|
|
};
|
|
let error = compile_bundle(&cyclic_parent, ProjectLimits::default()).unwrap_err();
|
|
let parent = error
|
|
.iter()
|
|
.find(|issue| issue.diagnostic.code == "invalid_parent")
|
|
.unwrap();
|
|
assert_eq!(parent.path, "child.arcadia");
|
|
assert_eq!(parent.diagnostic.span.start, offset);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_alias_and_identity_report_both_files_including_project_identity() {
|
|
for (left, right) in [
|
|
("oa_activity same \"A\" {}", "oa_activity same \"B\" {}"),
|
|
(
|
|
"oa_activity first \"A\" id \"00000000-0000-4000-8000-000000000001\" {}",
|
|
"oa_activity second \"B\" id \"00000000-0000-4000-8000-000000000001\" {}",
|
|
),
|
|
] {
|
|
let input = bundle(
|
|
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
|
&[("a.arcadia", left), ("b.arcadia", right)],
|
|
);
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
assert_eq!(error.diagnostic.code, "duplicate_declaration");
|
|
assert_eq!(error.path, "b.arcadia");
|
|
assert_eq!(error.related[0].path, "a.arcadia");
|
|
}
|
|
let input = bundle(
|
|
"arcadia 3; project p \"P\" { include \"child.arcadia\"; }",
|
|
&[("child.arcadia", "oa_activity p \"Collision\" {}")],
|
|
);
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
assert_eq!(error.related[0].path, "project.arcadia");
|
|
}
|
|
|
|
#[test]
|
|
fn includes_are_once_only_and_cycles_missing_and_unreachable_files_are_located() {
|
|
let mut input = bundle(
|
|
"arcadia 3; project p \"P\" { include \"sub/a.arcadia\"; include \"sub/b.arcadia\"; }",
|
|
&[
|
|
("sub/a.arcadia", "include \"../shared.arcadia\";"),
|
|
("sub/b.arcadia", "include \"../shared.arcadia\";"),
|
|
("shared.arcadia", "oa_activity shared_work \"Shared\" {}"),
|
|
],
|
|
);
|
|
assert_eq!(compile(&input).decoded.project.elements.len(), 1);
|
|
input
|
|
.files
|
|
.insert("shared.arcadia".into(), "include \"sub/a.arcadia\";".into());
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
assert_eq!(error.path, "shared.arcadia");
|
|
assert_eq!(error.diagnostic.code, "include_cycle");
|
|
assert!(
|
|
error
|
|
.diagnostic
|
|
.message
|
|
.contains("sub/a.arcadia -> shared.arcadia -> sub/a.arcadia")
|
|
);
|
|
input.files.remove("shared.arcadia");
|
|
let error = compile_bundle(&input, ProjectLimits::default())
|
|
.unwrap_err()
|
|
.remove(0);
|
|
assert_eq!(error.path, "sub/a.arcadia");
|
|
assert_eq!(error.diagnostic.code, "missing_include");
|
|
input.files.insert("shared.arcadia".into(), String::new());
|
|
input.files.insert("unused.arcadia".into(), String::new());
|
|
assert_eq!(
|
|
compile_bundle(&input, ProjectLimits::default()).unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"unreachable_file"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn portable_paths_reject_escapes_devices_urls_and_case_collisions() {
|
|
for path in [
|
|
"../escape.arcadia",
|
|
"/abs.arcadia",
|
|
"C:/drive.arcadia",
|
|
"https://host/a.arcadia",
|
|
"a\\b.arcadia",
|
|
"a//b.arcadia",
|
|
"con.arcadia",
|
|
"dir./a.arcadia",
|
|
"a.txt",
|
|
] {
|
|
assert!(resolve_include("project.arcadia", path).is_err(), "{path}");
|
|
}
|
|
assert_eq!(
|
|
resolve_include("sub/a.arcadia", "../b.arcadia").unwrap(),
|
|
"b.arcadia"
|
|
);
|
|
assert!(resolve_include("../bad.arcadia", "a.arcadia").is_err());
|
|
let input = bundle(
|
|
"arcadia 3; project p \"P\" {}",
|
|
&[("A.arcadia", ""), ("a.arcadia", "")],
|
|
);
|
|
assert_eq!(
|
|
compile_bundle(&input, ProjectLimits::default()).unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"duplicate_path"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn project_admission_is_aggregate_and_precedes_global_resolution() {
|
|
let input = bundle(
|
|
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
|
&[
|
|
("a.arcadia", "oa_activity one \"One\" { parent @missing; }"),
|
|
("b.arcadia", "oa_activity two \"Two\" {}"),
|
|
],
|
|
);
|
|
let cases = [
|
|
ProjectLimits {
|
|
max_files: 2,
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
max_include_depth: 0,
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
max_include_edges: 1,
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_graph_objects: 1,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_records: 2,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
},
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_source_bytes: input.files.values().map(String::len).sum::<usize>() - 1,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
},
|
|
];
|
|
for limits in cases {
|
|
let error = compile_bundle(&input, limits).unwrap_err();
|
|
assert!(error[0].diagnostic.code.ends_with("limit"), "{error:?}");
|
|
assert!(!error[0].diagnostic.message.contains("@missing"));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn canonical_export_groups_semantic_roots_ports_and_placements_and_is_deterministic() {
|
|
let mut project = compile(&typed()).decoded.project;
|
|
let child = project
|
|
.add_element(
|
|
Kind::Function,
|
|
Layer::Sa,
|
|
"Child",
|
|
Some(id(&project, "send")),
|
|
)
|
|
.unwrap();
|
|
let diagram = project.ensure_diagram(Layer::Sa);
|
|
let node = project.place(diagram, child, Position { x: 40., y: 80. });
|
|
let output = format_bundle(&project);
|
|
assert_eq!(format_bundle(&project), output);
|
|
let result = compile(&output);
|
|
assert_eq!(result.decoded.project, project);
|
|
assert!(
|
|
result.index.declarations[&child]
|
|
.path
|
|
.contains("function_send.arcadia")
|
|
);
|
|
assert_eq!(
|
|
result.index.declarations[&child].path,
|
|
result.index.declarations[&id(&project, "output")].path
|
|
);
|
|
assert_eq!(
|
|
result.index.declarations[&node].path,
|
|
result.index.declarations[&diagram].path
|
|
);
|
|
let work = &output.files[&result.index.declarations[&child].path];
|
|
assert!(
|
|
work.find("sa_function send ").unwrap()
|
|
< work
|
|
.find(&format!("sa_function {} ", project.elements[&child].alias))
|
|
.unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn moving_declarations_between_files_changes_no_semantics_or_ids() {
|
|
let compiled = compile(&typed());
|
|
let mut moved = compiled.bundle.clone();
|
|
let port = id(&compiled.decoded.project, "output");
|
|
let location = &compiled.index.declarations[&port];
|
|
let declaration =
|
|
moved.files[&location.path][location.span.start..location.span.end].to_owned();
|
|
moved
|
|
.files
|
|
.get_mut(&location.path)
|
|
.unwrap()
|
|
.replace_range(location.span.start..location.span.end, "");
|
|
moved
|
|
.files
|
|
.get_mut("exchanges.arcadia")
|
|
.unwrap()
|
|
.push_str(&format!("\n{declaration}\n"));
|
|
let moved = compile(&moved);
|
|
assert_eq!(moved.decoded.project, compiled.decoded.project);
|
|
assert_eq!(moved.index.declarations[&port].path, "exchanges.arcadia");
|
|
}
|
|
|
|
#[test]
|
|
fn model_updates_rewrite_cross_file_alias_uses_preserve_ownership_and_untouched_comments() {
|
|
let compiled = compile(&typed());
|
|
let mut project = compiled.decoded.project.clone();
|
|
let send = id(&project, "send");
|
|
project.elements.get_mut(&send).unwrap().alias = "transmit".into();
|
|
project.elements.get_mut(&send).unwrap().name = "Transmit data".into();
|
|
project.description = "Documented project".into();
|
|
let result = update_bundle(&compiled, &project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(result.decoded.project, project);
|
|
assert_eq!(result.index.declarations[&send].path, "work/send.arcadia");
|
|
assert!(result.bundle.files["exchanges.arcadia"].contains("source @transmit;"));
|
|
assert!(result.bundle.files["work/send.arcadia"].contains("parent @transmit;"));
|
|
assert!(result.bundle.files["work/send.arcadia"].starts_with("// keep this explanation\n"));
|
|
assert_eq!(
|
|
result.bundle.files["types.arcadia"],
|
|
compiled.bundle.files["types.arcadia"]
|
|
);
|
|
assert_eq!(
|
|
result.bundle.files["work/receive.arcadia"],
|
|
compiled.bundle.files["work/receive.arcadia"]
|
|
);
|
|
assert_eq!(
|
|
update_bundle(&result, &project, ProjectLimits::default())
|
|
.unwrap()
|
|
.bundle,
|
|
result.bundle
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn graphical_move_updates_only_its_owned_view_document() {
|
|
let mut project = compile(&typed()).decoded.project;
|
|
let diagram = project.ensure_diagram(Layer::Sa);
|
|
let send = id(&project, "send");
|
|
let node = project.place(diagram, send, Position { x: 0., y: 0. });
|
|
let current = compile(&format_bundle(&project));
|
|
project
|
|
.diagrams
|
|
.get_mut(&diagram)
|
|
.unwrap()
|
|
.nodes
|
|
.get_mut(&node)
|
|
.unwrap()
|
|
.position = Position { x: 142., y: 83. };
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
let changed: Vec<_> = result
|
|
.bundle
|
|
.files
|
|
.iter()
|
|
.filter(|(path, text)| current.bundle.files[*path] != **text)
|
|
.map(|(path, _)| path.clone())
|
|
.collect();
|
|
assert_eq!(
|
|
changed,
|
|
vec![current.index.declarations[&node].path.clone()]
|
|
);
|
|
assert_eq!(result.decoded.project, project);
|
|
}
|
|
|
|
#[test]
|
|
fn new_children_inherit_owner_file_new_roots_add_include_and_deletion_keeps_files() {
|
|
let current = compile(&typed());
|
|
let mut project = current.decoded.project.clone();
|
|
let send = id(&project, "send");
|
|
let child = project
|
|
.add_element(Kind::Function, Layer::Sa, "Nested work", Some(send))
|
|
.unwrap();
|
|
let root = project
|
|
.add_element(Kind::Function, Layer::Sa, "Independent work", None)
|
|
.unwrap();
|
|
let result = update_bundle(¤t, &project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(result.index.declarations[&child].path, "work/send.arcadia");
|
|
let root_file = result.index.declarations[&root].path.clone();
|
|
assert!(root_file.contains("independent_work"));
|
|
assert!(result.bundle.files["project.arcadia"].contains(&format!("include \"{root_file}\";")));
|
|
project.elements.remove(&root);
|
|
let deleted = update_bundle(&result, &project, ProjectLimits::default()).unwrap();
|
|
assert!(deleted.bundle.files.contains_key(&root_file));
|
|
assert!(!deleted.index.declarations.contains_key(&root));
|
|
}
|
|
|
|
#[test]
|
|
fn failed_update_preserves_candidate_and_rejects_stale_index() {
|
|
let current = compile(&typed());
|
|
let original = current.bundle.clone();
|
|
let mut project = current.decoded.project.clone();
|
|
project
|
|
.elements
|
|
.get_mut(&id(¤t.decoded.project, "send"))
|
|
.unwrap()
|
|
.parent = Some(Id::new_v4());
|
|
assert!(update_bundle(¤t, &project, ProjectLimits::default()).is_err());
|
|
assert_eq!(current.bundle, original);
|
|
let mut stale = current.clone();
|
|
stale
|
|
.bundle
|
|
.files
|
|
.get_mut("work/send.arcadia")
|
|
.unwrap()
|
|
.insert_str(0, "// external edit\n");
|
|
assert_eq!(
|
|
update_bundle(&stale, ¤t.decoded.project, ProjectLimits::default()).unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"stale_source_index"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn single_string_and_legacy_apis_remain_explicit_and_filesystem_free() {
|
|
let source = "arcadia 3; project p \"P\" { include \"missing.arcadia\"; }";
|
|
assert!(
|
|
language::parse(source).unwrap_err()[0]
|
|
.message
|
|
.contains("requires a source bundle")
|
|
);
|
|
let old = "arcadia 2; project p \"Legacy\" { oa_activity work \"Work\" {} }";
|
|
let decoded = language::parse_with_report(old).unwrap();
|
|
assert!(decoded.migration.is_some());
|
|
assert!(compile_bundle(&bundle(old, &[]), ProjectLimits::default()).is_err());
|
|
assert_eq!(
|
|
compile(&format_bundle(&decoded.project)).decoded.project,
|
|
decoded.project
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn exported_case_colliding_root_aliases_are_portable_and_large_root_sets_are_bounded() {
|
|
let mut project = Project::empty();
|
|
let a = project
|
|
.add_element(Kind::Activity, Layer::Oa, "Upper", None)
|
|
.unwrap();
|
|
let b = project
|
|
.add_element(Kind::Activity, Layer::Oa, "Lower", None)
|
|
.unwrap();
|
|
project.elements.get_mut(&a).unwrap().alias = "Work".into();
|
|
project.elements.get_mut(&b).unwrap().alias = "work".into();
|
|
let result = compile(&format_bundle(&project));
|
|
assert_ne!(
|
|
result.index.declarations[&a].path.to_lowercase(),
|
|
result.index.declarations[&b].path.to_lowercase()
|
|
);
|
|
for n in 0..260 {
|
|
project
|
|
.add_element(Kind::Activity, Layer::Oa, &format!("Activity {n}"), None)
|
|
.unwrap();
|
|
}
|
|
let generated = format_bundle(&project);
|
|
assert!(generated.files.len() <= ProjectLimits::default().max_files);
|
|
assert_eq!(compile(&generated).decoded.project, project);
|
|
}
|
|
|
|
#[test]
|
|
fn complete_demo_preserves_behavior_artifacts_traces_and_every_placement() {
|
|
let project = Project::demo();
|
|
let generated = format_bundle(&project);
|
|
let result = compile(&generated);
|
|
assert_eq!(result.decoded.project, project);
|
|
for element in project
|
|
.elements
|
|
.values()
|
|
.filter(|e| matches!(e.kind(), Kind::Scenario | Kind::StateMachine))
|
|
{
|
|
assert!(
|
|
result.index.declarations[&element.id]
|
|
.path
|
|
.contains("/behavior/")
|
|
);
|
|
for child in project
|
|
.elements
|
|
.values()
|
|
.filter(|child| child.parent == Some(element.id))
|
|
{
|
|
assert_eq!(
|
|
result.index.declarations[&child.id].path,
|
|
result.index.declarations[&element.id].path
|
|
);
|
|
}
|
|
}
|
|
let json = serde_json::to_string(&generated).unwrap();
|
|
assert_eq!(
|
|
serde_json::from_str::<SourceBundle>(&json).unwrap(),
|
|
generated
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn json_bundle_rejects_duplicate_file_keys_and_unknown_fields() {
|
|
assert!(serde_json::from_str::<SourceBundle>(r#"{"entry":"project.arcadia","files":{"project.arcadia":"first","project.arcadia":"second"}}"#).unwrap_err().to_string().contains("Duplicate source path"));
|
|
assert!(
|
|
serde_json::from_str::<SourceBundle>(
|
|
r#"{"entry":"project.arcadia","files":{},"root":"C:/host"}"#
|
|
)
|
|
.is_err()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn generated_id_insertion_handles_adjacent_header_tokens_and_remains_exactly_indexed() {
|
|
let input = bundle(
|
|
"arcadia 3;project p\"P\"{include \"child.arcadia\";}",
|
|
&[(
|
|
"child.arcadia",
|
|
"oa_activity a\"A\"{}diagram d\"D\"{}node n{diagram @d;element @a;}",
|
|
)],
|
|
);
|
|
let result = compile(&input);
|
|
for (id, location) in &result.index.declarations {
|
|
assert!(
|
|
result.bundle.files[&location.path][location.span.start..location.span.end]
|
|
.contains(&id.to_string())
|
|
);
|
|
}
|
|
assert_eq!(
|
|
compile(&result.bundle).decoded.project,
|
|
result.decoded.project
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate_include_edges_count_toward_admission_and_ordered_references_are_aggregate() {
|
|
let input = bundle(
|
|
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"a.arcadia\"; }",
|
|
&[("a.arcadia", "")],
|
|
);
|
|
assert_eq!(
|
|
compile_bundle(
|
|
&input,
|
|
ProjectLimits {
|
|
max_include_edges: 1,
|
|
..ProjectLimits::default()
|
|
}
|
|
)
|
|
.unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"project_limit"
|
|
);
|
|
let input = bundle(
|
|
"arcadia 3; project p \"P\" { include \"a.arcadia\"; include \"b.arcadia\"; }",
|
|
&[
|
|
(
|
|
"a.arcadia",
|
|
"oa_process one \"One\" { steps [@step_one]; } oa_occurrence step_one \"Step one\" { parent @one; body { Control { kind Choice; }; }; }",
|
|
),
|
|
(
|
|
"b.arcadia",
|
|
"oa_process two \"Two\" { steps [@step_two]; } oa_occurrence step_two \"Step two\" { parent @two; body { Control { kind Choice; }; }; }",
|
|
),
|
|
],
|
|
);
|
|
assert_eq!(
|
|
compile_bundle(
|
|
&input,
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_step_references: 1,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
}
|
|
)
|
|
.unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"model_limit"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn include_inspection_reuses_strict_entry_fragment_syntax_and_real_spans() {
|
|
let source = "// include \"fake.arcadia\";\noa_activity work \"航海\" { parent @external_work; description \"include ignored\"; }\ninclude \"../shared.arcadia\";";
|
|
let includes =
|
|
inspect_includes("sub/work.arcadia", source, false, ProjectLimits::default()).unwrap();
|
|
assert_eq!(includes.len(), 1);
|
|
assert_eq!(includes[0].target, "shared.arcadia");
|
|
assert_eq!(includes[0].location.path, "sub/work.arcadia");
|
|
let span = includes[0].location.span;
|
|
assert_eq!(
|
|
&source[span.start..span.end],
|
|
"include \"../shared.arcadia\";"
|
|
);
|
|
let entry = "arcadia 3; project p \"P\" { include \"sub/work.arcadia\"; }";
|
|
assert_eq!(
|
|
inspect_includes("project.arcadia", entry, true, ProjectLimits::default()).unwrap()[0]
|
|
.target,
|
|
"sub/work.arcadia"
|
|
);
|
|
assert!(inspect_includes("project.arcadia", entry, false, ProjectLimits::default()).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn include_inspection_returns_located_failure_without_partial_discovery_and_is_bounded() {
|
|
let source = "include \"first.arcadia\";\noa_activity invalid \"航海\" { parent @; }";
|
|
let errors =
|
|
inspect_includes("broken.arcadia", source, false, ProjectLimits::default()).unwrap_err();
|
|
assert_eq!(errors[0].path, "broken.arcadia");
|
|
assert_eq!(errors[0].diagnostic.line, 2);
|
|
assert_eq!(
|
|
errors[0].diagnostic.span.start,
|
|
source.find("@;").unwrap() + 1
|
|
);
|
|
assert_eq!(
|
|
inspect_includes(
|
|
"broken.arcadia",
|
|
source,
|
|
false,
|
|
ProjectLimits {
|
|
model: language::ParseLimits {
|
|
max_source_bytes: 10,
|
|
..language::ParseLimits::UNLIMITED
|
|
},
|
|
..ProjectLimits::default()
|
|
}
|
|
)
|
|
.unwrap_err()[0]
|
|
.diagnostic
|
|
.code,
|
|
"source_limit"
|
|
);
|
|
let source = "include \"first.arcadia\"; include \"second.arcadia\";";
|
|
let errors = inspect_includes(
|
|
"project.arcadia",
|
|
source,
|
|
false,
|
|
ProjectLimits {
|
|
max_include_edges: 1,
|
|
..ProjectLimits::default()
|
|
},
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(
|
|
errors[0].diagnostic.span.start,
|
|
source.find("include \"second").unwrap()
|
|
);
|
|
assert_eq!(errors[0].diagnostic.code, "project_limit");
|
|
}
|