This commit is contained in:
deeaitch committed 2026-10-11 06:19:42 -04:00
1 parent bce707daf6
commit 51fcdb88ac
174 files changed
+5220 -2213

No files matched your search

+11
View File
@@ -54,6 +54,17 @@ found; insufficient payloads are checked through signal validity.
## Parser coverage
Structured diagnostic tests cover severity, code, supplied path, and physical
line numbers for skipped syntax and immediate/deferred errors; warnings preceding
an error are retained, and failed parses return an empty database. Common headers
and namespace symbol lists stay quiet. Tests also check file-open errors, fresh
parse sessions, and compatibility with the legacy throwing API.
The diagnostic audit also covers 15 unsupported-statement variants, all namespace
termination forms, CRLF and missing final newlines, relative paths with spaces,
first-error stopping, deferred diagnostic ordering, duplicate definitions/values,
selector ranges, overlaps, and preservation of supported decoding and metadata.
On Linux, reading a directory exercises the distinct `FileReadError` path.
Parser tests cover standard and extended CAN IDs, whitespace, payload storage
boundaries, missing fields, malformed and overflowing numbers, reserved ID bits,
error paths and line numbers, and integration with signals and comments.
+265
View File
@@ -63,6 +63,11 @@ private:
}
}
private Q_SLOTS:
void diagnostics();
void diagnosticErrors();
void diagnosticUnsupportedStatements();
void diagnosticNamespaceBoundaries();
void diagnosticFormatting();
void multiplexing();
void multiplexingBoundaries();
void validFrames();
@@ -79,6 +84,266 @@ private Q_SLOTS:
void invalidValueDescriptions();
};
void DbcParserTest::diagnostics() {
Fixture fixture;
fixture.Write("VERSION \"test\"\n"
"NS_ :\n"
" CM_\n"
" BA_\n"
" SIG_VALTYPE_\n"
" SG_MUL_VAL_\n\n"
"BS_:\n"
"BU_: ECU\n"
"BO_ 1 Frame: 1 ECU\n"
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
"SIG_VALTYPE_ 1 Value : 1;\n"
"BA_ \"SignalType\" SG_ 1 Value 1;\n"
"VENDOR_EXTENSION_ data;\n");
const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
CHECK(!result.HasErrors() && result.database.frames.size() == 1U,
"Warnings preserve the supported database");
QCOMPARE(result.diagnostics.size(), std::size_t(3U));
for (std::size_t index = 0U; index < result.diagnostics.size(); ++index) {
const DbcDiagnostic &diagnostic = result.diagnostics[index];
CHECK(diagnostic.severity == DbcDiagnosticSeverity::Warning &&
diagnostic.code == DbcDiagnosticCode::UnsupportedStatement,
"Unsupported statements have structured warning severity and code");
QCOMPARE(diagnostic.filePath, fixture.path.string());
QCOMPARE(diagnostic.lineNumber, 12U + index);
CHECK(diagnostic.message.find("ignored") != std::string::npos,
"Warning explains unsupported syntax");
}
CHECK(DbcParser().ParseFile(fixture.path.string()).frames.size() == 1U,
"Legacy API accepts files with warnings");
fixture.Write("VERSION \"\"\nNS_:\nSIG_VALTYPE_\n\tBA_\n BS_ :\n BU_ : ECU\n");
const DbcParseResult headers = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
CHECK(headers.diagnostics.empty(), "Common header variants stay quiet");
fixture.Write("NS_ :\n BA_\n BO_\n");
CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
"Namespace handling does not swallow a malformed frame");
}
void DbcParserTest::diagnosticErrors() {
Fixture fixture;
struct ErrorCase {
const char *text;
DbcDiagnosticCode code;
std::size_t line;
};
const ErrorCase cases[] = {
{"UNKNOWN_ data;\n\nBO_ invalid Frame: 1 ECU\n",
DbcDiagnosticCode::InvalidStatement, 3U},
{"BO_ 1 Frame: 1 ECU\nSG_MUL_VAL_ 1 Value Mode 1-2;\n",
DbcDiagnosticCode::UnsupportedStatement, 2U},
{"BO_ 1 Frame: 1 ECU\nVAL_ 1 Missing 0 \"Off\";\n",
DbcDiagnosticCode::InvalidValueDescription, 2U},
{"BO_ 1 Frame: 1 ECU\nSG_ Value m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
DbcDiagnosticCode::InvalidMultiplexing, 2U},
{"BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
DbcDiagnosticCode::InvalidMultiplexing, 2U},
{"VAL_ broken\n", DbcDiagnosticCode::InvalidValueDescription, 1U},
{"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
DbcDiagnosticCode::InvalidStatement, 1U},
{"BO_ 1 Frame: 1 ECU\nBO_ 1 Duplicate: 1 ECU\n",
DbcDiagnosticCode::InvalidStatement, 2U},
{"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
DbcDiagnosticCode::InvalidStatement, 3U},
{"BO_ 1 Frame: 1 ECU\nSG_ Value : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
DbcDiagnosticCode::InvalidStatement, 2U},
{"BO_ 1 Frame: 1 ECU\nVAL_ 2 Missing 0 \"Off\";\n",
DbcDiagnosticCode::InvalidValueDescription, 2U},
{"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
"VAL_ 1 Value 0 \"Off\";\nVAL_ 1 Value 0 \"Duplicate\";\n",
DbcDiagnosticCode::InvalidValueDescription, 4U},
{"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"
"SG_ Value m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
DbcDiagnosticCode::InvalidMultiplexing, 3U},
{"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
"SG_ Other M : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
DbcDiagnosticCode::InvalidMultiplexing, 3U},
{"BO_ 1 Frame: 1 ECU\nSG_ First : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
"SG_ Second : 7|8@0+ (1,0) [0|255] \"\" ECU\n",
DbcDiagnosticCode::InvalidMultiplexing, 3U}
};
for (const ErrorCase &test : cases) {
fixture.Write(test.text);
const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
CHECK(result.HasErrors() && result.database.frames.empty(),
"Failed parse never returns a partial database");
const DbcDiagnostic &diagnostic = result.diagnostics.back();
CHECK(diagnostic.severity == DbcDiagnosticSeverity::Error && diagnostic.code == test.code,
"Error has expected severity and code");
QCOMPARE(diagnostic.filePath, fixture.path.string());
QCOMPARE(diagnostic.lineNumber, test.line);
CHECK(!diagnostic.message.empty(), "Error includes a message");
const bool hasWarning = std::string(test.text).find("UNKNOWN_") == 0U;
QCOMPARE(result.diagnostics.size(), hasWarning ? std::size_t(2U) : std::size_t(1U));
try {
DbcParser().ParseFile(fixture.path.string());
CHECK(false, "Legacy parser must throw");
} catch (const std::runtime_error &error) {
QCOMPARE(std::string(error.what()), fixture.path.string() + ":" +
std::to_string(test.line) + ": " + diagnostic.message);
}
}
const std::string missingPath = fixture.path.string() + ".missing";
const DbcParseResult missing = DbcParser().ParseFileWithDiagnostics(missingPath);
CHECK(missing.HasErrors() && missing.database.frames.empty(), "Missing file returns error");
QCOMPARE(missing.diagnostics.size(), std::size_t(1U));
CHECK(missing.diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
"Missing file has a distinct code");
QCOMPARE(missing.diagnostics[0].lineNumber, std::size_t(0U));
QCOMPARE(missing.diagnostics[0].filePath, missingPath);
QCOMPARE(missing.diagnostics[0].message, "Failed to open DBC file: " + missingPath);
try {
DbcParser().ParseFile(missingPath);
CHECK(false, "Legacy API throws on missing file");
} catch (const std::runtime_error &error) {
QCOMPARE(std::string(error.what()), missing.diagnostics[0].message);
}
const std::string directoryPath = fixture.path.parent_path().string();
const DbcParseResult directory = DbcParser().ParseFileWithDiagnostics(directoryPath);
CHECK(directory.HasErrors() && directory.database.frames.empty(),
"Directory cannot be parsed as a file");
QCOMPARE(directory.diagnostics.size(), std::size_t(1U));
CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError ||
directory.diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
"Directory produces a file-level error on supported platforms");
QCOMPARE(directory.diagnostics[0].lineNumber, std::size_t(0U));
QCOMPARE(directory.diagnostics[0].filePath, directoryPath);
#ifdef __linux__
CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError,
"Linux directory input exercises the read-error path");
QCOMPARE(directory.diagnostics[0].message, "Failed to read DBC file: " + directoryPath);
#endif
try {
DbcParser().ParseFile(directoryPath);
CHECK(false, "Legacy API throws on unreadable input");
} catch (const std::runtime_error &error) {
QCOMPARE(std::string(error.what()), directory.diagnostics[0].message);
}
fixture.Write("BO_ 1 Frame: 1 ECU\n");
CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).diagnostics.empty(),
"Diagnostics belong to an independent parse session");
}
void DbcParserTest::diagnosticUnsupportedStatements() {
Fixture fixture;
const std::string definitions =
"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (2,10) [10|520] \"V\" ECU\n"
"CM_ SG_ 1 Value \"Signal comment\";\nVAL_ 1 Value 1 \"On\";\n";
const std::vector<std::string> statements = {
"SIG_VALTYPE_ 1 Value : 1;", "SIG_VALTYPE_ 1 Value : 2;",
"BA_ \"SignalType\" SG_ 1 Value 1;",
"BA_DEF_ SG_ \"SignalType\" INT 0 1;",
"BA_DEF_DEF_ \"SignalType\" 0;",
"BA_DEF_REL_ BU_SG_REL_ \"Relation\" INT 0 1;",
"BA_REL_ \"Relation\" BU_SG_REL_ ECU SG_ 1 Value 1;",
"SGTYPE_ Type : 8@1+ (1,0) [0|255] \"\" 0, Table;",
"SIG_TYPE_REF_ 1 Value : Type;",
"EV_ Environment : 0 [0|1] \"\" 0 1 DUMMY_NODE_VECTOR0 ECU;",
"ENVVAR_DATA_ Environment : 4;",
"VAL_TABLE_ Table 0 \"Off\" 1 \"On\";",
"BO_TX_BU_ 1 : ECU;", "VENDOR_EXTENSION_ data;", "UNKNOWN_"
};
for (const std::string &statement : statements) {
fixture.Write(definitions + statement + '\n');
const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
CHECK(!result.HasErrors(), statement);
QCOMPARE(result.diagnostics.size(), std::size_t(1U));
const DbcDiagnostic &warning = result.diagnostics[0];
CHECK(warning.severity == DbcDiagnosticSeverity::Warning &&
warning.code == DbcDiagnosticCode::UnsupportedStatement, statement);
QCOMPARE(warning.filePath, fixture.path.string());
QCOMPARE(warning.lineNumber, std::size_t(5U));
const std::string keyword = statement.substr(0U, statement.find(' '));
CHECK(warning.message.find("'" + keyword + "'") != std::string::npos, statement);
const DecodeDatabase runtime = DbcDecodeBuilder().Build(result.database);
RawCanFrame raw;
raw.canId = 1U;
raw.data = {1U};
const DecodedFrameValue decoded = DbcDecoder().Decode(runtime, raw);
CHECK(decoded.valid && decoded.signals.size() == 1U && decoded.signals[0].valid,
"Warnings preserve supported signal decoding");
QCOMPARE(decoded.signals[0].physicalValue, 12.0);
CHECK(decoded.signals[0].GetValueDescription() &&
*decoded.signals[0].GetValueDescription() == "On",
"Warnings preserve VAL_ descriptions");
QCOMPARE(runtime.frames[0].signals[0].comment, std::string("Signal comment"));
}
}
void DbcParserTest::diagnosticNamespaceBoundaries() {
Fixture fixture;
const std::vector<std::string> endings = {
"BS_\n", "BS_:\n", "BS_ :\n", "BU_\n", "BU_:\n", "BU_ : ECU\n",
"BO_ 1 Frame: 1 ECU\n", "VENDOR_EXTENSION_ data;\n"
};
for (const std::string &ending : endings) {
fixture.Write("NS_ :\n\tSG_MUL_VAL_\n\nBA_\n" + ending + "SIG_VALTYPE_\n");
const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
CHECK(!result.HasErrors(), ending);
const std::size_t expectedWarnings = ending.find("VENDOR_") == 0U ? 2U : 1U;
QCOMPARE(result.diagnostics.size(), expectedWarnings);
QCOMPARE(result.diagnostics.back().lineNumber, std::size_t(6U));
CHECK(result.diagnostics.back().message.find("SIG_VALTYPE_") != std::string::npos,
"Unsupported keyword outside NS_ must produce a warning");
}
for (const std::string &definition : {std::string("BO_"), std::string("SG_")}) {
fixture.Write("NS_:\n\tBA_\n\t" + definition + '\n');
const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
CHECK(result.HasErrors(), "Malformed definition ends NS_ instead of being hidden");
QCOMPARE(result.diagnostics.size(), std::size_t(1U));
QCOMPARE(result.diagnostics[0].lineNumber, std::size_t(3U));
}
}
void DbcParserTest::diagnosticFormatting() {
Fixture fixture;
fixture.path += " with spaces";
const DbcParser parser;
for (const std::string &text : {std::string(), std::string(" \t\r\n\n")}) {
fixture.Write(text);
const DbcParseResult empty = parser.ParseFileWithDiagnostics(fixture.path.string());
CHECK(!empty.HasErrors() && empty.diagnostics.empty() && empty.database.frames.empty(),
"Empty and whitespace-only inputs are valid");
}
fixture.Write("\r\n\tVENDOR_EXTENSION_ data;\r\n\r\nUNKNOWN_");
const std::string relativePath = std::filesystem::relative(fixture.path).string();
const DbcParseResult warnings = parser.ParseFileWithDiagnostics(relativePath);
CHECK(!warnings.HasErrors() && warnings.database.frames.empty(), "Warnings-only input");
QCOMPARE(warnings.diagnostics.size(), std::size_t(2U));
QCOMPARE(warnings.diagnostics[0].lineNumber, std::size_t(2U));
QCOMPARE(warnings.diagnostics[1].lineNumber, std::size_t(4U));
for (const DbcDiagnostic &warning : warnings.diagnostics)
QCOMPARE(warning.filePath, relativePath);
fixture.Write("UNKNOWN_ first;\nBO_ invalid Frame: 1 ECU\n"
"UNKNOWN_ after_error;\nSG_ broken\n");
const DbcParseResult stopped = parser.ParseFileWithDiagnostics(relativePath);
CHECK(stopped.HasErrors() && stopped.database.frames.empty(), "First error stops parsing");
QCOMPARE(stopped.diagnostics.size(), std::size_t(2U));
QCOMPARE(stopped.diagnostics[0].lineNumber, std::size_t(1U));
QCOMPARE(stopped.diagnostics[1].lineNumber, std::size_t(2U));
QCOMPARE(stopped.diagnostics[1].filePath, relativePath);
fixture.Write("VAL_ 1 Missing 0 \"Off\";\nBO_ 1 Frame: 1 ECU\nUNKNOWN_ data;\n");
const DbcParseResult deferred = parser.ParseFileWithDiagnostics(relativePath);
CHECK(deferred.HasErrors() && deferred.database.frames.empty(), "Deferred error rejects database");
QCOMPARE(deferred.diagnostics.size(), std::size_t(2U));
CHECK(deferred.diagnostics[0].severity == DbcDiagnosticSeverity::Warning &&
deferred.diagnostics[1].severity == DbcDiagnosticSeverity::Error,
"Deferred errors follow warnings in detection order");
QCOMPARE(deferred.diagnostics[0].lineNumber, std::size_t(3U));
QCOMPARE(deferred.diagnostics[1].lineNumber, std::size_t(1U));
QCOMPARE(deferred.diagnostics[1].filePath, relativePath);
fixture.Write("BO_ 1 Frame: 1 ECU");
const DbcParseResult recovered = parser.ParseFileWithDiagnostics(relativePath);
CHECK(!recovered.HasErrors() && recovered.diagnostics.empty() && recovered.database.frames.size() == 1U,
"Same parser has clean state after errors and accepts missing final newline");
}
void DbcParserTest::validFrames() {
Fixture fixture;
struct ValidCase {