Preserve loaded draft bytes and model history, reject stale workspace and input events, and keep captured Escape under modal priority. Verified 181 focused Windows tests and strict desktop Clippy on exact inputs4; earlier full737 scope and component/native preparation evidence are archived separately. Native navigation acceptance remains pending.
387 lines
14 KiB
Rust
387 lines
14 KiB
Rust
//! Prepared outline tests. Explicit compatibility tests additionally exercise
|
|
//! the existing parsers/analyze; the outline entry itself never calls them.
|
|
use super::*;
|
|
|
|
fn limits() -> OutlineLimits {
|
|
OutlineLimits {
|
|
max_source_bytes: 1024 * 1024,
|
|
max_tokens: 100_000,
|
|
max_declarations: 1_000,
|
|
}
|
|
}
|
|
|
|
fn declaration<'a>(result: &'a Outline<'_>, alias: &str) -> &'a OutlineDeclaration {
|
|
result
|
|
.declarations
|
|
.iter()
|
|
.find(|d| d.declaration.alias == alias)
|
|
.unwrap()
|
|
}
|
|
|
|
fn text(source: &str, span: Span) -> &str {
|
|
&source[span.start..span.end]
|
|
}
|
|
|
|
#[test]
|
|
fn outline_roles_preserve_unicode_crlf_and_exact_escaped_uuid_literal() {
|
|
let source = "// 検査 🚒\r\noa_actor responder \"救助\" id \"\\u00300000000-0000-0000-0000-000000000001\" {}\r\nsource @ // 使用\r\n responder;";
|
|
let before = source.as_bytes().to_vec();
|
|
let result = outline(source, limits()).unwrap();
|
|
assert_eq!(result.source.as_bytes(), before);
|
|
assert_eq!(result.source.as_ptr(), source.as_ptr());
|
|
let d = declaration(&result, "responder");
|
|
assert_eq!(text(source, d.kind_span), "oa_actor");
|
|
assert_eq!(text(source, d.alias_span), "responder");
|
|
assert_eq!(
|
|
text(source, d.id_literal_span.unwrap()),
|
|
"\"\\u00300000000-0000-0000-0000-000000000001\""
|
|
);
|
|
assert_eq!(d.declaration.id, Some(Id::from_u128(1)));
|
|
assert_eq!(d.declaration.span.start, source.find("oa_actor").unwrap());
|
|
assert_eq!(d.declaration.span.end, source.find("{}\r\n").unwrap() + 2);
|
|
let mut end = 0;
|
|
for token in &result.tokens {
|
|
assert_eq!(token.span.start, end);
|
|
assert!(token.span.start < token.span.end);
|
|
assert!(source.is_char_boundary(token.span.start));
|
|
assert!(source.is_char_boundary(token.span.end));
|
|
end = token.span.end;
|
|
}
|
|
assert_eq!(end, source.len());
|
|
assert!(!result.has_lexical_errors);
|
|
assert!(!result.has_unmatched_braces);
|
|
}
|
|
|
|
#[test]
|
|
fn closed_id_at_eof_is_inside_the_declaration_extent() {
|
|
for source in [
|
|
"oa_actor a \"Actor\" id \"00000000-0000-0000-0000-000000000001\"",
|
|
"node placed id \"00000000-0000-0000-0000-000000000001\"",
|
|
] {
|
|
let result = outline(source, limits()).unwrap();
|
|
let d = &result.declarations[0];
|
|
let literal = d.id_literal_span.unwrap();
|
|
assert_eq!(d.declaration.id, Some(Id::from_u128(1)));
|
|
assert_eq!(d.declaration.span.end, source.len());
|
|
assert!(d.declaration.span.start <= literal.start);
|
|
assert!(literal.end <= d.declaration.span.end);
|
|
}
|
|
for source in ["oa_actor a \"Actor\"", "oa_actor a \"Actor\" id"] {
|
|
let result = outline(source, limits()).unwrap();
|
|
let d = &result.declarations[0];
|
|
assert_eq!(d.declaration.span.end, source.len());
|
|
assert_eq!(d.declaration.id, None);
|
|
assert_eq!(d.id_literal_span, None);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn missing_bad_and_historical_ids_never_allocate_authored_identity() {
|
|
let source = "oa_actor missing \"No ID\" {}\noa_actor bad \"Bad ID\" id \"not-a-uuid\" {}\noa_actor historical \"00000000-0000-0000-0000-000000000001\" { history uuid \"00000000-0000-0000-0000-000000000001\"; }";
|
|
let first = outline(source, limits()).unwrap();
|
|
let second = outline(source, limits()).unwrap();
|
|
assert_eq!(first, second);
|
|
assert!(
|
|
first
|
|
.declarations
|
|
.iter()
|
|
.all(|d| d.declaration.id.is_none())
|
|
);
|
|
assert_eq!(declaration(&first, "missing").id_literal_span, None);
|
|
assert_eq!(declaration(&first, "historical").id_literal_span, None);
|
|
assert_eq!(
|
|
text(source, declaration(&first, "bad").id_literal_span.unwrap()),
|
|
"\"not-a-uuid\""
|
|
);
|
|
// Balanced tokens are not a successful syntax/semantic validation claim.
|
|
assert!(!first.has_lexical_errors);
|
|
assert!(!first.has_unmatched_braces);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_full_scan_keeps_closed_children_and_reports_lexical_body_facts() {
|
|
let source = "oa_entity outer \"Outer\" {\n oa_actor ready \"Ready\" { description \"} stays text\"; }\n oa_actor pending \"Pending\" { description \"unfinished 🌊";
|
|
let result = outline(source, limits()).unwrap();
|
|
assert_eq!(result.source, source);
|
|
assert_eq!(result.tokens.last().unwrap().span.end, source.len());
|
|
assert!(result.has_lexical_errors);
|
|
assert!(result.has_unmatched_braces);
|
|
let ready = declaration(&result, "ready");
|
|
assert_eq!(
|
|
text(source, ready.declaration.span),
|
|
"oa_actor ready \"Ready\" { description \"} stays text\"; }"
|
|
);
|
|
assert_eq!(
|
|
declaration(&result, "outer").declaration.span.end,
|
|
source.len()
|
|
);
|
|
assert_eq!(
|
|
declaration(&result, "pending").declaration.span.end,
|
|
source.len()
|
|
);
|
|
let unmatched_close = outline("}\n// brace in comment {\n", limits()).unwrap();
|
|
assert!(unmatched_close.has_unmatched_braces);
|
|
assert!(!unmatched_close.has_lexical_errors);
|
|
}
|
|
|
|
#[test]
|
|
fn missing_body_syntax_is_unchecked_even_when_known_error_flags_are_clear() {
|
|
let source = "oa_actor a \"A\" id \"00000000-0000-0000-0000-000000000001\"";
|
|
let result = outline(source, limits()).unwrap();
|
|
assert_eq!(result.declarations.len(), 1);
|
|
assert!(!result.has_lexical_errors);
|
|
assert!(!result.has_unmatched_braces);
|
|
// There is deliberately no `valid` flag: the known-complete fact is scanning
|
|
// these bytes, not the missing body becoming legal or the draft being applied.
|
|
assert_eq!(result.source, source);
|
|
}
|
|
|
|
#[test]
|
|
fn keyword_alias_and_collisions_keep_every_source_ordered_header() {
|
|
let source = "project root_model \"Model\" {\n oa_actor oa_activity \"First\" id \"00000000-0000-0000-0000-000000000001\" {}\n oa_actor oa_activity \"Second\" id \"00000000-0000-0000-0000-000000000001\" {}\n diagram view_one \"View\" {}\n node placement { diagram @view_one; }\n}";
|
|
let result = outline(source, limits()).unwrap();
|
|
assert_eq!(result.declarations.len(), 5);
|
|
let duplicates: Vec<_> = result
|
|
.declarations
|
|
.iter()
|
|
.filter(|d| d.declaration.alias == "oa_activity")
|
|
.collect();
|
|
assert_eq!(duplicates.len(), 2);
|
|
assert!(duplicates[0].alias_span.start < duplicates[1].alias_span.start);
|
|
assert_eq!(duplicates[0].declaration.id, duplicates[1].declaration.id);
|
|
assert!(
|
|
result
|
|
.tokens
|
|
.iter()
|
|
.any(|t| t.span == duplicates[0].alias_span && t.class == TokenClass::Keyword)
|
|
);
|
|
assert_eq!(
|
|
text(source, declaration(&result, "placement").kind_span),
|
|
"node"
|
|
);
|
|
assert_eq!(declaration(&result, "placement").declaration.id, None);
|
|
}
|
|
|
|
#[test]
|
|
fn bounds_refuse_before_returning_a_partial_outline() {
|
|
let source = "oa_actor a \"A\" {}\noa_actor b \"B\" {}";
|
|
let admitted = outline(source, limits()).unwrap();
|
|
let exact = OutlineLimits {
|
|
max_source_bytes: source.len(),
|
|
max_tokens: admitted.tokens.len(),
|
|
max_declarations: 2,
|
|
};
|
|
assert_eq!(outline(source, exact).unwrap(), admitted);
|
|
for resource in [
|
|
OutlineResource::SourceBytes,
|
|
OutlineResource::Tokens,
|
|
OutlineResource::Declarations,
|
|
] {
|
|
let mut lower = exact;
|
|
match resource {
|
|
OutlineResource::SourceBytes => lower.max_source_bytes -= 1,
|
|
OutlineResource::Tokens => lower.max_tokens -= 1,
|
|
OutlineResource::Declarations => lower.max_declarations -= 1,
|
|
}
|
|
let error = outline(source, lower).unwrap_err();
|
|
assert_eq!(error.resource, resource);
|
|
assert!(error.observed_at_least > error.limit);
|
|
assert!(source.is_char_boundary(error.offset));
|
|
if resource == OutlineResource::Declarations {
|
|
assert_eq!(error.offset, source.find("oa_actor b").unwrap());
|
|
}
|
|
}
|
|
let empty = outline(
|
|
"",
|
|
OutlineLimits {
|
|
max_source_bytes: 0,
|
|
max_tokens: 0,
|
|
max_declarations: 0,
|
|
},
|
|
)
|
|
.unwrap();
|
|
assert!(empty.tokens.is_empty() && empty.declarations.is_empty());
|
|
let unicode = "// 🌊\r\noa_actor a \"A\" {}";
|
|
let error = outline(
|
|
unicode,
|
|
OutlineLimits {
|
|
max_source_bytes: 4,
|
|
..limits()
|
|
},
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error.resource, OutlineResource::SourceBytes);
|
|
assert_eq!(error.offset, 0);
|
|
let error = outline(
|
|
unicode,
|
|
OutlineLimits {
|
|
max_tokens: 1,
|
|
..limits()
|
|
},
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error.resource, OutlineResource::Tokens);
|
|
assert_eq!(error.offset, unicode.find('\n').unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn outline_ranges_follow_current_bytes_without_identity_stabilization() {
|
|
let old = "oa_actor a \"A\" {}\nsource @a;";
|
|
let current = format!("// 航海 🚒\r\n{old}");
|
|
let old_outline = outline(old, limits()).unwrap();
|
|
let current_outline = outline(¤t, limits()).unwrap();
|
|
let shift = current.len() - old.len();
|
|
let old_role = &old_outline.declarations[0];
|
|
let current_role = ¤t_outline.declarations[0];
|
|
assert_eq!(
|
|
current_role.alias_span.start,
|
|
old_role.alias_span.start + shift
|
|
);
|
|
assert_eq!(
|
|
current_role.kind_span.start,
|
|
old_role.kind_span.start + shift
|
|
);
|
|
assert_eq!(
|
|
current_role.declaration.span.end,
|
|
old_role.declaration.span.end + shift
|
|
);
|
|
assert_eq!(current_outline.source, current);
|
|
assert_eq!(old_outline.source, old);
|
|
assert_eq!(current_role.declaration.id, None);
|
|
assert!(!current_outline.source.contains(" id "));
|
|
// Workspace revision/stale-handle rejection belongs to the query/adapter,
|
|
// not a claim that an old owned span can be applied to any other string.
|
|
}
|
|
|
|
#[test]
|
|
fn deeply_nested_bounds_use_the_same_iterative_walk_and_declaration_budget() {
|
|
let mut source = String::new();
|
|
for index in 0..600 {
|
|
source.push_str(&format!("oa_activity work_{index} \"Work\" {{\n"));
|
|
}
|
|
source.push_str(&"}\n".repeat(600));
|
|
let result = outline(&source, limits()).unwrap();
|
|
assert_eq!(result.declarations.len(), 600);
|
|
assert!(!result.has_unmatched_braces);
|
|
assert_eq!(
|
|
result.declarations[0].declaration.span.end,
|
|
source.len() - 1
|
|
);
|
|
let error = outline(
|
|
&source,
|
|
OutlineLimits {
|
|
max_declarations: 599,
|
|
..limits()
|
|
},
|
|
)
|
|
.unwrap_err();
|
|
assert_eq!(error.resource, OutlineResource::Declarations);
|
|
assert_eq!(error.observed_at_least, 600);
|
|
assert_eq!(error.offset, source.find("oa_activity work_599").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn noncanonical_uuid_spellings_have_no_outline_or_analysis_identity() {
|
|
for raw in [
|
|
"00000000000000000000000000000001",
|
|
"{00000000-0000-0000-0000-000000000001}",
|
|
"urn:uuid:00000000-0000-0000-0000-000000000001",
|
|
] {
|
|
assert!(
|
|
Id::parse_str(raw).is_ok(),
|
|
"The UUID crate alone is too permissive"
|
|
);
|
|
assert!(!has_uuid_header_shape(raw));
|
|
let literal = format!("\"{raw}\"");
|
|
let modern = format!("arcadia 7; project model \"Model\" id {literal} {{}}");
|
|
let before = modern.clone();
|
|
let result = outline(&modern, limits()).unwrap();
|
|
let d = declaration(&result, "model");
|
|
assert_eq!(d.declaration.id, None);
|
|
assert_eq!(text(&modern, d.id_literal_span.unwrap()), literal);
|
|
assert_eq!(modern, before);
|
|
let errors = parse(&modern).unwrap_err();
|
|
assert!(
|
|
errors
|
|
.iter()
|
|
.any(|error| error.message.contains("8-4-4-4-12")
|
|
&& error.span.start == modern.find(&literal).unwrap())
|
|
);
|
|
// Intentional compatibility correction: old analyze yielded Some(Id(1))
|
|
// here despite lint rejecting the header. It now shares outline's None.
|
|
let analysis = analyze(&modern);
|
|
assert_eq!(
|
|
analysis
|
|
.declarations
|
|
.iter()
|
|
.find(|d| d.alias == "model")
|
|
.unwrap()
|
|
.id,
|
|
None
|
|
);
|
|
assert!(
|
|
analysis
|
|
.diagnostics
|
|
.iter()
|
|
.any(|error| error.message.contains("8-4-4-4-12"))
|
|
);
|
|
let legacy_source = format!("arcadia 1; project \"Legacy\" id {literal} {{}}");
|
|
let legacy_errors = legacy::parse(&legacy_source).unwrap_err();
|
|
assert!(
|
|
legacy_errors
|
|
.iter()
|
|
.any(|error| error.message.contains("8-4-4-4-12"))
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn uppercase_and_json_escaped_header_ids_survive_both_parsers_and_outline() {
|
|
let expected = Id::from_u128(0xabcdef);
|
|
let canonical = expected.to_string();
|
|
let uppercase = canonical.to_uppercase();
|
|
assert_ne!(uppercase, canonical);
|
|
for raw in [uppercase.clone(), format!("\\u0030{}", &uppercase[1..])] {
|
|
let literal = format!("\"{raw}\"");
|
|
let decoded: String = serde_json::from_str(&literal).unwrap();
|
|
assert!(has_uuid_header_shape(&decoded));
|
|
let modern = format!("arcadia 7; project model \"Model\" id {literal} {{}}");
|
|
let result = outline(&modern, limits()).unwrap();
|
|
let d = declaration(&result, "model");
|
|
assert_eq!(d.declaration.id, Some(expected));
|
|
assert_eq!(text(&modern, d.id_literal_span.unwrap()), literal);
|
|
assert_eq!(result.source.as_bytes(), modern.as_bytes());
|
|
assert_eq!(parse(&modern).unwrap().id, expected);
|
|
let analysis = analyze(&modern);
|
|
assert_eq!(analysis.declarations[0].id, Some(expected));
|
|
assert!(
|
|
!analysis
|
|
.diagnostics
|
|
.iter()
|
|
.any(|error| error.severity == crate::validation::Severity::Error)
|
|
);
|
|
let legacy_source = format!("arcadia 1; project \"Legacy\" id {literal} {{}}");
|
|
assert_eq!(legacy::parse(&legacy_source).unwrap().id, expected);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn header_shape_still_requires_valid_hexadecimal_uuid_data() {
|
|
let raw = "00000000-0000-0000-0000-00000000000Z";
|
|
assert!(has_uuid_header_shape(raw));
|
|
assert!(Id::parse_str(raw).is_err());
|
|
let literal = format!("\"{raw}\"");
|
|
let source = format!("arcadia 7; project model \"Model\" id {literal} {{}}");
|
|
let result = outline(&source, limits()).unwrap();
|
|
let d = declaration(&result, "model");
|
|
assert_eq!(d.declaration.id, None);
|
|
assert_eq!(text(&source, d.id_literal_span.unwrap()), literal);
|
|
assert!(
|
|
parse(&source)
|
|
.unwrap_err()
|
|
.iter()
|
|
.any(|error| error.message == "Invalid UUID identity.")
|
|
);
|
|
assert_eq!(analyze(&source).declarations[0].id, None);
|
|
}
|