#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
use a2l_parse::{A2lError, A2lProject};
fn parse(src: &str) -> Result<A2lProject, A2lError> {
A2lProject::parse(src)
}
fn wrap(body: &str) -> String {
format!("/begin PROJECT p \"d\"\n/begin MODULE m \"d\"\n{body}/end MODULE\n/end PROJECT\n")
}
#[test]
fn unterminated_comment() {
let err = parse("/* never closed /begin PROJECT p \"d\" /end PROJECT").unwrap_err();
assert_eq!(err, A2lError::UnterminatedComment { line: 1 });
assert_eq!(err.line(), Some(1));
assert_eq!(err.to_string(), "line 1: unterminated /* comment");
}
#[test]
fn unterminated_string() {
let err = parse("/begin PROJECT p \"oops").unwrap_err();
assert_eq!(err, A2lError::UnterminatedString { line: 1 });
assert_eq!(err.to_string(), "line 1: unterminated string");
let err = parse("/begin PROJECT p \"oops\\").unwrap_err();
assert_eq!(err, A2lError::UnterminatedString { line: 1 });
}
#[test]
fn invalid_numbers() {
let src =
wrap("/begin CHARACTERISTIC c \"d\" VALUE 0x10 RL 1.2.3 cm 0 1\n/end CHARACTERISTIC\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken { expected: "COMPU_METHOD reference", got, line: 3 } if got == "."
));
let src = wrap("/begin CHARACTERISTIC c \"d\" VALUE 0xFFFFFFFFFFFFFFFFFF RL 0 cm 0 1\n/end CHARACTERISTIC\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::InvalidNumber { ref text, line: 3 } if text == "0xFFFFFFFFFFFFFFFFFF"
));
let src = wrap("/begin CHARACTERISTIC c \"d\" VALUE 0x10 RL 1e+ cm 0 1\n/end CHARACTERISTIC\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::InvalidNumber { ref text, .. } if text == "1e+"
));
let src = wrap("/begin CHARACTERISTIC c \"d\" VALUE 0x RL 0 cm 0 1\n/end CHARACTERISTIC\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken { expected: "MAX_DIFF", got, .. } if got == "RL"
));
let src =
wrap("/begin CHARACTERISTIC c \"d\" VALUE 0x10 RL 1e5e5 cm 0 1\n/end CHARACTERISTIC\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::InvalidNumber { ref text, .. } if text == "1e5e5"
));
}
#[test]
fn unexpected_eof_contexts() {
let err = parse("/begin").unwrap_err();
assert_eq!(
err,
A2lError::UnexpectedEof {
context: "block keyword after /begin"
}
);
let src = "/begin PROJECT p \"d\"\n/begin MODULE m \"d\"\n/end\n";
let err = parse(src).unwrap_err();
assert_eq!(
err,
A2lError::UnexpectedEof {
context: "block keyword after /end"
}
);
let err = parse("/begin PROJECT").unwrap_err();
assert_eq!(
err,
A2lError::UnexpectedEof {
context: "PROJECT name"
}
);
let err = parse("/begin MODULE m \"d\" /end MODULE").unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken { expected: "/begin PROJECT", got, .. } if got == "MODULE"
));
let err = parse("/begin PROJECT /end PROJECT").unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken {
expected: "PROJECT name",
..
}
));
}
#[test]
fn unexpected_token_variants() {
let src = wrap("/begin CHARACTERISTIC c \"d\" 0x10 0x10 RL 0 cm 0 1\n/end CHARACTERISTIC\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken {
expected: "CHARACTERISTIC type",
..
}
));
let src = "/begin PROJECT p \"d\"\n/begin MODULE \"d\"\n/end MODULE\n/end PROJECT\n";
let err = parse(src).unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken {
expected: "MODULE name",
..
}
));
let src = wrap("/begin MEASUREMENT x \"d\"\n/end MEASUREMENT\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken {
expected: "DATATYPE keyword",
..
}
));
let src = wrap("/begin MEASUREMENT x \"d\" \"UWORD\" cm 0 0 0 100\n/end MEASUREMENT\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken { expected: "DATATYPE keyword", got, line: 3 } if got == "UWORD"
));
let src = wrap("/begin RECORD_LAYOUT \"name\"\n/end RECORD_LAYOUT\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken {
expected: "RECORD_LAYOUT name",
..
}
));
let src = wrap("/begin COMPU_METHOD c \"d\" \"RAT_FUNC\" \"f\" \"u\"\n/end COMPU_METHOD\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::UnexpectedToken {
expected: "COMPU_METHOD conversion type",
..
}
));
let err = parse("/begin PROJECT p \"d\"\n/end MODULE\n").unwrap_err();
assert!(matches!(
err,
A2lError::BlockMismatch { ref opened, ref closed, .. }
if opened == "PROJECT" && closed == "MODULE"
));
}
#[test]
fn unterminated_blocks_per_type() {
let err = parse("/begin PROJECT p \"d\"\n/begin MODULE m \"d\"\n").unwrap_err();
assert!(matches!(
err,
A2lError::UnterminatedBlock { ref block, .. } if block == "MODULE"
));
let err = parse("/begin PROJECT p \"d\"\n").unwrap_err();
assert!(matches!(
err,
A2lError::UnterminatedBlock { ref block, .. } if block == "PROJECT"
));
let truncated = [
(
"/begin PROJECT p \"d\"\n/begin MODULE m \"d\"\n/begin CHARACTERISTIC c \"d\" VALUE 0x10 RL 0 cm 0 1\n",
"CHARACTERISTIC",
),
(
"/begin PROJECT p \"d\"\n/begin MODULE m \"d\"\n/begin MEASUREMENT x \"d\" UWORD cm 0 0 0 100\n",
"MEASUREMENT",
),
(
"/begin PROJECT p \"d\"\n/begin MODULE m \"d\"\n/begin RECORD_LAYOUT rl\n",
"RECORD_LAYOUT",
),
(
"/begin PROJECT p \"d\"\n/begin MODULE m \"d\"\n/begin COMPU_METHOD cm \"d\" RAT_FUNC \"%.0f\" \"-\"\n",
"COMPU_METHOD",
),
(
"/begin PROJECT p \"d\"\n/begin MODULE m \"d\"\n/begin VENDOR \"x\"\n",
"VENDOR",
),
];
for (src, block) in truncated {
let err = parse(src).unwrap_err();
assert!(
matches!(&err, A2lError::UnterminatedBlock { block: b, .. } if b == block),
"expected UnterminatedBlock {block}: {err:?}"
);
}
let src = wrap("/begin CHARACTERISTIC c \"d\" VALUE 0x10 RL 0 cm 0 1\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::BlockMismatch { ref opened, ref closed, .. }
if opened == "CHARACTERISTIC" && closed == "MODULE"
));
}
#[test]
fn depth_limit_error_display() {
let err = A2lError::DepthLimitExceeded { line: 9 };
assert_eq!(
err.to_string(),
"line 9: block nesting exceeds the parser limit"
);
assert_eq!(err.line(), Some(9));
let err = A2lError::UnexpectedEof { context: "x" };
assert_eq!(err.line(), None);
assert_eq!(err.to_string(), "unexpected end of input: expected x");
let err = A2lError::MissingProject;
assert_eq!(err.line(), None);
assert_eq!(err.to_string(), "no `/begin PROJECT` block found");
let err = A2lError::InvalidAddress {
value: 5.5e10,
line: 2,
};
assert_eq!(
err.to_string(),
"line 2: value 55000000000 is not a 32-bit ECU address"
);
let err = A2lError::InvalidNumber {
text: "1,2".into(),
line: 4,
};
assert_eq!(err.to_string(), "line 4: invalid number `1,2`");
let err = A2lError::UnexpectedToken {
expected: "y".into(),
got: "z".into(),
line: 7,
};
assert_eq!(err.to_string(), "line 7: expected y, found `z`");
let err = A2lError::InvalidDatatype {
text: "X".into(),
line: 1,
};
assert_eq!(err.to_string(), "line 1: unknown ASAP2 datatype `X`");
let err = A2lError::InvalidCharType {
text: "Y".into(),
line: 1,
};
assert_eq!(err.to_string(), "line 1: unknown CHARACTERISTIC type `Y`");
let err = A2lError::InvalidConversionType {
text: "Z".into(),
line: 1,
};
assert_eq!(
err.to_string(),
"line 1: unknown COMPU_METHOD conversion type `Z`"
);
let err = A2lError::UnterminatedBlock {
block: "B".into(),
line: 3,
};
assert_eq!(
err.to_string(),
"line 3: `/begin B` without matching `/end B`"
);
let err = A2lError::BlockMismatch {
opened: "A".into(),
closed: "B".into(),
line: 5,
};
assert_eq!(err.to_string(), "line 5: `/end B` closes `/begin A`");
}
#[test]
fn lexer_string_escapes() {
let src = "/begin PROJECT p \"tab\\tand\\nnewline quote\\\" back\\\\ raw\\x\"\n\
/begin MODULE m \"d\" /end MODULE\n\
/end PROJECT\n";
let project = parse(src).unwrap();
assert_eq!(project.project, "p");
}
#[test]
fn lexer_number_forms() {
let src = wrap(
"/begin CHARACTERISTIC c \"d\" VALUE 0x10 RL 1 cm -0x10 1.5e-2 +3\n/end CHARACTERISTIC\n",
);
let src =
wrap("/begin CHARACTERISTIC c \"d\" VALUE 0x10 RL 1.5e-2 cm -16 +3\n/end CHARACTERISTIC\n");
let project = parse(&src).unwrap();
let c = &project.modules[0].characteristics[0];
assert_eq!(c.address, 0x10);
assert_eq!(c.max_diff, 0.015);
assert_eq!(c.lower_limit, -16.0);
assert_eq!(c.upper_limit, 3.0);
let src = wrap("- +\n");
let project = parse(&src).unwrap();
assert_eq!(project.modules[0].characteristics.len(), 0);
}
#[test]
fn compu_method_optional_unit_and_tab_ref_only() {
let src =
wrap("/begin COMPU_METHOD cm \"d\" RAT_FUNC \"-\" COEFFS 0 1 0 0 0 1\n/end COMPU_METHOD\n");
let project = parse(&src).unwrap();
let cm = &project.modules[0].compu_methods[0];
assert_eq!(cm.unit, "");
assert_eq!(
cm.coeffs,
a2l_parse::Coeffs::RatFunc([0.0, 1.0, 0.0, 0.0, 0.0, 1.0])
);
let src = wrap("/begin COMPU_METHOD cm \"d\" TABLE \"%.0f\" \"-\" COMPU_TAB_REF tabby\n/end COMPU_METHOD\n");
let project = parse(&src).unwrap();
let cm = &project.modules[0].compu_methods[0];
assert_eq!(cm.tab_ref.as_deref(), Some("tabby"));
let src = wrap(
"/begin COMPU_METHOD cm \"d\" LINEAR \"%.1f\" \"degC\" COEFFS_LINEAR 2 3 STATUS_PROMPT stable\n/end COMPU_METHOD\n",
);
let project = parse(&src).unwrap();
let cm = &project.modules[0].compu_methods[0];
assert_eq!(cm.coeffs, a2l_parse::Coeffs::Linear([2.0, 3.0]));
}
#[test]
fn ecu_address_without_number_is_ignored() {
let src = wrap("/begin MEASUREMENT x \"d\" UWORD cm 0 0 0 100 ECU_ADDRESS NOT_A_NUMBER\n/end MEASUREMENT\n");
let project = parse(&src).unwrap();
let m = &project.modules[0].measurements[0];
assert_eq!(m.address, 0);
}
#[test]
fn nested_unknown_block_inside_known_block() {
let src = wrap(
"/begin CHARACTERISTIC c \"d\" CURVE 0x10 RL 0 cm 0 1\n\
/begin AXIS_DESCR ax \"axis\" UWORD 0x20\n\
NO_AXIS_PTS 4\n\
/begin AXIS_POINTS \"predefined\" READ_ONLY\n\
junk here\n\
/end AXIS_POINTS\n\
/end AXIS_DESCR\n\
/end CHARACTERISTIC\n",
);
let project = parse(&src).unwrap();
let c = &project.modules[0].characteristics[0];
assert_eq!(c.r#type, a2l_parse::CharType::Curve);
assert_eq!(c.address, 0x10);
}
#[test]
fn fnc_values_variants() {
let src = wrap("/begin RECORD_LAYOUT rl\nFNC_VALUES 4 FLOAT64_IEEE COLUMN_DIR DIRECT\n/end RECORD_LAYOUT\n");
let project = parse(&src).unwrap();
let rl = &project.modules[0].record_layouts[0];
let fnc = rl.fnc_values.unwrap();
assert_eq!(fnc.position, 4);
assert_eq!(fnc.datatype, a2l_parse::DataType::Float64);
let src =
wrap("/begin RECORD_LAYOUT rl\nFNC_VALUES 1 UBIT128 COLUMN_DIR\n/end RECORD_LAYOUT\n");
let err = parse(&src).unwrap_err();
assert!(matches!(
err,
A2lError::InvalidDatatype { ref text, .. } if text == "UBIT128"
));
}
#[test]
fn trailing_content_after_project_is_tolerated() {
let src = "/begin PROJECT p \"d\"\n/end PROJECT\ntrailing junk 0x1 \"str\"\n";
let project = parse(src).unwrap();
assert_eq!(project.project, "p");
}
#[test]
fn multibyte_strings_and_escapes_never_panic() {
let src = "/begin PROJECT p \"backslash\\—dash\" \n/end PROJECT\n";
let project = parse(src).unwrap();
assert_eq!(project.project, "p");
let src = "/begin PROJECT prömje \"Berechnung—Temperatur °C\"\n\
/begin MODULE m \"Einheit µm\"\n\
/begin COMPU_METHOD cm \"d\" RAT_FUNC \"%.0f\" \"Nm²\" COEFFS 0 1 0 0 0 1\n\
/end COMPU_METHOD\n\
/end MODULE\n\
/end PROJECT\n";
let project = parse(src).unwrap();
let cm = project
.module_by_name("m")
.unwrap()
.compu_method_by_name("cm")
.unwrap();
assert_eq!(cm.unit, "Nm²");
let err = parse("/begin PROJECT p \"\\n\u{4e2d}").unwrap_err();
assert!(matches!(err, A2lError::UnterminatedString { .. }));
let src = "/begin PROJECT p \"d\"\n— → ✓ /begin MODULE m \"d\" /end MODULE\n/end PROJECT\n";
let project = parse(src).unwrap();
assert_eq!(project.modules.len(), 1);
}