Test public mixed reads through external model and diagnostic oracles
This commit is contained in:
@@ -0,0 +1,812 @@
|
||||
//! External consumer: the linked oa-core library is not built with cfg(test).
|
||||
//! Expected models use authored public values, never a parser or formatter oracle.
|
||||
use oa_core::analysis as a;
|
||||
use oa_core::analysis_numeric::Exact;
|
||||
use oa_core::language::{self, ParseLimits, Span};
|
||||
use oa_core::source_project::{ProjectLimits, SourceBundle, SourceDiagnostic};
|
||||
pub use oa_core::*;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
// This existing helper constructs independent public model values only. It does
|
||||
// not import private parser/index/writer code or enable cfg(test) in oa-core.
|
||||
#[path = "../src/language/analysis_source/model_oracle.rs"]
|
||||
mod independent_model;
|
||||
|
||||
fn id(n: u128) -> Id {
|
||||
Id::from_u128(n)
|
||||
}
|
||||
|
||||
fn empty_project() -> Project {
|
||||
let mut p = Project::empty();
|
||||
p.id = id(1);
|
||||
p.alias = "p".into();
|
||||
p.name = "Project".into();
|
||||
p
|
||||
}
|
||||
|
||||
fn document(body: &str) -> String {
|
||||
format!(
|
||||
"arcadia 8;\nproject p \"Project\" id \"{}\" {{\n{body}\n}}\n",
|
||||
id(1)
|
||||
)
|
||||
}
|
||||
|
||||
fn single(body: &str) -> SourceBundle {
|
||||
SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), document(body))]),
|
||||
}
|
||||
}
|
||||
|
||||
fn succeeds(
|
||||
bundle: &SourceBundle,
|
||||
limits: ProjectLimits,
|
||||
expected: &Project,
|
||||
warnings: &[SourceDiagnostic],
|
||||
) {
|
||||
let original = bundle.clone();
|
||||
let checked = language::check_mixed_bundle(bundle, limits)
|
||||
.unwrap_or_else(|errors| panic!("expected complete read: {errors:#?}"));
|
||||
assert_eq!(checked.project(), expected);
|
||||
assert_eq!(checked.warnings(), warnings);
|
||||
assert_eq!(bundle, &original, "read success preserves every input byte");
|
||||
drop(checked);
|
||||
assert_eq!(bundle, &original);
|
||||
}
|
||||
|
||||
fn fails(bundle: &SourceBundle, limits: ProjectLimits) -> Vec<SourceDiagnostic> {
|
||||
let original = bundle.clone();
|
||||
let errors = match language::check_mixed_bundle(bundle, limits) {
|
||||
Ok(_) => panic!("invalid input returned a successful read witness"),
|
||||
Err(errors) => errors,
|
||||
};
|
||||
assert!(!errors.is_empty());
|
||||
assert_eq!(bundle, &original, "read refusal preserves every input byte");
|
||||
errors
|
||||
}
|
||||
|
||||
fn authored_span(source: &str, literal: &str) -> Span {
|
||||
assert_eq!(
|
||||
source.matches(literal).count(),
|
||||
1,
|
||||
"ambiguous authored span"
|
||||
);
|
||||
let start = source.find(literal).unwrap();
|
||||
Span {
|
||||
start,
|
||||
end: start + literal.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn error_at(
|
||||
bundle: &SourceBundle,
|
||||
errors: &[SourceDiagnostic],
|
||||
path: &str,
|
||||
span: Span,
|
||||
code: &str,
|
||||
message: &str,
|
||||
) {
|
||||
assert_eq!(errors.len(), 1, "{errors:#?}");
|
||||
let error = &errors[0];
|
||||
assert_eq!(error.path, path);
|
||||
assert_eq!(error.diagnostic.severity, validation::Severity::Error);
|
||||
assert_eq!(error.diagnostic.code, code);
|
||||
assert_eq!(error.diagnostic.message, message);
|
||||
assert_eq!(error.diagnostic.span, span);
|
||||
let source = bundle.files.get(path).map_or("", String::as_str);
|
||||
let prefix = &source[..span.start];
|
||||
assert_eq!(
|
||||
error.diagnostic.line,
|
||||
prefix.bytes().filter(|b| *b == b'\n').count() + 1
|
||||
);
|
||||
assert_eq!(
|
||||
error.diagnostic.column,
|
||||
prefix.rsplit('\n').next().unwrap().chars().count() + 1
|
||||
);
|
||||
}
|
||||
|
||||
fn rich(layer: Layer, crlf: bool) -> (SourceBundle, Project, Vec<SourceDiagnostic>) {
|
||||
let (ordinary, typed, work) = match layer {
|
||||
Layer::Oa => (
|
||||
include_str!("../src/language/analysis_source/lower/fixtures/ordinary-oa.arcadia"),
|
||||
include_str!("../src/language/analysis_source/lower/fixtures/typed-oa.arcadia"),
|
||||
"oa_activity",
|
||||
),
|
||||
Layer::Sa => (
|
||||
include_str!("../src/language/analysis_source/lower/fixtures/ordinary-sa.arcadia"),
|
||||
include_str!("../src/language/analysis_source/lower/fixtures/typed-sa.arcadia"),
|
||||
"sa_function",
|
||||
),
|
||||
};
|
||||
let mut expected = independent_model::project(layer);
|
||||
let mut main = ordinary
|
||||
.replace("\r\n", "\n")
|
||||
.replacen("arcadia 7;", "arcadia 8;", 1);
|
||||
assert!(main.starts_with("arcadia 8;"));
|
||||
// Forward typed uses refer back into nested ordinary Work in this file.
|
||||
main = main.replacen("{\n", "{\n include \"typed.arcadia\";\n", 1);
|
||||
if layer == Layer::Sa {
|
||||
let system = Element {
|
||||
id: id(999),
|
||||
alias: "system".into(),
|
||||
name: "System".into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body: ElementBody::Sa(SaBody::System),
|
||||
};
|
||||
let at = main.rfind('}').unwrap();
|
||||
main.insert_str(
|
||||
at,
|
||||
&format!(
|
||||
" sa_system system \"System\" id \"{}\" {{}}\n",
|
||||
system.id
|
||||
),
|
||||
);
|
||||
assert!(expected.elements.insert(system.id, system).is_none());
|
||||
}
|
||||
let mut typed = typed.replace("\r\n", "\n");
|
||||
// Textual argument order is independent of the model's parameter-ID map.
|
||||
assert!(typed.contains("@combine(a: @receive_time, b: @dispatch_time)"));
|
||||
typed = typed.replace(
|
||||
"@combine(a: @receive_time, b: @dispatch_time)",
|
||||
"@combine(b: @dispatch_time, a: @receive_time)",
|
||||
);
|
||||
if crlf {
|
||||
main = format!("// 港🌊\r\n{}", main.replace('\n', "\r\n"));
|
||||
typed = typed.replace('\n', "\r\n");
|
||||
}
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([
|
||||
("main.arcadia".into(), main),
|
||||
("typed.arcadia".into(), typed),
|
||||
]),
|
||||
};
|
||||
// Two nested leaves, so four warnings, unlike the earlier one-Work fixture.
|
||||
// Existing validation orders both allocation warnings before coverage warnings.
|
||||
let mut warnings = Vec::new();
|
||||
for allocation in [true, false] {
|
||||
for (alias, display) in [("receive", "Receive"), ("dispatch", "Dispatch")] {
|
||||
let element = expected
|
||||
.elements
|
||||
.values()
|
||||
.find(|e| e.alias == alias)
|
||||
.unwrap();
|
||||
let literal = format!("{work} {alias} \"{display}\" id \"{}\" {{}}", element.id);
|
||||
let source = &bundle.files["main.arcadia"];
|
||||
let span = authored_span(source, &literal);
|
||||
let prefix = &source[..span.start];
|
||||
warnings.push(SourceDiagnostic {
|
||||
path: "main.arcadia".into(), related: Vec::new(),
|
||||
diagnostic: language::Diagnostic {
|
||||
severity: validation::Severity::Warning,
|
||||
code: if allocation { "unallocated_leaf" } else { "work_without_behavior_context" }.into(),
|
||||
object_id: Some(element.id),
|
||||
field: allocation.then(|| "allocation".into()),
|
||||
message: if allocation { format!("Allocate leaf '{display}' to a performer.") } else {
|
||||
"Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence.".into()
|
||||
},
|
||||
span, line: prefix.bytes().filter(|b| *b == b'\n').count() + 1,
|
||||
column: prefix.rsplit('\n').next().unwrap().chars().count() + 1,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
(bundle, expected, warnings)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_complete_oa_sa_models_and_all_located_warnings() {
|
||||
for layer in [Layer::Oa, Layer::Sa] {
|
||||
for crlf in [false, true] {
|
||||
let (bundle, expected, warnings) = rich(layer, crlf);
|
||||
assert_eq!(expected.definitions.len(), 4);
|
||||
assert_eq!(warnings.len(), 4);
|
||||
succeeds(&bundle, ProjectLimits::default(), &expected, &warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calc(alias: &str, number: u128, body: &str) -> String {
|
||||
format!(
|
||||
"calculation {alias} \"Calculation\" id \"{}\" {{ input x id \"{}\" : Number; result Number; body {body}; }}",
|
||||
id(number),
|
||||
id(1)
|
||||
)
|
||||
}
|
||||
|
||||
fn calculation_model(alias: &str, number: u128, body: a::Expr) -> a::Definition {
|
||||
a::Definition {
|
||||
id: id(number),
|
||||
alias: alias.into(),
|
||||
name: "Calculation".into(),
|
||||
description: String::new(),
|
||||
body: a::DefinitionBody::Calculation(a::CalculationDefinition {
|
||||
parameters: vec![a::Parameter {
|
||||
id: a::ParameterId(id(1)),
|
||||
name: "x".into(),
|
||||
value_type: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
}],
|
||||
result: a::AnalysisType::Scalar(a::ScalarType::Number),
|
||||
body: a::Binding::Expression(body),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_scoped_uuid_reuse_and_cross_file_call_are_not_global_collisions() {
|
||||
let mut bundle = single("include \"f.arcadia\"; include \"g.arcadia\";");
|
||||
bundle
|
||||
.files
|
||||
.insert("f.arcadia".into(), calc("f", 2, "@g(x: $x)"));
|
||||
bundle.files.insert("g.arcadia".into(), calc("g", 3, "$x"));
|
||||
let mut expected = empty_project();
|
||||
expected.definitions.insert(
|
||||
id(2),
|
||||
calculation_model(
|
||||
"f",
|
||||
2,
|
||||
a::Expr::Call {
|
||||
definition: a::DefinitionRef::Local(id(3)),
|
||||
arguments: BTreeMap::from([(
|
||||
a::ParameterId(id(1)),
|
||||
a::Expr::Parameter(a::ParameterId(id(1))),
|
||||
)]),
|
||||
},
|
||||
),
|
||||
);
|
||||
expected.definitions.insert(
|
||||
id(3),
|
||||
calculation_model("g", 3, a::Expr::Parameter(a::ParameterId(id(1)))),
|
||||
);
|
||||
succeeds(&bundle, ProjectLimits::default(), &expected, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_explicit_identity_classes_versions_and_long_forms() {
|
||||
let empty = single("");
|
||||
succeeds(&empty, ProjectLimits::default(), &empty_project(), &[]);
|
||||
let ordinary = format!("oa_entity e \"Entity\" id \"{}\" {{}}", id(2));
|
||||
let typed = calc("f", 3, "$x");
|
||||
let valid = single(&format!("{ordinary}\n{typed}"));
|
||||
assert!(language::check_mixed_bundle(&valid, ProjectLimits::default()).is_ok());
|
||||
// Each deletion starts from the same valid complete input.
|
||||
for (literal, expected_start) in [
|
||||
(format!("id \"{}\"", id(1)), None),
|
||||
(format!("id \"{}\"", id(2)), Some("oa_entity")),
|
||||
(format!("id \"{}\"", id(3)), Some("calculation")),
|
||||
] {
|
||||
let mut missing = valid.clone();
|
||||
let text = missing.files.get_mut("main.arcadia").unwrap();
|
||||
*text = text.replacen(&literal, "", 1);
|
||||
let start = match expected_start {
|
||||
None => text.find('{').unwrap(),
|
||||
Some("oa_entity") => {
|
||||
text.find("oa_entity").unwrap()
|
||||
+ text[text.find("oa_entity").unwrap()..].find('{').unwrap()
|
||||
}
|
||||
Some(word) => text.find(word).unwrap(),
|
||||
};
|
||||
let errors = fails(&missing, ProjectLimits::default());
|
||||
error_at(
|
||||
&missing,
|
||||
&errors,
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Mixed source8 requires explicit UUIDs.",
|
||||
);
|
||||
}
|
||||
let mut parameter = valid.clone();
|
||||
let text = parameter.files.get_mut("main.arcadia").unwrap();
|
||||
*text = text.replace(&format!("input x id \"{}\"", id(1)), "input x");
|
||||
let start = text.find("calculation").unwrap();
|
||||
error_at(
|
||||
¶meter,
|
||||
&fails(¶meter, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Mixed source8 requires explicit UUIDs.",
|
||||
);
|
||||
for version in ["3", "7", "9"] {
|
||||
let mut bad = empty.clone();
|
||||
*bad.files.get_mut("main.arcadia").unwrap() =
|
||||
document("").replacen("arcadia 8", &format!("arcadia {version}"), 1);
|
||||
error_at(
|
||||
&bad,
|
||||
&fails(&bad, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span { start: 8, end: 9 },
|
||||
"invalid_source",
|
||||
"Mixed source requires arcadia 8.",
|
||||
);
|
||||
}
|
||||
for word in [
|
||||
"oa_engineering_property",
|
||||
"sa_engineering_property",
|
||||
"oa_analysis_check",
|
||||
"sa_analysis_check",
|
||||
"oa_analysis_case",
|
||||
"sa_analysis_case",
|
||||
] {
|
||||
let bad = single(&format!("{word} v \"Value\" id \"{}\" {{}}", id(2)));
|
||||
let errors = fails(&bad, ProjectLimits::default());
|
||||
let start = bad.files["main.arcadia"].find(word).unwrap();
|
||||
assert_eq!(errors[0].path, "main.arcadia");
|
||||
assert_eq!(errors[0].diagnostic.code, "invalid_source");
|
||||
assert_eq!(
|
||||
errors[0].diagnostic.span,
|
||||
Span {
|
||||
start,
|
||||
end: start + 1
|
||||
}
|
||||
);
|
||||
assert!(
|
||||
errors[0].diagnostic.message.contains("short"),
|
||||
"{errors:#?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn chain() -> SourceBundle {
|
||||
let mut bundle = single("include \"part.arcadia\";");
|
||||
bundle.files.insert(
|
||||
"part.arcadia".into(),
|
||||
"include \"last.arcadia\";\r\n".into(),
|
||||
);
|
||||
bundle.files.insert("last.arcadia".into(), "//é\r\n".into());
|
||||
bundle
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_exact_input_file_edge_and_depth_boundaries() {
|
||||
let empty = single("");
|
||||
assert_eq!(empty.files["main.arcadia"].len(), 78);
|
||||
for cap in [78, 77] {
|
||||
let limits = ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_source_bytes: cap,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
if cap == 78 {
|
||||
succeeds(&empty, limits, &empty_project(), &[]);
|
||||
} else {
|
||||
error_at(
|
||||
&empty,
|
||||
&fails(&empty, limits),
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 1 },
|
||||
"invalid_source",
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
);
|
||||
}
|
||||
}
|
||||
let bundle = chain();
|
||||
assert_eq!(bundle.files.values().map(String::len).sum::<usize>(), 132);
|
||||
let fits = ProjectLimits {
|
||||
max_files: 3,
|
||||
max_include_edges: 2,
|
||||
max_include_depth: 2,
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 132,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
};
|
||||
succeeds(&bundle, fits, &empty_project(), &[]);
|
||||
for (limits, path, span, code, message) in [
|
||||
(
|
||||
ProjectLimits {
|
||||
max_files: 2,
|
||||
..fits
|
||||
},
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 0 },
|
||||
"project_limit",
|
||||
"Source project exceeds 2 files.",
|
||||
),
|
||||
(
|
||||
ProjectLimits {
|
||||
max_include_edges: 1,
|
||||
..fits
|
||||
},
|
||||
"part.arcadia",
|
||||
Span { start: 0, end: 1 },
|
||||
"project_limit",
|
||||
"Include graph exceeds 1 edges.",
|
||||
),
|
||||
(
|
||||
ProjectLimits {
|
||||
max_include_depth: 1,
|
||||
..fits
|
||||
},
|
||||
"part.arcadia",
|
||||
Span { start: 0, end: 23 },
|
||||
"project_limit",
|
||||
"Include depth exceeds 1.",
|
||||
),
|
||||
(
|
||||
ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_source_bytes: 131,
|
||||
..fits.model
|
||||
},
|
||||
..fits
|
||||
},
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 1 },
|
||||
"invalid_source",
|
||||
"Source8 index/output byte limit exceeded.",
|
||||
),
|
||||
] {
|
||||
error_at(&bundle, &fails(&bundle, limits), path, span, code, message);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_paths_missing_cycles_and_detached_files_are_errors() {
|
||||
let good = chain();
|
||||
succeeds(&good, ProjectLimits::default(), &empty_project(), &[]);
|
||||
let mut missing = good.clone();
|
||||
missing.files.remove("last.arcadia");
|
||||
error_at(
|
||||
&missing,
|
||||
&fails(&missing, ProjectLimits::default()),
|
||||
"part.arcadia",
|
||||
Span { start: 0, end: 23 },
|
||||
"missing_include",
|
||||
"Included file 'last.arcadia' is missing.",
|
||||
);
|
||||
let mut detached = good.clone();
|
||||
detached
|
||||
.files
|
||||
.insert("unused.arcadia".into(), "// valid but detached\n".into());
|
||||
error_at(
|
||||
&detached,
|
||||
&fails(&detached, ProjectLimits::default()),
|
||||
"unused.arcadia",
|
||||
Span { start: 0, end: 0 },
|
||||
"unreachable_file",
|
||||
"Supplied source file is not reachable from the entry file.",
|
||||
);
|
||||
let mut cycle = good.clone();
|
||||
cycle
|
||||
.files
|
||||
.insert("last.arcadia".into(), "include \"part.arcadia\";".into());
|
||||
error_at(
|
||||
&cycle,
|
||||
&fails(&cycle, ProjectLimits::default()),
|
||||
"last.arcadia",
|
||||
Span { start: 0, end: 23 },
|
||||
"include_cycle",
|
||||
"Include cycle: part.arcadia -> last.arcadia -> part.arcadia.",
|
||||
);
|
||||
let mut invalid = good.clone();
|
||||
invalid
|
||||
.files
|
||||
.insert("../escape.arcadia".into(), String::new());
|
||||
error_at(
|
||||
&invalid,
|
||||
&fails(&invalid, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 0 },
|
||||
"invalid_path",
|
||||
"Non-canonical source path '../escape.arcadia'.",
|
||||
);
|
||||
let mut collision = good;
|
||||
collision
|
||||
.files
|
||||
.insert("PART.arcadia".into(), "// detached uppercase\n".into());
|
||||
let errors = fails(&collision, ProjectLimits::default());
|
||||
error_at(
|
||||
&collision,
|
||||
&errors,
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 0 },
|
||||
"duplicate_path",
|
||||
"Case-colliding source paths 'PART.arcadia' and 'part.arcadia'.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_final_nested_model_limit_is_distinct_from_syntax_entries() {
|
||||
let bundle = single(&calc("f", 2, "$x"));
|
||||
let mut expected = empty_project();
|
||||
expected.definitions.insert(
|
||||
id(2),
|
||||
calculation_model("f", 2, a::Expr::Parameter(a::ParameterId(id(1)))),
|
||||
);
|
||||
// Two declarations fit both policies. Parameter and expression make four
|
||||
// final records; this does not pretend to set the internal entry quota.
|
||||
let limits = ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_records: 4,
|
||||
max_graph_objects: 1,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
succeeds(&bundle, limits, &expected, &[]);
|
||||
let errors = fails(
|
||||
&bundle,
|
||||
ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_records: 3,
|
||||
..limits.model
|
||||
},
|
||||
..limits
|
||||
},
|
||||
);
|
||||
error_at(
|
||||
&bundle,
|
||||
&errors,
|
||||
"main.arcadia",
|
||||
Span { start: 0, end: 1 },
|
||||
"model_limit",
|
||||
&format!(
|
||||
"Model exceeds admission limits (1 graph objects, 3 total records, {} ordered references).",
|
||||
usize::MAX
|
||||
),
|
||||
);
|
||||
let errors = fails(
|
||||
&bundle,
|
||||
ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_graph_objects: 0,
|
||||
..limits.model
|
||||
},
|
||||
..limits
|
||||
},
|
||||
);
|
||||
assert_eq!(errors[0].diagnostic.code, "model_limit");
|
||||
let start = bundle.files["main.arcadia"].find("calculation").unwrap();
|
||||
assert_eq!(
|
||||
errors[0].diagnostic.span,
|
||||
Span {
|
||||
start,
|
||||
end: start + 1
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_typed_reference_caller_allowance_has_a_real_positive_control() {
|
||||
let bundle = single(&calc("f", 2, "$x"));
|
||||
let mut expected = empty_project();
|
||||
expected.definitions.insert(
|
||||
id(2),
|
||||
calculation_model("f", 2, a::Expr::Parameter(a::ParameterId(id(1)))),
|
||||
);
|
||||
let fits = ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_step_references: 1,
|
||||
..ParseLimits::UNLIMITED
|
||||
},
|
||||
..ProjectLimits::default()
|
||||
};
|
||||
succeeds(&bundle, fits, &expected, &[]);
|
||||
let start = bundle.files["main.arcadia"].find("$x").unwrap() + 1;
|
||||
error_at(
|
||||
&bundle,
|
||||
&fails(
|
||||
&bundle,
|
||||
ProjectLimits {
|
||||
model: ParseLimits {
|
||||
max_step_references: 0,
|
||||
..fits.model
|
||||
},
|
||||
..fits
|
||||
},
|
||||
),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Source8 references limit exceeded.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_malformed_duplicate_and_wrong_domain_references() {
|
||||
let calculation = calc("f", 2, "1");
|
||||
let valid = single(&calculation);
|
||||
let mut expected = empty_project();
|
||||
expected.definitions.insert(
|
||||
id(2),
|
||||
calculation_model("f", 2, a::Expr::Number(Exact::from_integer(1))),
|
||||
);
|
||||
succeeds(&valid, ProjectLimits::default(), &expected, &[]);
|
||||
|
||||
let mut malformed = valid.clone();
|
||||
*malformed.files.get_mut("main.arcadia").unwrap() = document(&calc("f", 2, ""));
|
||||
let start = malformed.files["main.arcadia"].find("body ;").unwrap() + 5;
|
||||
error_at(
|
||||
&malformed,
|
||||
&fails(&malformed, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Expected an analysis expression.",
|
||||
);
|
||||
|
||||
let mut unresolved = single("include \"typed.arcadia\";");
|
||||
unresolved
|
||||
.files
|
||||
.insert("typed.arcadia".into(), calc("f", 2, "@missing(x: 1)"));
|
||||
let start = unresolved.files["typed.arcadia"].find("@missing").unwrap() + 1;
|
||||
error_at(
|
||||
&unresolved,
|
||||
&fails(&unresolved, ProjectLimits::default()),
|
||||
"typed.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 7,
|
||||
},
|
||||
"invalid_source",
|
||||
"Unknown global reference '@missing'.",
|
||||
);
|
||||
|
||||
let ordinary = format!("oa_entity q \"Entity\" id \"{}\" {{}}", id(3));
|
||||
let wrong = single(&format!("{ordinary}\n{}", calc("f", 2, "@q(x: 1)")));
|
||||
let start = wrong.files["main.arcadia"].find("@q").unwrap() + 1;
|
||||
error_at(
|
||||
&wrong,
|
||||
&fails(&wrong, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start,
|
||||
end: start + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Wrong reference domain for '@q': Element(Entity, Oa).",
|
||||
);
|
||||
|
||||
let duplicate = single(&format!("{calculation}\n{}", calc("f", 3, "1")));
|
||||
let errors = fails(&duplicate, ProjectLimits::default());
|
||||
let second = calc("f", 3, "1");
|
||||
let span = authored_span(&duplicate.files["main.arcadia"], &second);
|
||||
error_at(
|
||||
&duplicate,
|
||||
&errors,
|
||||
"main.arcadia",
|
||||
span,
|
||||
"invalid_source",
|
||||
"Duplicate global alias.",
|
||||
);
|
||||
assert_eq!(errors[0].related.len(), 1);
|
||||
assert_eq!(errors[0].related[0].path, "main.arcadia");
|
||||
assert_eq!(
|
||||
errors[0].related[0].span,
|
||||
authored_span(&duplicate.files["main.arcadia"], &calculation)
|
||||
);
|
||||
|
||||
let second = calc("g", 2, "1");
|
||||
let duplicate_id = single(&format!("{calculation}\n{second}"));
|
||||
error_at(
|
||||
&duplicate_id,
|
||||
&fails(&duplicate_id, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
authored_span(&duplicate_id.files["main.arcadia"], &second),
|
||||
"invalid_source",
|
||||
"Duplicate global identity.",
|
||||
);
|
||||
|
||||
let duplicate_parameter = calc("f", 2, "$x").replace(
|
||||
"result Number;",
|
||||
&format!("input x id \"{}\" : Number; result Number;", id(4)),
|
||||
);
|
||||
let bad = single(&duplicate_parameter);
|
||||
let at = bad.files["main.arcadia"].rfind("input x").unwrap() + 6;
|
||||
error_at(
|
||||
&bad,
|
||||
&fails(&bad, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start: at,
|
||||
end: at + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Duplicate parameter name.",
|
||||
);
|
||||
let second_parameter = format!("input y id \"{}\" : Number;", id(1));
|
||||
let duplicate_parameter_id = calc("f", 2, "$x").replace(
|
||||
"result Number;",
|
||||
&format!("{second_parameter} result Number;"),
|
||||
);
|
||||
let bad = single(&duplicate_parameter_id);
|
||||
let at = authored_span(&bad.files["main.arcadia"], &second_parameter).start + 11;
|
||||
error_at(
|
||||
&bad,
|
||||
&fails(&bad, ProjectLimits::default()),
|
||||
"main.arcadia",
|
||||
Span {
|
||||
start: at,
|
||||
end: at + 1,
|
||||
},
|
||||
"invalid_source",
|
||||
"Duplicate parameter identity.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_unicode_columns_are_not_utf8_byte_columns() {
|
||||
let source = "arcadia 8;\r\nproject p \"港🌊\" id \"00000000-0000-0000-0000-000000000001\" { oa_activity w \"Work\" id \"00000000-0000-0000-0000-000000000002\" {} }\r\n";
|
||||
let bundle = SourceBundle {
|
||||
entry: "main.arcadia".into(),
|
||||
files: BTreeMap::from([("main.arcadia".into(), source.into())]),
|
||||
};
|
||||
let mut expected = empty_project();
|
||||
expected.name = "港🌊".into();
|
||||
expected.elements.insert(
|
||||
id(2),
|
||||
Element {
|
||||
id: id(2),
|
||||
alias: "w".into(),
|
||||
name: "Work".into(),
|
||||
description: String::new(),
|
||||
parent: None,
|
||||
body: ElementBody::Oa(OaBody::Activity),
|
||||
},
|
||||
);
|
||||
let warnings = [
|
||||
("unallocated_leaf", Some("allocation"), "Allocate leaf 'Work' to a performer."),
|
||||
("work_without_behavior_context", None, "Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence."),
|
||||
].map(|(code, field, message)| SourceDiagnostic {
|
||||
path: "main.arcadia".into(), related: Vec::new(),
|
||||
diagnostic: language::Diagnostic { severity: validation::Severity::Warning, code: code.into(), object_id: Some(id(2)), field: field.map(str::to_owned), message: message.into(), span: Span { start: 76, end: 141 }, line: 2, column: 60 },
|
||||
});
|
||||
succeeds(&bundle, ProjectLimits::default(), &expected, &warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_mixed_read_ordinary_parity_uses_an_independent_complete_model() {
|
||||
let b = single(&format!(
|
||||
"oa_entity team \"Team\" id \"{}\" {{ oa_actor sailor \"Sailor\" id \"{}\" {{ description \"海\"; }} }}",
|
||||
id(2),
|
||||
id(3)
|
||||
));
|
||||
let mut expected = empty_project();
|
||||
for (n, alias, name, description, parent, body) in [
|
||||
(2, "team", "Team", "", None, ElementBody::Oa(OaBody::Entity)),
|
||||
(
|
||||
3,
|
||||
"sailor",
|
||||
"Sailor",
|
||||
"海",
|
||||
Some(id(2)),
|
||||
ElementBody::Oa(OaBody::Actor),
|
||||
),
|
||||
] {
|
||||
expected.elements.insert(
|
||||
id(n),
|
||||
Element {
|
||||
id: id(n),
|
||||
alias: alias.into(),
|
||||
name: name.into(),
|
||||
description: description.into(),
|
||||
parent,
|
||||
body,
|
||||
},
|
||||
);
|
||||
}
|
||||
succeeds(&b, ProjectLimits::default(), &expected, &[]);
|
||||
let ordinary = language::parse_with_report(&b.files["main.arcadia"]).unwrap();
|
||||
assert!(ordinary.migration.is_none());
|
||||
assert_eq!(ordinary.project, expected);
|
||||
let compiled = source_project::compile_bundle(&b, ProjectLimits::default()).unwrap();
|
||||
assert_eq!(compiled.decoded.project, expected);
|
||||
assert_eq!(compiled.bundle, b);
|
||||
}
|
||||
Reference in New Issue
Block a user