use sysml_v2_parser::ast::*;
use sysml_v2_parser::{parse, parse_with_diagnostics};
#[test]
fn test_parse_with_diagnostics_partial_ast_and_multiple_errors() {
let input = "package Foo;\nnot valid\nalso bad\npackage Bar;";
let result = parse_with_diagnostics(input);
assert!(!result.is_ok(), "should have parse errors");
assert_eq!(result.errors.len(), 2, "should report two parse errors");
assert_eq!(
result.root.elements.len(),
2,
"partial AST should contain both valid packages"
);
let names: Vec<&str> = result
.root
.elements
.iter()
.filter_map(|n| {
if let RootElement::Package(p) = &n.value {
p.value.identification.name.as_deref()
} else {
None
}
})
.collect();
assert_eq!(names, ["Foo", "Bar"]);
for err in &result.errors {
assert!(
err.found.is_some(),
"error should have 'found' snippet: {}",
err.message
);
assert!(
err.expected.is_some(),
"error should have 'expected' context: {}",
err.message
);
assert!(
err.expected
.as_deref()
.is_some_and(|e| e.contains("package") || e.contains("namespace")),
"expected should mention package or namespace: {:?}",
err.expected
);
assert!(err.code.is_some(), "error should have a code");
}
assert!(
result.errors[0]
.found
.as_deref()
.is_some_and(|f| f.contains("not")),
"first error found should mention invalid token: {:?}",
result.errors[0].found
);
}
#[test]
fn test_parse_error_expected_end_of_input_has_found() {
let input = "package Foo; package Bar; garbage";
let result = parse(input);
let err = result.unwrap_err();
assert!(
err.message.contains("expected end of input"),
"error should be 'expected end of input': {}",
err
);
assert!(
err.found.is_some(),
"expected end of input error should have 'found': {}",
err
);
assert!(
err.found.as_deref().is_some_and(|f| f.contains("garbage")),
"found should show trailing text: {:?}",
err.found
);
assert_eq!(err.code.as_deref(), Some("expected_end_of_input"));
}
#[test]
fn test_parse_error_display_includes_found_and_location() {
let input = "package Foo;\nxyz";
let result = parse_with_diagnostics(input);
let err = &result.errors[0];
let display = err.to_string();
assert!(
display.contains("line"),
"Display should include line number"
);
assert!(
err.found.as_ref().is_some_and(|f| display.contains(f)),
"Display should include found snippet: {}",
display
);
}
#[test]
fn test_action_def_is_not_parsed_as_action_usage() {
let input = r#"package P {
action def ExecutePatrol {
}
}"#;
let root = sysml_v2_parser::parse_root(input).expect("should parse");
let pkg = match &root.elements[0].value {
sysml_v2_parser::ast::RootElement::Package(p) => &p.value,
_ => panic!("expected package root element"),
};
let sysml_v2_parser::ast::PackageBody::Brace { elements } = &pkg.body else {
panic!("expected brace body");
};
let first = elements.first().expect("expected a body element");
match &first.value {
sysml_v2_parser::ast::PackageBodyElement::ActionDef(a) => {
assert_eq!(
a.value.identification.name.as_deref(),
Some("ExecutePatrol"),
"expected ActionDef name ExecutePatrol"
);
}
other => panic!("expected ActionDef, got {:?}", other),
}
}
#[test]
fn test_parse_with_diagnostics_recovers_and_reports_later_errors() {
let input = r#"package P {
action def A {
badstmt {};
badstmt2 {};
}
action def B { }
}"#;
let result = parse_with_diagnostics(input);
assert!(
result.errors.len() >= 2,
"expected 2+ diagnostics, got: {:?}",
result.errors
);
let pkg = match &result.root.elements[0].value {
sysml_v2_parser::ast::RootElement::Package(p) => &p.value,
_ => panic!("expected package root element"),
};
let sysml_v2_parser::ast::PackageBody::Brace { elements } = &pkg.body else {
panic!("expected brace body");
};
let has_b = elements.iter().any(|e| match &e.value {
sysml_v2_parser::ast::PackageBodyElement::ActionDef(a) => {
a.value.identification.name.as_deref() == Some("B")
}
_ => false,
});
assert!(has_b, "expected later ActionDef `B` to still be parsed");
}
#[test]
fn test_package_body_recovery_skips_annotated_member_and_keeps_later_sibling() {
let input = "package P {\n#fmeaspec requirement req1 { }\npart def Good;\n}";
let result = parse(input).expect("parse should succeed with recovery");
let pkg = match &result.elements[0].value {
RootElement::Package(p) => &p.value,
_ => panic!("expected package"),
};
let elements = match &pkg.body {
PackageBody::Brace { elements } => elements,
_ => panic!("expected brace body"),
};
assert!(
elements.iter().any(|e| matches!(e.value, PackageBodyElement::PartDef(_))),
"later valid sibling should still be present after recovering from annotated unsupported member"
);
assert!(
elements
.iter()
.any(|e| matches!(e.value, PackageBodyElement::Error(_))),
"recovered package region should be represented explicitly in the AST"
);
}
#[test]
fn test_package_body_recovery_skips_malformed_abstract_part_and_keeps_next_member() {
let input = "package P {\nabstract part def Broken { invalid }\npart def Good;\n}";
let result = parse(input).expect("parse should succeed");
let pkg = match &result.elements[0].value {
RootElement::Package(p) => &p.value,
_ => panic!("expected package"),
};
let elements = match &pkg.body {
PackageBody::Brace { elements } => elements,
_ => panic!("expected brace body"),
};
assert_eq!(
elements
.iter()
.filter(|e| matches!(e.value, PackageBodyElement::PartDef(_)))
.count(),
2,
"both part declarations should map to dedicated PartDef nodes"
);
}
#[test]
fn test_part_def_recovery_preserves_other_member_and_later_sibling() {
let input =
"package P {\npart def Vehicle {\nstate monitor: Mode;\nattribute mass: MassValue;\n}\n}";
let result = parse_with_diagnostics(input);
let pkg = match &result.root.elements[0].value {
RootElement::Package(p) => &p.value,
_ => panic!("expected package"),
};
let PackageBody::Brace { elements } = &pkg.body else {
panic!("expected brace body");
};
let part_def = elements
.iter()
.find_map(|e| match &e.value {
PackageBodyElement::PartDef(p) => Some(&p.value),
_ => None,
})
.expect("part def should be present");
let sysml_v2_parser::ast::PartDefBody::Brace { elements } = &part_def.body else {
panic!("expected part def body");
};
assert!(
elements.iter().any(|e| matches!(
e.value,
sysml_v2_parser::ast::PartDefBodyElement::Other(_)
| sysml_v2_parser::ast::PartDefBodyElement::OpaqueMember(_)
)),
"library-tolerant unmodeled part members should be preserved explicitly"
);
assert!(
elements.iter().any(|e| matches!(
&e.value,
sysml_v2_parser::ast::PartDefBodyElement::AttributeDef(a)
if a.value.typing.is_some()
)),
"later modeled members should still parse"
);
}
#[test]
fn test_state_def_recovery_no_longer_truncates_body() {
let input = "package P {\nstate def Machine {\nunknown stuff;\ntransition t then Ready;\n}\n}";
let result = parse_with_diagnostics(input);
let pkg = match &result.root.elements[0].value {
RootElement::Package(p) => &p.value,
_ => panic!("expected package"),
};
let PackageBody::Brace { elements } = &pkg.body else {
panic!("expected brace body");
};
let state_def = elements
.iter()
.find_map(|e| match &e.value {
PackageBodyElement::StateDef(s) => Some(&s.value),
_ => None,
})
.expect("state def should be present");
let sysml_v2_parser::ast::StateDefBody::Brace { elements } = &state_def.body else {
panic!("expected state body");
};
assert!(
elements
.iter()
.any(|e| matches!(e.value, sysml_v2_parser::ast::StateDefBodyElement::Other(_))),
"unknown state members should be preserved explicitly instead of truncating the body"
);
assert!(
elements.iter().any(|e| matches!(
e.value,
sysml_v2_parser::ast::StateDefBodyElement::Transition(_)
)),
"later valid state members should still parse"
);
}
#[test]
fn test_parse_with_diagnostics_accepts_structured_requirement_attributes() {
let input = "package P {\nrequirement def R {\nsubject vehicle : Vehicle;\nattribute massActual: MassValue;\nrequire constraint { }\n}\n}";
let result = parse_with_diagnostics(input);
assert!(
result.errors.is_empty(),
"structured requirement attributes should not produce recovery diagnostics: {:?}",
result.errors
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_actor_name_in_use_case_body() {
let input = "package P {\nuse case def U {\nactor: User;\nobjective { }\n}\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"missing actor name should produce diagnostics"
);
let err = result
.errors
.iter()
.find(|e| e.code.as_deref() == Some("missing_member_name"))
.expect("expected missing_member_name diagnostic");
assert_eq!(err.expected.as_deref(), Some("actor name before ':'"));
assert!(
err.suggestion
.as_deref()
.is_some_and(|s| s.contains("actor user: User;")),
"diagnostic should show an actor example fix"
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_subject_type_in_requirement_body() {
let input = "package P {\nrequirement def R {\nsubject laptop: ;\nrequire constraint { }\n}\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"missing subject type should produce diagnostics"
);
let err = result
.errors
.iter()
.find(|e| e.code.as_deref() == Some("missing_type_reference"))
.expect("expected missing_type_reference diagnostic");
assert_eq!(err.expected.as_deref(), Some("subject type after ':'"));
assert!(
err.suggestion
.as_deref()
.is_some_and(|s| s.contains("subject laptop: Laptop;")),
"diagnostic should show a subject type example fix"
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_actor_type_in_use_case_body() {
let input = "package P {\nuse case def U {\nactor user: ;\nobjective { }\n}\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"missing actor type should produce diagnostics"
);
let err = result
.errors
.iter()
.find(|e| e.code.as_deref() == Some("missing_type_reference"))
.expect("expected missing_type_reference diagnostic");
assert_eq!(err.expected.as_deref(), Some("actor type after ':'"));
assert!(
err.suggestion
.as_deref()
.is_some_and(|s| s.contains("actor user: User;")),
"diagnostic should show an actor type example fix"
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_state_name_in_state_body() {
let input = "package P {\nstate def Machine {\nstate: Mode;\ntransition t then Ready;\n}\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"missing state name should produce diagnostics"
);
let err = result
.errors
.iter()
.find(|e| e.expected.as_deref() == Some("state name before ':'"))
.expect("expected state-name diagnostic");
assert!(
err.suggestion
.as_deref()
.is_some_and(|s| s.contains("state ready: Mode;")),
"diagnostic should show a state example fix"
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_part_type_in_part_body() {
let input = "package P {\npart def Vehicle {\npart wheel: ;\nattribute mass: MassValue;\n}\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"missing part type should produce diagnostics"
);
let err = result
.errors
.iter()
.find(|e| e.expected.as_deref() == Some("part type after ':'"))
.expect("expected part-type diagnostic");
assert!(
err.suggestion
.as_deref()
.is_some_and(|s| s.contains("part wheel: Wheel;")),
"diagnostic should show a part type example fix"
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_attribute_type_in_part_body() {
let input = "package P {\npart Vehicle {\nattribute bad : ;\nattribute ok : MassValue;\n}\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"missing attribute type should produce diagnostics"
);
assert!(
!result.errors.is_empty(),
"missing attribute type should be reported"
);
let pkg = match &result.root.elements[0].value {
RootElement::Package(p) => &p.value,
other => panic!("expected package, got {other:?}"),
};
let PackageBody::Brace { elements } = &pkg.body else {
panic!("expected package body");
};
let part = elements
.iter()
.find_map(|e| match &e.value {
PackageBodyElement::PartUsage(p) => Some(&p.value),
_ => None,
})
.expect("part usage should survive recovery");
let PartUsageBody::Brace { elements } = &part.body else {
panic!("expected part body");
};
assert!(
elements.iter().any(|e| matches!(
&e.value,
PartUsageBodyElement::AttributeUsage(a) if a.value.name == "ok"
)),
"later attribute sibling should remain parseable"
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_occurrence_type_and_keeps_sibling() {
let input = "package P {\noccurrence bad defined by ;\npart def Good;\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"missing occurrence type should produce diagnostics"
);
assert!(
!result.errors.is_empty(),
"missing occurrence type should be reported"
);
let pkg = match &result.root.elements[0].value {
RootElement::Package(p) => &p.value,
other => panic!("expected package, got {other:?}"),
};
let PackageBody::Brace { elements } = &pkg.body else {
panic!("expected package body");
};
assert!(
elements
.iter()
.any(|e| matches!(&e.value, PackageBodyElement::PartDef(p) if p.value.identification.name.as_deref() == Some("Good"))),
"later package sibling should remain parseable"
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_part_name_in_part_body() {
let input = "package P {\npart def Vehicle {\npart: Wheel;\nattribute mass: MassValue;\n}\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"missing part name should produce diagnostics"
);
let err = result
.errors
.iter()
.find(|e| e.expected.as_deref() == Some("part name before ':'"))
.expect("expected part-name diagnostic");
assert!(
err.suggestion
.as_deref()
.is_some_and(|s| s.contains("part wheel: Wheel;")),
"diagnostic should show a part example fix"
);
}
#[test]
fn test_parse_with_diagnostics_reports_local_package_recovery() {
let input = "package P {\n#fmeaspec requirement req1 { }\npart def Good;\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"package-level recovery should surface as diagnostics"
);
let err = result
.errors
.iter()
.find(|e| e.code.as_deref() == Some("unsupported_annotation_syntax"))
.expect("expected local package recovery diagnostic");
assert_eq!(err.line, Some(2));
assert!(
err.found
.as_deref()
.is_some_and(|f| f.contains("#fmeaspec")),
"diagnostic should preserve recovered snippet"
);
assert!(
err.message.contains("annotation"),
"annotation recovery should say why the declaration could not be parsed: {}",
err.message
);
assert_eq!(
err.severity,
Some(sysml_v2_parser::DiagnosticSeverity::Warning)
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_semicolon_between_package_members() {
let input = "package P {\npart def A {\nexhibit state s : S\npart b : B;\n}\n}";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"missing semicolon should produce diagnostics"
);
let err = result
.errors
.iter()
.find(|e| e.code.as_deref() == Some("missing_semicolon"))
.expect("expected missing_semicolon diagnostic");
assert_eq!(err.expected.as_deref(), Some("';'"));
assert!(
err.suggestion
.as_deref()
.is_some_and(|s| s.contains("Insert ';'")),
"diagnostic should include a semicolon suggestion"
);
}
#[test]
fn test_parse_with_diagnostics_accepts_expose_feature_chain() {
let input = "package Views { view structure: GeneralView { expose SurveillanceDrone.SurveillanceQuadrotorDrone; } }";
let result = parse_with_diagnostics(input);
assert!(
result.is_ok(),
"expose feature chains should parse without separator diagnostics: {:?}",
result.errors
);
assert!(
!result
.errors
.iter()
.any(|e| e.code.as_deref() == Some("invalid_qualified_name_separator")),
"feature-chain expose should not emit invalid_qualified_name_separator: {:?}",
result.errors
);
}
#[test]
fn test_parse_with_diagnostics_reports_illegal_top_level_part_definition() {
let input = "part def TopLevel;";
let result = parse_with_diagnostics(input);
assert!(!result.is_ok(), "top-level part def should fail");
let err = &result.errors[0];
assert_eq!(err.code.as_deref(), Some("illegal_top_level_definition"));
assert!(
err.message.contains("illegal top-level"),
"message should describe illegal top-level declaration"
);
assert!(
err.suggestion
.as_deref()
.is_some_and(|s| s.contains("package") && s.contains("namespace")),
"diagnostic should suggest wrapping in package or namespace"
);
}
#[test]
fn test_parse_reports_missing_closing_brace_for_unterminated_package() {
let input = "package P {\npart def A;\n";
let err = parse(input).expect_err("unterminated package should fail");
assert_eq!(err.code.as_deref(), Some("missing_closing_brace"));
assert_eq!(err.expected.as_deref(), Some("'}'"));
assert!(
err.suggestion
.as_deref()
.is_some_and(|s| s.contains("Add '}'")),
"missing brace diagnostic should suggest how to close the body"
);
}
#[test]
fn test_parse_with_diagnostics_reports_missing_closing_brace_for_unterminated_package() {
let input = "package P {\npart def A;\n";
let result = parse_with_diagnostics(input);
assert!(
!result.is_ok(),
"unterminated package should produce diagnostics"
);
let err = result
.errors
.iter()
.find(|e| e.code.as_deref() == Some("missing_closing_brace"))
.expect("expected missing closing brace diagnostic");
assert_eq!(err.expected.as_deref(), Some("'}'"));
}
#[test]
fn test_parse_reports_illegal_top_level_part_definition() {
let input = "part def TopLevel;";
let err = parse(input).expect_err("top-level part def should fail");
assert_eq!(err.code.as_deref(), Some("illegal_top_level_definition"));
assert_eq!(
err.expected.as_deref(),
Some("'package', 'namespace', or 'import'")
);
}
#[test]
fn test_invalid_input_corpus_is_handled_gracefully() {
let invalid_inputs = [
"package P {",
"package P { part def A {",
"package P { @@@ ??? }",
"package P { /* unterminated",
"namespace N { part def X { ;;; }",
"part def TopLevel;",
];
for input in invalid_inputs {
let strict = std::panic::catch_unwind(|| {
let _ = parse(input).is_ok();
});
assert!(strict.is_ok(), "parse should not panic for {:?}", input);
let recovered = std::panic::catch_unwind(|| parse_with_diagnostics(input));
assert!(
recovered.is_ok(),
"parse_with_diagnostics should not panic for {:?}",
input
);
}
}