4#include "dbc_statement_handler.h"
13#define CHECK(condition, name) QVERIFY2((condition), std::string(name).c_str())
17 std::filesystem::path path = std::filesystem::temp_directory_path() /
18 (
"dbc_parser_test_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) +
".dbc");
20 std::error_code error;
21 std::filesystem::remove(path, error);
23 void Write(
const std::string &text)
const {
24 std::ofstream output(path);
27 throw std::runtime_error(
"Cannot write test fixture.");
33 inline static int instances = 0;
34 inline static int calls = 0;
35 CountingHandler() { ++instances; }
36 void Handle(
const std::string &,
DbcParseContext &context)
const override {
38 context.database.
frames.emplace_back();
42std::string SignalLine(
const std::string &start,
const std::string &length,
43 const std::string &order,
const std::string &factor,
44 const std::string &offset,
const std::string &minimum,
45 const std::string &maximum,
const std::string &name =
"Value") {
46 return "SG_ " + name +
" : " + start +
"|" + length +
"@" + order +
47 " (" + factor +
"," + offset +
") [" + minimum +
"|" + maximum +
"] \"\" ECU";
50class DbcParserTest :
public QObject {
53 void RejectSignal(
const Fixture &fixture,
const std::string &line,
unsigned payload = 8U) {
54 fixture.Write(
"BO_ 1 Frame: " + std::to_string(payload) +
" ECU\n\n" + line +
"\n");
57 CHECK(
false,
"Reject " + line);
58 }
catch (
const std::runtime_error &error) {
59 const std::string prefix = fixture.path.string() +
":3: ";
60 CHECK(std::string(error.what()).find(prefix) == 0U &&
61 std::string(error.what()).size() > prefix.size(),
67 void diagnosticErrors();
68 void diagnosticUnsupportedStatements();
69 void diagnosticNamespaceBoundaries();
70 void diagnosticFormatting();
72 void multiplexingBoundaries();
75 void duplicateFrames();
76 void duplicateSignals();
77 void distinctDefinitions();
79 void invalidSignals();
82 void independentSessions();
83 void valueDescriptions();
84 void invalidValueDescriptions();
87void DbcParserTest::diagnostics() {
89 fixture.Write(
"VERSION \"test\"\n"
97 "BO_ 1 Frame: 1 ECU\n"
98 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
99 "SIG_VALTYPE_ 1 Value : 1;\n"
100 "BA_ \"SignalType\" SG_ 1 Value 1;\n"
101 "VENDOR_EXTENSION_ data;\n");
103 CHECK(!result.HasErrors() && result.database.
frames.size() == 1U,
104 "Warnings preserve the supported database");
105 QCOMPARE(result.diagnostics.size(), std::size_t(3U));
106 for (std::size_t index = 0U; index < result.diagnostics.size(); ++index) {
108 CHECK(diagnostic.severity == DbcDiagnosticSeverity::Warning &&
109 diagnostic.code == DbcDiagnosticCode::UnsupportedStatement,
110 "Unsupported statements have structured warning severity and code");
111 QCOMPARE(diagnostic.filePath, fixture.path.string());
112 QCOMPARE(diagnostic.lineNumber, 12U + index);
113 CHECK(diagnostic.message.find(
"ignored") != std::string::npos,
114 "Warning explains unsupported syntax");
116 CHECK(
DbcParser().ParseFile(fixture.path.string()).frames.size() == 1U,
117 "Legacy API accepts files with warnings");
118 fixture.Write(
"VERSION \"\"\nNS_:\nSIG_VALTYPE_\n\tBA_\n BS_ :\n BU_ : ECU\n");
120 CHECK(headers.diagnostics.empty(),
"Common header variants stay quiet");
121 fixture.Write(
"NS_ :\n BA_\n BO_\n");
122 CHECK(
DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
123 "Namespace handling does not swallow a malformed frame");
126void DbcParserTest::diagnosticErrors() {
130 DbcDiagnosticCode code;
133 const ErrorCase cases[] = {
134 {
"UNKNOWN_ data;\n\nBO_ invalid Frame: 1 ECU\n",
135 DbcDiagnosticCode::InvalidStatement, 3U},
136 {
"BO_ 1 Frame: 1 ECU\nSG_MUL_VAL_ 1 Value Mode 1-2;\n",
137 DbcDiagnosticCode::UnsupportedStatement, 2U},
138 {
"BO_ 1 Frame: 1 ECU\nVAL_ 1 Missing 0 \"Off\";\n",
139 DbcDiagnosticCode::InvalidValueDescription, 2U},
140 {
"BO_ 1 Frame: 1 ECU\nSG_ Value m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
141 DbcDiagnosticCode::InvalidMultiplexing, 2U},
142 {
"BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
143 DbcDiagnosticCode::InvalidMultiplexing, 2U},
144 {
"VAL_ broken\n", DbcDiagnosticCode::InvalidValueDescription, 1U},
145 {
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
146 DbcDiagnosticCode::InvalidStatement, 1U},
147 {
"BO_ 1 Frame: 1 ECU\nBO_ 1 Duplicate: 1 ECU\n",
148 DbcDiagnosticCode::InvalidStatement, 2U},
149 {
"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
150 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
151 DbcDiagnosticCode::InvalidStatement, 3U},
152 {
"BO_ 1 Frame: 1 ECU\nSG_ Value : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
153 DbcDiagnosticCode::InvalidStatement, 2U},
154 {
"BO_ 1 Frame: 1 ECU\nVAL_ 2 Missing 0 \"Off\";\n",
155 DbcDiagnosticCode::InvalidValueDescription, 2U},
156 {
"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
157 "VAL_ 1 Value 0 \"Off\";\nVAL_ 1 Value 0 \"Duplicate\";\n",
158 DbcDiagnosticCode::InvalidValueDescription, 4U},
159 {
"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"
160 "SG_ Value m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
161 DbcDiagnosticCode::InvalidMultiplexing, 3U},
162 {
"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
163 "SG_ Other M : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
164 DbcDiagnosticCode::InvalidMultiplexing, 3U},
165 {
"BO_ 1 Frame: 1 ECU\nSG_ First : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
166 "SG_ Second : 7|8@0+ (1,0) [0|255] \"\" ECU\n",
167 DbcDiagnosticCode::InvalidMultiplexing, 3U}
169 for (
const ErrorCase &test : cases) {
170 fixture.Write(test.text);
172 CHECK(result.HasErrors() && result.database.
frames.empty(),
173 "Failed parse never returns a partial database");
175 CHECK(diagnostic.severity == DbcDiagnosticSeverity::Error && diagnostic.code == test.code,
176 "Error has expected severity and code");
177 QCOMPARE(diagnostic.filePath, fixture.path.string());
178 QCOMPARE(diagnostic.lineNumber, test.line);
179 CHECK(!diagnostic.message.empty(),
"Error includes a message");
180 const bool hasWarning = std::string(test.text).find(
"UNKNOWN_") == 0U;
181 QCOMPARE(result.diagnostics.size(), hasWarning ? std::size_t(2U) : std::size_t(1U));
184 CHECK(
false,
"Legacy parser must throw");
185 }
catch (
const std::runtime_error &error) {
186 QCOMPARE(std::string(error.what()), fixture.path.string() +
":" +
187 std::to_string(test.line) +
": " + diagnostic.message);
190 const std::string missingPath = fixture.path.string() +
".missing";
192 CHECK(missing.HasErrors() && missing.database.
frames.empty(),
"Missing file returns error");
193 QCOMPARE(missing.diagnostics.size(), std::size_t(1U));
194 CHECK(missing.diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
195 "Missing file has a distinct code");
196 QCOMPARE(missing.diagnostics[0].lineNumber, std::size_t(0U));
197 QCOMPARE(missing.diagnostics[0].filePath, missingPath);
198 QCOMPARE(missing.diagnostics[0].message,
"Failed to open DBC file: " + missingPath);
201 CHECK(
false,
"Legacy API throws on missing file");
202 }
catch (
const std::runtime_error &error) {
203 QCOMPARE(std::string(error.what()), missing.diagnostics[0].message);
205 const std::string directoryPath = fixture.path.parent_path().string();
207 CHECK(directory.HasErrors() && directory.database.
frames.empty(),
208 "Directory cannot be parsed as a file");
209 QCOMPARE(directory.diagnostics.size(), std::size_t(1U));
210 CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError ||
211 directory.diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
212 "Directory produces a file-level error on supported platforms");
213 QCOMPARE(directory.diagnostics[0].lineNumber, std::size_t(0U));
214 QCOMPARE(directory.diagnostics[0].filePath, directoryPath);
216 CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError,
217 "Linux directory input exercises the read-error path");
218 QCOMPARE(directory.diagnostics[0].message,
"Failed to read DBC file: " + directoryPath);
222 CHECK(
false,
"Legacy API throws on unreadable input");
223 }
catch (
const std::runtime_error &error) {
224 QCOMPARE(std::string(error.what()), directory.diagnostics[0].message);
226 fixture.Write(
"BO_ 1 Frame: 1 ECU\n");
227 CHECK(
DbcParser().ParseFileWithDiagnostics(fixture.path.string()).diagnostics.empty(),
228 "Diagnostics belong to an independent parse session");
231void DbcParserTest::diagnosticUnsupportedStatements() {
233 const std::string definitions =
234 "BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (2,10) [10|520] \"V\" ECU\n"
235 "CM_ SG_ 1 Value \"Signal comment\";\nVAL_ 1 Value 1 \"On\";\n";
236 const std::vector<std::string> statements = {
237 "SIG_VALTYPE_ 1 Value : 1;",
"SIG_VALTYPE_ 1 Value : 2;",
238 "BA_ \"SignalType\" SG_ 1 Value 1;",
239 "BA_DEF_ SG_ \"SignalType\" INT 0 1;",
240 "BA_DEF_DEF_ \"SignalType\" 0;",
241 "BA_DEF_REL_ BU_SG_REL_ \"Relation\" INT 0 1;",
242 "BA_REL_ \"Relation\" BU_SG_REL_ ECU SG_ 1 Value 1;",
243 "SGTYPE_ Type : 8@1+ (1,0) [0|255] \"\" 0, Table;",
244 "SIG_TYPE_REF_ 1 Value : Type;",
245 "EV_ Environment : 0 [0|1] \"\" 0 1 DUMMY_NODE_VECTOR0 ECU;",
246 "ENVVAR_DATA_ Environment : 4;",
247 "VAL_TABLE_ Table 0 \"Off\" 1 \"On\";",
248 "BO_TX_BU_ 1 : ECU;",
"VENDOR_EXTENSION_ data;",
"UNKNOWN_"
250 for (
const std::string &statement : statements) {
251 fixture.Write(definitions + statement +
'\n');
253 CHECK(!result.HasErrors(), statement);
254 QCOMPARE(result.diagnostics.size(), std::size_t(1U));
256 CHECK(warning.severity == DbcDiagnosticSeverity::Warning &&
257 warning.code == DbcDiagnosticCode::UnsupportedStatement, statement);
258 QCOMPARE(warning.filePath, fixture.path.string());
259 QCOMPARE(warning.lineNumber, std::size_t(5U));
260 const std::string keyword = statement.substr(0U, statement.find(
' '));
261 CHECK(warning.message.find(
"'" + keyword +
"'") != std::string::npos, statement);
267 CHECK(decoded.
valid && decoded.signals.size() == 1U && decoded.signals[0].
valid,
268 "Warnings preserve supported signal decoding");
269 QCOMPARE(decoded.signals[0].physicalValue, 12.0);
270 CHECK(decoded.signals[0].GetValueDescription() &&
271 *decoded.signals[0].GetValueDescription() ==
"On",
272 "Warnings preserve VAL_ descriptions");
273 QCOMPARE(runtime.
GetFrames()[0].signals[0].comment, std::string(
"Signal comment"));
277void DbcParserTest::diagnosticNamespaceBoundaries() {
279 const std::vector<std::string> endings = {
280 "BS_\n",
"BS_:\n",
"BS_ :\n",
"BU_\n",
"BU_:\n",
"BU_ : ECU\n",
281 "BO_ 1 Frame: 1 ECU\n",
"VENDOR_EXTENSION_ data;\n"
283 for (
const std::string &ending : endings) {
284 fixture.Write(
"NS_ :\n\tSG_MUL_VAL_\n\nBA_\n" + ending +
"SIG_VALTYPE_\n");
286 CHECK(!result.HasErrors(), ending);
287 const std::size_t expectedWarnings = ending.find(
"VENDOR_") == 0U ? 2U : 1U;
288 QCOMPARE(result.diagnostics.size(), expectedWarnings);
289 QCOMPARE(result.diagnostics.back().lineNumber, std::size_t(6U));
290 CHECK(result.diagnostics.back().message.find(
"SIG_VALTYPE_") != std::string::npos,
291 "Unsupported keyword outside NS_ must produce a warning");
293 for (
const std::string &definition : {std::string(
"BO_"), std::string(
"SG_")}) {
294 fixture.Write(
"NS_:\n\tBA_\n\t" + definition +
'\n');
296 CHECK(result.HasErrors(),
"Malformed definition ends NS_ instead of being hidden");
297 QCOMPARE(result.diagnostics.size(), std::size_t(1U));
298 QCOMPARE(result.diagnostics[0].lineNumber, std::size_t(3U));
302void DbcParserTest::diagnosticFormatting() {
304 fixture.path +=
" with spaces";
306 for (
const std::string &text : {std::string(), std::string(
" \t\r\n\n")}) {
309 CHECK(!empty.HasErrors() && empty.diagnostics.empty() && empty.database.
frames.empty(),
310 "Empty and whitespace-only inputs are valid");
312 fixture.Write(
"\r\n\tVENDOR_EXTENSION_ data;\r\n\r\nUNKNOWN_");
313 const std::string relativePath = std::filesystem::relative(fixture.path).string();
315 CHECK(!warnings.HasErrors() && warnings.database.
frames.empty(),
"Warnings-only input");
316 QCOMPARE(warnings.diagnostics.size(), std::size_t(2U));
317 QCOMPARE(warnings.diagnostics[0].lineNumber, std::size_t(2U));
318 QCOMPARE(warnings.diagnostics[1].lineNumber, std::size_t(4U));
320 QCOMPARE(warning.filePath, relativePath);
322 fixture.Write(
"UNKNOWN_ first;\nBO_ invalid Frame: 1 ECU\n"
323 "UNKNOWN_ after_error;\nSG_ broken\n");
325 CHECK(stopped.HasErrors() && stopped.database.
frames.empty(),
"First error stops parsing");
326 QCOMPARE(stopped.diagnostics.size(), std::size_t(2U));
327 QCOMPARE(stopped.diagnostics[0].lineNumber, std::size_t(1U));
328 QCOMPARE(stopped.diagnostics[1].lineNumber, std::size_t(2U));
329 QCOMPARE(stopped.diagnostics[1].filePath, relativePath);
331 fixture.Write(
"VAL_ 1 Missing 0 \"Off\";\nBO_ 1 Frame: 1 ECU\nUNKNOWN_ data;\n");
333 CHECK(deferred.HasErrors() && deferred.database.
frames.empty(),
"Deferred error rejects database");
334 QCOMPARE(deferred.diagnostics.size(), std::size_t(2U));
335 CHECK(deferred.diagnostics[0].severity == DbcDiagnosticSeverity::Warning &&
336 deferred.diagnostics[1].severity == DbcDiagnosticSeverity::Error,
337 "Deferred errors follow warnings in detection order");
338 QCOMPARE(deferred.diagnostics[0].lineNumber, std::size_t(3U));
339 QCOMPARE(deferred.diagnostics[1].lineNumber, std::size_t(1U));
340 QCOMPARE(deferred.diagnostics[1].filePath, relativePath);
341 fixture.Write(
"BO_ 1 Frame: 1 ECU");
343 CHECK(!recovered.HasErrors() && recovered.diagnostics.empty() && recovered.database.
frames.size() == 1U,
344 "Same parser has clean state after errors and accepts missing final newline");
347void DbcParserTest::validFrames() {
355 const ValidCase valid[] = {
356 {
"BO_ 0 Frame: 0 ECU", 0U,
false, 0U},
357 {
"BO_ 2047 Frame: 8 ECU", 2047U,
false, 8U},
358 {
"BO_ 2048 Frame: 64 ECU", 2048U,
true, 64U},
359 {
"BO_ 536870911 Frame: 255 ECU", 0x1FFFFFFFU,
true, 255U},
360 {
"BO_ 2147483649 Frame: 8 ECU", 1U,
true, 8U},
361 {
"BO_ 2684354559 Frame: 8 ECU", 0x1FFFFFFFU,
true, 8U},
362 {
" BO_\t256\tFrame :\t8\tECU \r", 256U,
false, 8U}};
363 for (
const ValidCase &test: valid) {
364 fixture.Write(std::string(test.line) +
'\n');
367 QCOMPARE(database.
frames.size(), std::size_t(1U));
369 QCOMPARE(frame.
name, std::string(
"Frame"));
370 QCOMPARE(frame.
canId, test.id);
372 QCOMPARE(
static_cast<unsigned>(frame.
dlc), test.length);
374 }
catch (
const std::exception &error) { CHECK(
false, error.what()); }
378void DbcParserTest::invalidFrames() {
380 const char *invalid[] = {
381 "BO_ invalid",
"BO_",
"BO_ 1",
"BO_ 1 : 8 ECU",
"BO_ 1 Frame 8 ECU",
382 "BO_ 1 Frame:",
"BO_ 1 Frame: 8",
"BO_ 1 Frame: 8 ECU extra",
383 "BO_ 1 Two Names: 8 ECU",
"BO_ -1 Frame: 8 ECU",
"BO_ +1 Frame: 8 ECU",
384 "BO_ 1x Frame: 8 ECU",
"BO_ 0x123 Frame: 8 ECU",
"BO_ 1.5 Frame: 8 ECU",
385 "BO_ 4294967296 Frame: 8 ECU",
"BO_ 999999999999999999999 Frame: 8 ECU",
386 "BO_ 536870912 Frame: 8 ECU",
"BO_ 1073741824 Frame: 8 ECU",
387 "BO_ 2684354560 Frame: 8 ECU",
"BO_ 4294967295 Frame: 8 ECU",
388 "BO_ 1 Frame: -1 ECU",
"BO_ 1 Frame: +8 ECU",
"BO_ 1 Frame: 8x ECU",
389 "BO_ 1 Frame: 8.5 ECU",
"BO_ 1 Frame: 256 ECU",
390 "BO_ 1 Frame: 4294967296 ECU",
"BO_\tinvalid"};
391 for (
const char *line: invalid) {
392 fixture.Write(std::string(
"VERSION \"test\"\n\n") + line +
'\n');
396 }
catch (
const std::runtime_error &error) {
397 const std::string prefix = fixture.path.string() +
":3: ";
398 CHECK(std::string(error.what()).find(prefix) == 0U &&
399 std::string(error.what()).size() > prefix.size(),
405void DbcParserTest::duplicateFrames() {
407 struct DuplicateCase {
409 const char *duplicateId;
412 const DuplicateCase cases[] = {
413 {
"1",
"1",
"Duplicate standard frame CAN ID: 1"},
414 {
"2147483649",
"2147483649",
"Duplicate extended frame CAN ID: 1"},
415 {
"2048",
"2147485696",
"Duplicate extended frame CAN ID: 2048"},
416 {
"2147485696",
"2048",
"Duplicate extended frame CAN ID: 2048"},
417 {
"536870911",
"2684354559",
"Duplicate extended frame CAN ID: 536870911"}
420 for (
const DuplicateCase &test : cases) {
421 fixture.Write(
"BO_ " + std::string(test.firstId) +
" First: 8 ECU\n"
422 "BO_ 2 Other: 8 ECU\n\n"
423 " BO_\t" + test.duplicateId +
"\tSecond: 8 ECU\n");
426 CHECK(
false,
"Duplicate frame must be rejected");
427 }
catch (
const std::runtime_error &error) {
428 QCOMPARE(std::string(error.what()), fixture.path.string() +
":4: " + test.message);
431 fixture.Write(
"BO_ 1 First: 8 ECU\n");
432 QCOMPARE(parser.
ParseFile(fixture.path.string()).
frames.size(), std::size_t(1U));
435void DbcParserTest::duplicateSignals() {
437 const std::string first =
"SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n";
438 const std::string duplicates[] = {
440 " SG_\tState : 16|8@1+ (2,1) [0|511] \"V\" ECU\n",
441 "SG_ State m1 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
443 for (
const std::string &duplicate : duplicates) {
444 fixture.Write(
"BO_ 1 Frame: 8 ECU\n" + first +
445 "SG_ Other : 8|8@1+ (1,0) [0|255] \"\" ECU\n\n" + duplicate);
448 CHECK(
false,
"Duplicate signal must be rejected");
449 }
catch (
const std::runtime_error &error) {
450 QCOMPARE(std::string(error.what()), fixture.path.string() +
451 ":5: Duplicate signal name 'State' in frame 'Frame'.");
456void DbcParserTest::distinctDefinitions() {
458 fixture.Write(
"BO_ 1 Shared: 1 ECU\n"
459 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
460 "BO_ 2147483649 Shared: 1 ECU\n"
461 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
462 "BO_ 2 Shared: 2 ECU\n"
463 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
464 "SG_ state : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
465 "CM_ BO_ 1 \"Standard\";\n"
466 "CM_ BO_ 2147483649 \"Extended\";\n"
467 "CM_ SG_ 1 State \"Standard state\";\n"
468 "CM_ SG_ 2147483649 State \"Extended state\";\n");
470 QCOMPARE(parsed.
frames.size(), std::size_t(3U));
471 QCOMPARE(parsed.
frames[0].canId, parsed.
frames[1].canId);
472 CHECK(!parsed.
frames[0].isExtended && parsed.
frames[1].isExtended,
473 "Standard and extended frames with the same numeric ID remain distinct");
474 QCOMPARE(parsed.
frames[0].comment, std::string(
"Standard"));
475 QCOMPARE(parsed.
frames[1].comment, std::string(
"Extended"));
476 QCOMPARE(parsed.
frames[0].signals[0].comment, std::string(
"Standard state"));
477 QCOMPARE(parsed.
frames[1].signals[0].comment, std::string(
"Extended state"));
478 QCOMPARE(parsed.
frames[2].signals.size(), std::size_t(2U));
484 QVERIFY(standard.
valid);
485 QCOMPARE(standard.
definition->comment, std::string(
"Standard"));
488 QVERIFY(extended.
valid);
489 QCOMPARE(extended.
definition->comment, std::string(
"Extended"));
492void DbcParserTest::integration() {
495 "BO_TX_BU_ 256 : ECU;\nBO_ 256 Frame: 8 ECU\n"
496 " SG_ Speed : 0|16@1+ (0.5,1) [0|100] \"rpm\" ECU\n"
497 "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Speed \"Signal comment\";\n"
498 "BO_ 257 Second: 8 ECU\n");
500 CHECK(database.
frames.size() == 2U && database.
frames[0].signals.size() == 1U &&
501 database.
frames[0].comment ==
"Frame comment" &&
502 database.
frames[0].signals[0].comment ==
"Signal comment",
504 fixture.Write(
"\nSG_ Speed : invalid\n");
507 CHECK(
false,
"Signal error location");
508 }
catch (
const std::runtime_error &error) {
509 CHECK(std::string(error.what()).find(fixture.path.string() +
":2: ") == 0U,
510 "Signal error location");
514void DbcParserTest::multiplexing() {
516 const std::string selector =
"SG_ Mode M : 0|8@1+ (10,3) [0|255] \"\" ECU\n";
517 const std::string first =
"SG_ First m1 : 8|8@1- (2,0) [-128|127] \"V\" ECU\n";
518 const std::string second =
"SG_ Second m2 : 15|8@0+ (1,0) [0|255] \"\" ECU\n";
519 const std::string common =
"SG_ Common : 16|8@1+ (1,0) [0|255] \"\" ECU\n";
520 fixture.Write(
"BO_ 1 Frame: 3 ECU\n" + first + second + common + selector +
521 "VAL_ 1 First -1 \"Negative\";\n");
524 CHECK(parsed.
frames[0].signals[3].multiplexing.role == MultiplexRole::Selector,
525 "Selector declared after branches");
527 CHECK(tree->GetChild(0U)->GetChild(0U)->GetSignal()->multiplexing.value == 1U,
528 "Tree preserves multiplexing");
531 raw.
data = {1U, 255U, 7U};
533 CHECK(result.signals[0].
valid && std::get<std::int64_t>(result.signals[0].rawValue) == -1 &&
534 result.signals[0].physicalValue == -2 && *result.signals[0].GetValueDescription() ==
"Negative",
535 "Active signed branch and VAL_ use exact raw selector");
536 CHECK(!result.signals[1].
valid && result.signals[1].
status == SignalDecodeStatus::Inactive &&
537 result.signals[1].GetValueDescription() ==
nullptr && result.signals[2].
valid,
538 "Inactive branch and unconditional signal");
541 CHECK(result.signals[0].
status == SignalDecodeStatus::Inactive && result.signals[1].
valid &&
542 std::get<std::uint64_t>(result.signals[1].rawValue) == 255U,
543 "Motorola branch becomes active");
546 CHECK(result.signals[0].
status == SignalDecodeStatus::Inactive &&
547 result.signals[1].
status == SignalDecodeStatus::Inactive &&
548 result.signals[2].
status == SignalDecodeStatus::InsufficientPayload,
549 "Unmapped selector leaves branches inactive even with missing bytes");
552 CHECK(result.signals[0].
status == SignalDecodeStatus::InsufficientPayload,
553 "Active branch requires payload");
556 CHECK(result.signals[0].
status == SignalDecodeStatus::SelectorUnavailable &&
557 result.signals[3].
status == SignalDecodeStatus::InsufficientPayload,
558 "Unavailable selector does not imply inactive branch");
560 const std::vector<std::string> invalid = {
562 selector +
"SG_ Other M : 16|8@1+ (1,0) [0|255] \"\" ECU\n",
563 "SG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
564 selector +
"SG_ TooLarge m256 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
565 selector + first +
"SG_ Same m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n",
566 selector +
"SG_ Collision m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
567 selector + first +
"SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
568 "SG_MUL_VAL_ 1 First Mode 1-2;\n"
570 for (
const std::string &signals : invalid) {
571 fixture.Write(
"BO_ 1 Frame: 3 ECU\n" + signals);
572 bool rejected =
false;
575 }
catch (
const std::runtime_error &error) {
576 rejected = std::string(error.what()).find(fixture.path.string() +
":") == 0U;
578 CHECK(rejected,
"Invalid multiplexing rejected with location");
580 for (
const std::string marker : {
"m",
"m-1",
"m+1",
"m1M",
"m18446744073709551616",
"M m1"})
581 RejectSignal(fixture,
"SG_ Bad " + marker +
" : 8|8@1+ (1,0) [0|255] \"\" ECU");
584void DbcParserTest::multiplexingBoundaries() {
586 fixture.Write(
"BO_ 1 Frame: 9 ECU\n"
587 "SG_ Branch m18446744073709551615 : 64|8@1+ (1,0) [0|255] \"\" ECU\n"
588 "SG_ Mode M : 7|64@0+ (0,5) [0|1] \"\" ECU\n");
591 CHECK(runtime.
GetFrames()[0].signals[0].multiplexing.value == std::numeric_limits<std::uint64_t>::max(),
592 "Exact uint64 branch metadata");
595 raw.
data = std::vector<std::uint8_t>(9U, 255U);
597 CHECK(result.signals[0].
valid && result.signals[1].
valid &&
598 std::get<std::uint64_t>(result.signals[1].rawValue) == std::numeric_limits<std::uint64_t>::max(),
599 "Motorola uint64 selector compares raw value before zero scaling");
602 CHECK(result.signals[0].
status == SignalDecodeStatus::Inactive,
603 "Adjacent large integers remain distinct");
604 std::vector<DecodeFrame> definitions = runtime.
GetFrames();
605 definitions[0].signals[1].length = 0U;
608 CHECK(result.signals[0].
status == SignalDecodeStatus::InvalidDefinition &&
609 result.signals[1].
status == SignalDecodeStatus::InvalidDefinition,
610 "Malformed runtime selector distinguished from payload errors");
611 definitions[0].signals.pop_back();
614 CHECK(result.signals[0].
status == SignalDecodeStatus::InvalidDefinition,
615 "Missing runtime selector");
616 fixture.Write(
"BO_ 1 Frame: 2 ECU\n\n"
617 "SG_ Branch m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
618 "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n");
619 bool rejected =
false;
622 }
catch (
const std::runtime_error &error) {
623 rejected = std::string(error.what()).find(fixture.path.string() +
":3: ") == 0U;
625 CHECK(rejected,
"Deferred validation reports branch source line");
626 fixture.Write(
"BO_ 1 Frame: 2 ECU\n"
627 "SG_ A : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
628 "SG_ B : 15|8@0+ (1,0) [0|255] \"\" ECU\n");
632 }
catch (
const std::runtime_error &) {
635 CHECK(rejected,
"Ordinary overlapping signals rejected");
638void DbcParserTest::invalidSignals() {
640 const std::string ordinary = SignalLine(
"0",
"8",
"1+",
"1",
"0",
"0",
"255");
641 for (
const std::string value: {
"-1",
"+1",
"0junk",
"0x10",
"1.5",
642 "4294967296",
"999999999999999999999"}) {
643 RejectSignal(fixture, SignalLine(value,
"8",
"1+",
"1",
"0",
"0",
"255"));
644 RejectSignal(fixture, SignalLine(
"0", value,
"1+",
"1",
"0",
"0",
"255"));
646 for (
const std::string length: {
"0",
"65"})
647 RejectSignal(fixture, SignalLine(
"0", length,
"1+",
"1",
"0",
"0",
"255"));
648 for (
const std::string order: {
"2+",
"9-",
"1",
"1x",
"1++",
"01+"})
649 RejectSignal(fixture, SignalLine(
"0",
"8", order,
"1",
"0",
"0",
"255"));
650 for (
const std::string value: {
"1junk",
"nan",
"NaN",
"inf",
"-inf",
"1e999",
"1e-999",
"0x1p2",
"",
"1e",
"1.2.3"}) {
651 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+", value,
"0",
"0",
"255"));
652 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1", value,
"0",
"255"));
653 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1",
"0", value,
"255"));
654 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1",
"0",
"0", value));
656 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1",
"0",
"2",
"1"));
657 for (
const std::string name: {
"",
"Two Names",
"Value m1M"}) {
658 std::string line = ordinary;
659 line.replace(4U, 5U, name);
660 RejectSignal(fixture, line);
662 for (
const std::string broken: {
663 "SG_",
"SG_ Value",
"SG_ Value : 0|8@1+ (1,0) [0|255] ECU",
664 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\"",
665 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU,",
666 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU extra"})
667 RejectSignal(fixture, broken);
668 RejectSignal(fixture, SignalLine(
"64",
"1",
"1+",
"1",
"0",
"0",
"1"));
669 RejectSignal(fixture, SignalLine(
"63",
"2",
"1+",
"1",
"0",
"0",
"3"));
670 RejectSignal(fixture, SignalLine(
"56",
"2",
"0+",
"1",
"0",
"0",
"3"));
671 RejectSignal(fixture, SignalLine(
"4294967295",
"64",
"1+",
"1",
"0",
"0",
"1"));
672 RejectSignal(fixture, ordinary, 0U);
673 for (
const char delimiter: {
'|',
'@',
'(',
')',
'[',
']',
'"'}) {
674 std::string line = ordinary;
675 line.erase(line.find(delimiter), 1U);
676 RejectSignal(fixture, line);
680void DbcParserTest::validSignals() {
683 fixture.Write(
"BO_ 1 Frame: 8 ECU\n" +
684 SignalLine(
"0",
"8",
"1+",
"0",
"0",
"0",
"0") +
"\n" +
685 SignalLine(
"8",
"8",
"1-",
"-1",
"0",
"-255",
"0",
"Negative") +
"\n");
687 CHECK(factors.
frames[0].signals.size() == 2U &&
688 factors.
frames[0].signals[0].factor == 0.0 &&
689 factors.
frames[0].signals[1].factor == -1.0,
690 "Zero and negative factors and equal range boundaries");
698 const SignalCase validSignals[] = {
699 {
"0",
"1",
"1+", 1U},
700 {
"7",
"1",
"0-", 1U},
701 {
"0",
"64",
"1+", 8U},
702 {
"7",
"64",
"0-", 8U},
703 {
"63",
"1",
"1+", 8U},
704 {
"56",
"1",
"0+", 8U},
705 {
"3",
"12",
"0+", 2U},
706 {
"2039",
"1",
"1+", 255U}};
707 for (
const SignalCase &test: validSignals) {
708 fixture.Write(
"BO_ 1 Frame: " + std::to_string(test.payload) +
" ECU\n" +
709 SignalLine(test.start, test.length, test.order,
".5e+1",
"-2.5E-1",
"-1.",
"+2e2") +
"\n");
713 QCOMPARE(signal.
startBit,
static_cast<std::uint32_t
>(std::stoul(test.start)));
714 QCOMPARE(signal.
length,
static_cast<std::uint32_t
>(std::stoul(test.length)));
716 QCOMPARE(signal.
isSigned, test.order[1] ==
'-');
717 QCOMPARE(signal.
factor, 5.0);
718 QCOMPARE(signal.
offset, -0.25);
719 QCOMPARE(signal.
minimum, -1.0);
720 QCOMPARE(signal.
maximum, 200.0);
721 }
catch (
const std::exception &error) { CHECK(
false, error.what()); }
723 fixture.Write(
"BO_ 1 Frame: 8 ECU\n"
724 "SG_\tValue\t:\t0 | 8 @1+ ( 1 , 0 ) [ 0 | 255 ] \"rpm\" ECU1,\tECU2\n");
728 CHECK(signal.
name ==
"Value" && signal.
unit ==
"rpm" &&
729 signal.
receivers == std::vector<std::string>({
"ECU1",
"ECU2"}),
730 "Signal tabs, spaces, and receiver list");
731 }
catch (
const std::exception &error) { CHECK(
false, error.what()); }
734void DbcParserTest::dispatch() {
735 CountingHandler::instances = 0;
736 CountingHandler::calls = 0;
740 dispatcher.RegisterType<CountingHandler>(
"TEST_");
741 CHECK(dispatcher.
Dispatch(
"TEST_ first", context) &&
742 dispatcher.
Dispatch(
"TEST_ second", context) &&
743 CountingHandler::instances == 1 && CountingHandler::calls == 2 &&
744 dispatchDatabase.
frames.size() == 2U,
745 "Factory dispatch and handler reuse");
746 CHECK(!dispatcher.
Dispatch(
"TEST_OTHER data", context) &&
747 !dispatcher.
Dispatch(
"BO_TX_BU_ 1 : ECU;", context) &&
748 !dispatcher.
Dispatch(
"VAL_TABLE_ States 0 \"Stopped\";", context) &&
749 CountingHandler::calls == 2,
750 "Exact and unknown keyword dispatch");
753void DbcParserTest::independentSessions() {
756 fixture.Write(
"BO_ 1 First: 8 ECU\n");
757 CHECK(parser.
ParseFile(fixture.path.string()).
frames.size() == 1U,
"First parse");
758 fixture.Write(
"SG_ Speed : invalid\n");
761 CHECK(
false,
"Independent parse context");
762 }
catch (
const std::runtime_error &error) {
763 CHECK(std::string(error.what()).find(
"Signal found before any frame") !=
765 "Independent parse context");
767 fixture.Write(
"BO_ 2 Second: 8 ECU\n");
769 CHECK(recovered.
frames.size() == 1U && recovered.
frames[0].canId == 2U,
770 "Fresh parse after error");
774void DbcParserTest::valueDescriptions() {
777 "VAL_\t256\tState\t0\t\"Off\"\t1 \"On\" 2 \"Error with spaces; and \\\"quotes\\\"\";\n"
778 "BO_ 256 Status: 8 ECU\n"
779 "SG_ State : 0|8@1+ (2,10) [0|520] \"\" ECU\n"
780 "VAL_ 256 State 3 \"\";\n"
781 "VAL_ 2147483904 State -9223372036854775808 \"Minimum\" -1 \"Negative\" 9223372036854775807 \"Maximum\";\n"
782 "BO_ 2147483904 Extended: 8 ECU\n"
783 "SG_ State : 0|64@1- (1,0) [-1e20|1e20] \"\" ECU\n"
784 "BO_ 2048 Unsigned: 8 ECU\n"
785 "SG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n"
786 "VAL_ 2147485696 State 9223372036854775808 \"High\" 18446744073709551615 \"Full\";\n");
790 CHECK(tree->GetChild(0)->GetSignal() ==
nullptr,
"Frame node");
791 for (std::size_t index = 0; index < parsed.
frames.size(); ++index) {
792 CHECK(tree->GetChild(index)->GetChild(0)->GetSignal()->valueDescriptions ==
793 parsed.
frames[index].signals[0].valueDescriptions &&
794 runtime.
GetFrames()[index].signals[0].valueDescriptions ==
795 parsed.
frames[index].signals[0].valueDescriptions,
796 "All mappings survive both builders");
798 CHECK(tree->GetChild(0)->GetChild(0)->GetSignal()->valueDescriptions ==
799 runtime.
GetFrames()[0].signals[0].valueDescriptions,
800 "Both builders preserve descriptions");
803 frame.
data.resize(8);
804 const std::string expected[] = {
"Off",
"On",
"Error with spaces; and \"quotes\"",
""};
805 for (
unsigned value = 0; value < 5; ++value) {
806 frame.
data[0] = value;
808 CHECK(decoded.
valid && decoded.
physicalValue == value * 2 + 10,
"Numeric scaling unchanged");
810 CHECK(value < 4 ? description && *description == expected[value] : !description,
811 "Raw lookup and unknown value");
816 const char *description;
818 for (
const Boundary &test: {Boundary{0x8000000000000000ULL,
"Minimum"},
819 Boundary{0xFFFFFFFFFFFFFFFFULL,
"Negative"},
820 Boundary{0x7FFFFFFFFFFFFFFFULL,
"Maximum"}}) {
821 for (
unsigned byte = 0;
byte < 8; ++byte)
822 frame.
data[
byte] =
static_cast<std::uint8_t
>(test.bits >> (
byte * 8));
825 "Signed boundaries and distinct extended frame");
828 for (
const Boundary &test: {Boundary{0x8000000000000000ULL,
"High"},
829 Boundary{0xFFFFFFFFFFFFFFFFULL,
"Full"}}) {
830 for (
unsigned byte = 0;
byte < 8; ++byte)
831 frame.
data[
byte] =
static_cast<std::uint8_t
>(test.bits >> (
byte * 8));
834 "Unsigned boundaries");
836 CHECK(FindValueDescription(runtime.
GetFrames()[0].signals[0].valueDescriptions, std::int64_t(1)) !=
nullptr,
837 "Nonnegative signed lookup matches unsigned key");
839 CHECK(!
DbcDecoder().Decode(runtime, frame).signals[0].GetValueDescription(),
"Invalid decode has no description");
842void DbcParserTest::invalidValueDescriptions() {
844 const std::string definitions =
"BO_ 1 Frame: 8 ECU\nSG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n";
845 for (
const std::string statement: {
846 "VAL_",
"VAL_ 1 State;",
"VAL_ 1 State ;",
"VAL_ 1 State 0 \"Off\"",
847 "VAL_ 1 State 0 \"Off\"; junk",
"VAL_ 1 State 0 Off;",
848 "VAL_ 1 State 0 \"Unclosed;",
"VAL_ 1 State 0 \"Off\"1 \"On\";",
849 "VAL_ 1 State 0 \"Off\" 00 \"Duplicate\";",
850 "VAL_ 1 State 0 \"Off\" -0 \"Duplicate\";",
851 "VAL_ 1 State 0 \"Off\";\nVAL_ 1 State 0 \"Duplicate\";",
852 "VAL_ 2 State 0 \"Missing frame\";",
"VAL_ 1 Missing 0 \"Missing signal\";",
853 "VAL_ -1 State 0 \"Bad ID\";",
"VAL_ 536870912 State 0 \"Bad ID\";",
854 "VAL_ 4294967296 State 0 \"Bad ID\";",
"VAL_ Environment 0 \"Unsupported\";",
855 "VAL_ 1 State 0 \"Bad\\q escape\";"}) {
856 fixture.Write(definitions + statement +
'\n');
857 const int line = statement.find(
'\n') == std::string::npos ? 3 : 4;
860 CHECK(
false, statement);
861 }
catch (
const std::runtime_error &error) {
862 CHECK(std::string(error.what()).find(fixture.path.string() +
":" + std::to_string(line) +
": ") == 0,
863 statement +
" " + error.what());
866 for (
const std::string suffix: {
867 "BO_ 1 Other: 8 ECU\n",
868 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
870 fixture.Write(
"VAL_ 1 " + std::string(suffix.empty() ?
"Missing" :
"State") +
871 " 0 \"Description\";\n" + definitions + suffix);
874 CHECK(
false,
"Missing reference or duplicate definition");
875 }
catch (
const std::runtime_error &error) {
876 const std::string line = suffix.empty() ?
"1" :
"4";
877 CHECK(std::string(error.what()).find(fixture.path.string() +
":" + line +
": ") == 0,
878 "Error retains the reference or duplicate definition source location");
881 for (
const std::string number: {
"+1",
"1x",
"0x10",
"1.5",
"1e2",
"--1",
882 "-9223372036854775809",
"18446744073709551616"}) {
883 fixture.Write(
"VAL_ 1 State " + number +
" \"Bad\";\n" + definitions);
886 CHECK(
false, number);
887 }
catch (
const std::runtime_error &error) {
888 CHECK(std::string(error.what()).find(fixture.path.string() +
":1: ") == 0, number);
893int RunParserTests(
int argc,
char **argv) {
895 return QTest::qExec(&tests, argc, argv);
898#include "dbc_parser_test.moc"
Converts parsed DBC data into runtime decode database.
DecodeDatabase Build(const DbcDatabase &source) const
Build runtime decode database.
Runtime CAN decoder using prebuilt decode database.
DecodedFrameValue Decode(const DecodeDatabase &database, const RawCanFrame &frame) const
Decode one raw CAN frame.
DbcParseResult ParseFileWithDiagnostics(const std::string &filePath) const
Parse with warnings and a structured first error instead of throwing.
DbcDatabase ParseFile(const std::string &filePath) const
Parse DBC file.
bool Dispatch(const std::string &line, DbcParseContext &context) const
Builds a simple tree from parsed DBC database.
std::unique_ptr< TreeNode > Build(const DbcDatabase &database) const
Build tree representation of parsed DBC data.
Immutable runtime snapshot. Copies share storage; reload replaces a snapshot. Owning lookup handles a...
const std::vector< DecodeFrame > & GetFrames() const
Created: 2026-03-13 Author: Deeaitch (Dim. Himro)
Created: 2026-03-13 Author: Deeaitch (Dim. Himro)
Created: 2026-03-13 Author: Deeaitch (Dim. Himro)
Created: 2026-03-13 Author: Deeaitch (Dim. Himro)
Parsed DBC content stored in a simple internal form.
std::vector< FrameInfo > frames
std::shared_ptr< const DecodeFrame > definition
One decoded signal value.
const std::string * GetValueDescription() const
Describes one CAN frame from a DBC file.
Raw CAN frame used for runtime or trace decoding.
std::vector< std::uint8_t > data
Describes one signal inside a DBC frame.
std::vector< std::string > receivers