4#include "dbc_statement_handler.h"
16#define CHECK(condition, name) QVERIFY2((condition), std::string(name).c_str())
20 std::filesystem::path path = std::filesystem::temp_directory_path() /
21 (
"dbc_parser_test_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) +
".dbc");
23 std::error_code error;
24 std::filesystem::remove(path, error);
26 void Write(
const std::string &text)
const {
27 std::ofstream output(path);
30 throw std::runtime_error(
"Cannot write test fixture.");
34class CommaDecimalPunctuation :
public std::numpunct<char> {
36 char do_decimal_point()
const override {
return ','; }
37 char do_thousands_sep()
const override {
return '.'; }
38 std::string do_grouping()
const override {
return "\3"; }
41class ApplicationLocaleGuard {
43 ApplicationLocaleGuard()
44 : previousCppLocale(std::locale()), previousCLocale(std::setlocale(LC_ALL, nullptr)) {}
45 ~ApplicationLocaleGuard() {
46 std::locale::global(previousCppLocale);
47 std::setlocale(LC_ALL, previousCLocale.c_str());
51 std::locale previousCppLocale;
52 std::string previousCLocale;
57 inline static int instances = 0;
58 inline static int calls = 0;
59 CountingHandler() { ++instances; }
66std::string SignalLine(
const std::string &start,
const std::string &length,
67 const std::string &order,
const std::string &factor,
68 const std::string &offset,
const std::string &minimum,
69 const std::string &maximum,
const std::string &name =
"Value") {
70 return "SG_ " + name +
" : " + start +
"|" + length +
"@" + order +
71 " (" + factor +
"," + offset +
") [" + minimum +
"|" + maximum +
"] \"\" ECU";
74class DbcParserTest :
public QObject {
77 void CheckPhysicalNumbers();
78 void RejectSignal(
const Fixture &fixture,
const std::string &line,
unsigned payload = 8U) {
79 fixture.Write(
"BO_ 1 Frame: " + std::to_string(payload) +
" ECU\n\n" + line +
"\n");
82 CHECK(
false,
"Reject " + line);
83 }
catch (
const std::runtime_error &error) {
84 const std::string prefix = fixture.path.string() +
":3: ";
85 CHECK(std::string(error.what()).find(prefix) == 0U &&
86 std::string(error.what()).size() > prefix.size(),
92 void multilineAttributes();
93 void invalidMultilineAttributes();
94 void invalidAttributes();
95 void attributeNumbers();
96 void invalidAttributeNumbers_data();
97 void invalidAttributeNumbers();
98 void attributeWarnings();
100 void diagnosticErrors();
101 void diagnosticUnsupportedStatements();
102 void unsupportedSignalValueTypes_data();
103 void unsupportedSignalValueTypes();
104 void diagnosticNamespaceBoundaries();
105 void diagnosticFormatting();
107 void multiplexingBoundaries();
108 void extendedMultiplexing();
109 void extendedMultiplexingErrors();
111 void invalidFrames();
112 void duplicateFrames();
113 void duplicateSignals();
114 void distinctDefinitions();
116 void invalidComments_data();
117 void invalidComments();
119 void invalidSignals();
121 void longSignalDefinitions_data();
122 void longSignalDefinitions();
123 void physicalNumbersCppLocale();
124 void physicalNumbersCLocale();
126 void independentSessions();
127 void valueDescriptions();
128 void invalidValueDescriptions();
131void DbcParserTest::attributes() {
134 "BA_ \"GenMsgCycleTime\" BO_ 1 25;\n"
135 "BA_ \"VFrameFormat\" BO_ 2147483649 1;\n"
136 "BA_ \"Label\" SG_ 1 Value \"a\\\"b\\\\c\";\n"
137 "BA_ \"NodeLabel\" BU_ ECU \"node\";\n"
138 "BA_ \"Environment\" EV_ Env 2;\n"
139 "BA_DEF_DEF_ \"GenMsgCycleTime\" 100;\n"
140 "BA_DEF_DEF_ \"VFrameFormat\" \"StandardCAN\";\n"
141 "BA_DEF_DEF_ \"Label\" \"default\";\n"
142 "BA_DEF_DEF_ \"BusLabel\" \"CAN\";\n"
143 "BA_DEF_DEF_ \"Ratio\" 1.5e-1;\n"
144 "BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 10000;\n"
145 "BA_DEF_ BO_ \"VFrameFormat\" ENUM \"StandardCAN\",\"ExtendedCAN\",\"StandardCAN_FD\";\n"
146 "BA_DEF_ SG_ \"Label\" STRING;\n"
147 "BA_DEF_ \"BusLabel\" STRING;\n"
148 "BA_DEF_ BO_ \"Ratio\" FLOAT -1 1;\n"
149 "BA_DEF_ BU_ \"NodeLabel\" STRING;\n"
150 "BA_DEF_ EV_ \"Environment\" HEX 0 255;\n"
151 "BO_ 1 Standard: 1 ECU\n"
152 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
153 "BO_ 2147483649 Extended: 1 ECU\n"
154 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n");
161 QCOMPARE(std::get<std::string>(database.
attributes.at(
"BusLabel")), std::string(
"CAN"));
162 QCOMPARE(database.
frames[0].cycleTimeMs.value(), 25U);
163 QCOMPARE(database.
frames[1].cycleTimeMs.value(), 100U);
164 QCOMPARE(database.
frames[1].frameFormat.value(), std::string(
"ExtendedCAN"));
165 QVERIFY(!database.
frames[1].hasPgn);
166 QCOMPARE(std::get<double>(database.
frames[0].attributes.at(
"Ratio")), 0.15);
167 QCOMPARE(std::get<std::string>(database.
frames[0].signals[0].attributes.at(
"Label")), std::string(
"a\"b\\c"));
168 QCOMPARE(std::get<std::string>(database.
frames[1].signals[0].attributes.at(
"Label")), std::string(
"default"));
170 QCOMPARE(runtime.
GetFrames()[0].cycleTimeMs.value(), 25U);
171 QCOMPARE(runtime.
GetFrames()[1].frameFormat.value(), std::string(
"ExtendedCAN"));
172 QCOMPARE(runtime.
GetFrames()[0].signals[0].attributes, database.
frames[0].signals[0].attributes);
174 QCOMPARE(tree->GetChild(0)->GetFrame()->cycleTimeMs.value(), 25U);
175 QCOMPARE(tree->GetChild(0)->GetChild(0)->GetSignal()->attributes, database.
frames[0].signals[0].attributes);
176 fixture.Write(
"BA_DEF_ BO_ \"Period\" INT 0 10;\nBA_DEF_DEF_ \"Period\" 1;\n"
177 "BA_ \"Period\" BO_ 1 2;\nBA_ \"Period\" BO_ 1 3;\nBO_ 1 F: 0 ECU\n");
179 QCOMPARE(std::get<std::int64_t>(repeated.
frames[0].attributes.at(
"Period")), std::int64_t(3));
181 fixture.Write(
"BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 4294967295;\n"
182 "BA_DEF_DEF_ \"GenMsgCycleTime\" 0;\n"
183 "BA_ \"GenMsgCycleTime\" BO_ 1 4294967295;\n"
184 "BA_DEF_ \"Wide\" INT -9223372036854775808 9223372036854775807;\n"
185 "BA_DEF_DEF_ \"Wide\" -9223372036854775808;\n"
186 "BA_DEF_ SG_ \"State\" ENUM \"off\",\"on\";\n"
187 "BA_DEF_DEF_ \"State\" 1;\n"
188 "BO_ 1 F: 0 ECU\nBO_ 2 Empty: 0 ECU\n");
190 QCOMPARE(boundaries.
frames[0].cycleTimeMs.value(), std::numeric_limits<std::uint32_t>::max());
191 QCOMPARE(boundaries.
frames[1].cycleTimeMs.value(), 0U);
192 QVERIFY(!boundaries.
frames[0].frameFormat);
193 QCOMPARE(std::get<std::int64_t>(boundaries.
attributes.at(
"Wide")), std::numeric_limits<std::int64_t>::min());
194 QCOMPARE(std::get<std::int64_t>(*boundaries.
attributeDefinitions.at(
"State").defaultValue), std::int64_t(1));
197void DbcParserTest::multilineAttributes() {
199 fixture.Write(R
"DBC(BA_
200 "GenMsgCycleTime" BO_ 1
202BA_DEF_ BO_ "VFrameFormat" ENUM
208BA_DEF_ BO_ "GenMsgCycleTime"
210BA_DEF_DEF_ "GenMsgCycleTime"
212BA_DEF_ SG_ "Label" STRING;
213BA_ "Label" SG_ 1 Value
216BA_ "VFrameFormat" BO_ 1
219SG_ Value : 0|8@1+ (1,0) [0|255] "" ECU
227 QCOMPARE(result.
database.
frames[0].frameFormat.value(), std::string(
"StandardCAN"));
228 QCOMPARE(result.
database.
frames[1].frameFormat.value(), std::string(
"StandardCAN_FD"));
229 QCOMPARE(std::get<std::string>(result.
database.
frames[0].signals[0].attributes.at(
"Label")),
230 std::string(
"first; \"quoted\"\nsecond\\path"));
231 QCOMPARE(result.
diagnostics.size(), std::size_t(1));
232 QCOMPARE(result.
diagnostics[0].code, DbcDiagnosticCode::UnsupportedAttribute);
233 QCOMPARE(result.
diagnostics[0].lineNumber, std::size_t(7));
235 fixture.Write(
"BA_DEF_ BO_ \"VFrameFormat\" ENUM\r\n \"StandardCAN\",\r\n \"ExtendedCAN_FD\";\r\n"
236 "BA_\r\n \"VFrameFormat\" BO_ 1\r\n 1;\r\nBO_ 1 F: 0 ECU\r\n");
239 QCOMPARE(explicitFormat.
diagnostics.size(), std::size_t(1));
240 QCOMPARE(explicitFormat.
diagnostics[0].code, DbcDiagnosticCode::UnsupportedAttribute);
241 QCOMPARE(explicitFormat.
diagnostics[0].lineNumber, std::size_t(4));
242 QCOMPARE(explicitFormat.
database.
frames[0].frameFormat.value(), std::string(
"ExtendedCAN_FD"));
244 fixture.Write(
"BA_DEF_ \"Text\" STRING;\n BA_ \"Text\" \"first \nsecond\";\n"
245 "UnsupportedStatement\n");
248 QCOMPARE(std::get<std::string>(whitespace.
database.
attributes.at(
"Text")), std::string(
"first \nsecond"));
249 QCOMPARE(whitespace.
diagnostics.size(), std::size_t(1));
250 QCOMPARE(whitespace.
diagnostics[0].code, DbcDiagnosticCode::UnsupportedStatement);
251 QCOMPARE(whitespace.
diagnostics[0].lineNumber, std::size_t(4));
254void DbcParserTest::invalidMultilineAttributes() {
256 const std::vector<std::string> invalid = {
257 "BA_DEF_\n BO_ \"A\" INT\n 10 0;\n",
258 "BA_DEF_ BO_ \"A\" ENUM\n \"one\",\n;\n",
259 "BA_DEF_ BO_ \"A\" STRING\n",
260 "BA_DEF_ BO_ \"A\" ENUM\n \"unterminated;\n",
261 "BA_DEF_DEF_\n \"Unknown\"\n 0;\n",
262 "BA_\n \"Unknown\" BO_ 1\n 0;\n",
263 "BA_DEF_ BO_ \"A\" STRING\n ; trailing\n"};
264 for (
const std::string &statement: invalid) {
265 fixture.Write(
"BO_ 1 F: 0 ECU\n" + statement);
267 QVERIFY2(result.
HasErrors(), statement.c_str());
269 QCOMPARE(result.
diagnostics.back().code, DbcDiagnosticCode::InvalidAttribute);
270 QCOMPARE(result.
diagnostics.back().lineNumber, std::size_t(2));
271 QVERIFY_THROWS_EXCEPTION(std::runtime_error,
DbcParser().ParseFile(fixture.path.string()));
275void DbcParserTest::invalidAttributes() {
277 const std::vector<std::string> invalid = {
278 "BA_DEF_ BO_ \"A\"STRING;",
279 "BA_DEF_ BO_\"A\" STRING;",
280 "BA_DEF_ BO_ \"A\" \"STRING\";",
281 "BA_DEF_ BO_ A STRING;",
282 "BA_DEF_ ZZ_ \"A\" STRING;",
283 "BA_DEF_ BO_ \"A\" BLOB;",
284 "BA_DEF_ BO_ \"A\" FLOAT 2 1;",
285 "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 0;",
286 "BA_DEF_ BO_ \"A\" FLOAT 0 1;\nBA_ \"A\" BO_ 1 2;",
287 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ -1 0;",
288 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 4294967296 0;",
289 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 536870912 0;",
290 "BA_DEF_ BO_ \"A\" INT 2 1;",
291 "BA_DEF_ BO_ \"A\" FLOAT nan 1;",
292 "BA_DEF_ BO_ \"A\" FLOAT 0 inf;",
293 "BA_DEF_ BO_ \"A\" INT 0 9223372036854775808;",
294 "BA_DEF_ BO_ \"A\" STRING; trailing",
295 "BA_DEF_ BO_ \"A\" ENUM;",
296 "BA_DEF_ BO_ \"A\" ENUM \"one\",;",
297 "BA_DEF_ BO_ \"A\" ENUM \"one\",\"one\";",
298 "BA_DEF_ BO_ \"VFrameFormat\" STRING;",
299 "BA_DEF_ SG_ \"GenMsgCycleTime\" INT 0 10;",
300 "BA_ \"Unknown\" BO_ 1 0;",
301 "BA_DEF_DEF_ \"Unknown\" 0;",
302 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 1 2;",
303 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 1 \"0\";",
304 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" SG_ 1 S 0;",
305 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 2 0;",
306 "BA_DEF_ SG_ \"A\" INT 0 1;\nBA_ \"A\" SG_ 1 Missing 0;",
307 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_DEF_ BO_ \"A\" INT 0 1;",
308 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_DEF_DEF_ \"A\" 0;\nBA_DEF_DEF_ \"A\" 1;",
309 "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 \"bad\\q\";",
310 "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 \"unterminated;",
311 "BA_DEF_ BO_ \"A\" ENUM \"one\";\nBA_DEF_DEF_ \"A\" \"missing\";",
312 "BA_DEF_ BO_ \"A\" ENUM \"one\";\nBA_ \"A\" BO_ 1 1;",
313 "BA_DEF_ BO_ \"GenMsgCycleTime\" INT -1 4294967296;\nBA_DEF_DEF_ \"GenMsgCycleTime\" -1;",
314 "BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 4294967296;\nBA_ \"GenMsgCycleTime\" BO_ 1 4294967296;"};
315 for (
const std::string &statement: invalid) {
316 fixture.Write(
"BO_ 1 F: 0 ECU\n" + statement +
'\n');
318 QVERIFY2(result.
HasErrors(), statement.c_str());
320 QCOMPARE(result.
diagnostics.back().code, DbcDiagnosticCode::InvalidAttribute);
321 QCOMPARE(result.
diagnostics.back().lineNumber, std::size_t(2 + std::count(statement.begin(), statement.end(),
'\n')));
322 QVERIFY_THROWS_EXCEPTION(std::runtime_error,
DbcParser().ParseFile(fixture.path.string()));
326void DbcParserTest::attributeNumbers() {
327 ApplicationLocaleGuard guard;
328 std::locale::global(std::locale(std::locale::classic(),
new CommaDecimalPunctuation));
330 fixture.Write(
"BA_DEF_ \"I\" INT -9223372036854775808 +9223372036854775807;\n"
331 "BA_DEF_DEF_ \"I\" -9223372036854775808;\n"
332 "BA_ \"I\" +9223372036854775807;\n"
333 "BA_DEF_ \"H\" HEX -10 +10;\n"
334 "BA_DEF_DEF_ \"H\" +0;\n"
336 "BA_DEF_ \"F\" FLOAT -1.25e+2 +2.5E2;\n"
337 "BA_DEF_DEF_ \"F\" +.5;\n"
338 "BA_ \"F\" -1.25e+2;\n"
339 "BA_DEF_ BU_ \"NodeDefault\" INT 0 10;\n"
340 "BA_DEF_DEF_ \"NodeDefault\" 5;\n"
341 "BA_DEF_ EV_ \"EnvironmentDefault\" STRING;\n"
342 "BA_DEF_DEF_ \"EnvironmentDefault\" \"default\";\n");
347 QCOMPARE(std::get<std::int64_t>(result.
database.
attributes.at(
"I")), std::numeric_limits<std::int64_t>::max());
349 QCOMPARE(std::get<std::int64_t>(result.
database.
attributes.at(
"H")), std::int64_t(-10));
357void DbcParserTest::invalidAttributeNumbers_data() {
358 QTest::addColumn<QString>(
"type");
359 QTest::addColumn<QString>(
"number");
360 const char *types[] = {
"INT",
"HEX",
"FLOAT"};
361 const char *invalid[] = {
"+",
"-",
"++1",
"+-1",
"--1",
"1x",
"0x10",
362 "nan",
"inf",
"-inf",
"1e9999",
"1e-9999"};
363 for (
const char *type: types) {
364 for (
const char *number: invalid) {
365 const std::string name = std::string(type) +
'-' + number;
366 QTest::newRow(name.c_str()) << QString(type) << QString(number);
369 QTest::newRow(
"integer-overflow") << QString(
"INT") << QString(
"9223372036854775808");
370 QTest::newRow(
"integer-underflow") << QString(
"INT") << QString(
"-9223372036854775809");
371 QTest::newRow(
"integer-fraction") << QString(
"INT") << QString(
"1.5");
374void DbcParserTest::invalidAttributeNumbers() {
375 QFETCH(QString, type);
376 QFETCH(QString, number);
377 const std::string kind = type.toStdString();
378 const std::string text = number.toStdString();
379 const std::string definition =
"BA_DEF_ \"A\" " + kind +
" -10 10;\n";
380 const std::string statements[] = {
381 "BA_DEF_ \"A\" " + kind +
' ' + text +
" 10;\n",
382 "BA_DEF_ \"A\" " + kind +
" -10 " + text +
";\n",
383 definition +
"BA_DEF_DEF_ \"A\" " + text +
";\n",
384 definition +
"BA_ \"A\" " + text +
";\n"};
386 for (
const std::string &statement: statements) {
387 fixture.Write(statement);
389 QVERIFY2(result.
HasErrors(), statement.c_str());
391 QCOMPARE(result.
diagnostics.back().code, DbcDiagnosticCode::InvalidAttribute);
393 static_cast<std::size_t
>(std::count(statement.begin(), statement.end(),
'\n')));
394 QVERIFY_THROWS_EXCEPTION(std::runtime_error,
DbcParser().ParseFile(fixture.path.string()));
398void DbcParserTest::attributeWarnings() {
400 fixture.Write(
"BA_DEF_ BO_ \"VFrameFormat\" ENUM \"StandardCAN\",\"ExtendedCAN\",\"J1939PG\",\"StandardCAN_FD\";\n"
401 "BA_DEF_DEF_ \"VFrameFormat\" \"StandardCAN_FD\";\n"
402 "BA_ \"VFrameFormat\" BO_ 1 2;\n"
403 "BA_ \"VFrameFormat\" BO_ 2147483649 0;\n"
404 "BO_ 1 F: 0 ECU\nBO_ 2147483649 E: 0 ECU\nBO_ 2 Default: 0 ECU\n");
407 QCOMPARE(result.
diagnostics.size(), std::size_t(3));
408 const std::size_t lines[] = {3U, 4U, 2U};
409 for (std::size_t index = 0; index < 3U; ++index) {
410 QCOMPARE(result.
diagnostics[index].code, DbcDiagnosticCode::UnsupportedAttribute);
411 QCOMPARE(result.
diagnostics[index].severity, DbcDiagnosticSeverity::Warning);
412 QCOMPARE(result.
diagnostics[index].lineNumber, lines[index]);
413 QCOMPARE(result.
diagnostics[index].filePath, fixture.path.string());
415 QCOMPARE(result.
database.
frames[0].frameFormat.value(), std::string(
"J1939PG"));
420void DbcParserTest::diagnostics() {
422 fixture.Write(
"VERSION \"test\"\n"
430 "BO_ 1 Frame: 1 ECU\n"
431 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
432 "SIG_TYPE_REF_ 1 Value : Custom;\n"
433 "BA_REL_ \"SignalType\" SG_ 1 Value 1;\n"
434 "VENDOR_EXTENSION_ data;\n");
437 "Warnings preserve the supported database");
438 QCOMPARE(result.
diagnostics.size(), std::size_t(3U));
439 for (std::size_t index = 0U; index < result.
diagnostics.size(); ++index) {
441 CHECK(diagnostic.
severity == DbcDiagnosticSeverity::Warning &&
442 diagnostic.
code == DbcDiagnosticCode::UnsupportedStatement,
443 "Unsupported statements have structured warning severity and code");
444 QCOMPARE(diagnostic.
filePath, fixture.path.string());
446 CHECK(diagnostic.
message.find(
"ignored") != std::string::npos,
447 "Warning explains unsupported syntax");
449 CHECK(
DbcParser().ParseFile(fixture.path.string()).frames.size() == 1U,
450 "Legacy API accepts files with warnings");
451 fixture.Write(
"VERSION \"\"\nNS_:\nSIG_VALTYPE_\n\tBA_\n BS_ :\n BU_ : ECU\n");
453 CHECK(headers.
diagnostics.empty(),
"Common header variants stay quiet");
454 fixture.Write(
"NS_ :\n BA_\n BO_\n");
455 CHECK(
DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
456 "Namespace handling does not swallow a malformed frame");
459void DbcParserTest::diagnosticErrors() {
463 DbcDiagnosticCode code;
466 const ErrorCase cases[] = {
467 {
"UNKNOWN_ data;\n\nBO_ invalid Frame: 1 ECU\n",
468 DbcDiagnosticCode::InvalidStatement, 3U},
469 {
"BO_ 1 Frame: 1 ECU\nSG_MUL_VAL_ 1 Value Mode 1-2;\n",
470 DbcDiagnosticCode::InvalidMultiplexing, 2U},
471 {
"BO_ 1 Frame: 1 ECU\nVAL_ 1 Missing 0 \"Off\";\n",
472 DbcDiagnosticCode::InvalidValueDescription, 2U},
473 {
"BO_ 1 Frame: 1 ECU\nSG_ Value m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
474 DbcDiagnosticCode::InvalidMultiplexing, 2U},
475 {
"BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
476 DbcDiagnosticCode::InvalidMultiplexing, 2U},
477 {
"VAL_ broken\n", DbcDiagnosticCode::InvalidValueDescription, 1U},
478 {
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
479 DbcDiagnosticCode::InvalidStatement, 1U},
480 {
"BO_ 1 Frame: 1 ECU\nBO_ 1 Duplicate: 1 ECU\n",
481 DbcDiagnosticCode::InvalidStatement, 2U},
482 {
"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
483 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
484 DbcDiagnosticCode::InvalidStatement, 3U},
485 {
"BO_ 1 Frame: 1 ECU\nSG_ Value : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
486 DbcDiagnosticCode::InvalidStatement, 2U},
487 {
"BO_ 1 Frame: 1 ECU\nVAL_ 2 Missing 0 \"Off\";\n",
488 DbcDiagnosticCode::InvalidValueDescription, 2U},
489 {
"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
490 "VAL_ 1 Value 0 \"Off\";\nVAL_ 1 Value 0 \"Duplicate\";\n",
491 DbcDiagnosticCode::InvalidValueDescription, 4U},
492 {
"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"
493 "SG_ Value m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
494 DbcDiagnosticCode::InvalidMultiplexing, 3U},
495 {
"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
496 "SG_ Other M : 8|8@1- (1,0) [-128|127] \"\" ECU\n",
497 DbcDiagnosticCode::InvalidMultiplexing, 3U},
498 {
"BO_ 1 Frame: 1 ECU\nSG_ First : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
499 "SG_ Second : 7|8@0+ (1,0) [0|255] \"\" ECU\n",
500 DbcDiagnosticCode::InvalidMultiplexing, 3U}};
501 for (
const ErrorCase &test : cases) {
502 fixture.Write(test.text);
505 "Failed parse never returns a partial database");
507 CHECK(diagnostic.
severity == DbcDiagnosticSeverity::Error && diagnostic.
code == test.code,
508 "Error has expected severity and code");
509 QCOMPARE(diagnostic.
filePath, fixture.path.string());
511 CHECK(!diagnostic.
message.empty(),
"Error includes a message");
512 const bool hasWarning = std::string(test.text).find(
"UNKNOWN_") == 0U;
513 QCOMPARE(result.
diagnostics.size(), hasWarning ? std::size_t(2U) : std::size_t(1U));
516 CHECK(
false,
"Legacy parser must throw");
517 }
catch (
const std::runtime_error &error) {
518 QCOMPARE(std::string(error.what()), fixture.path.string() +
":" +
519 std::to_string(test.line) +
": " + diagnostic.
message);
522 const std::string missingPath = fixture.path.string() +
".missing";
525 QCOMPARE(missing.
diagnostics.size(), std::size_t(1U));
526 CHECK(missing.
diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
527 "Missing file has a distinct code");
528 QCOMPARE(missing.
diagnostics[0].lineNumber, std::size_t(0U));
529 QCOMPARE(missing.
diagnostics[0].filePath, missingPath);
530 QCOMPARE(missing.
diagnostics[0].message,
"Failed to open DBC file: " + missingPath);
533 CHECK(
false,
"Legacy API throws on missing file");
534 }
catch (
const std::runtime_error &error) {
535 QCOMPARE(std::string(error.what()), missing.
diagnostics[0].message);
537 const std::string directoryPath = fixture.path.parent_path().string();
540 "Directory cannot be parsed as a file");
541 QCOMPARE(directory.
diagnostics.size(), std::size_t(1U));
542 CHECK(directory.
diagnostics[0].code == DbcDiagnosticCode::FileReadError ||
543 directory.
diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
544 "Directory produces a file-level error on supported platforms");
545 QCOMPARE(directory.
diagnostics[0].lineNumber, std::size_t(0U));
546 QCOMPARE(directory.
diagnostics[0].filePath, directoryPath);
548 CHECK(directory.
diagnostics[0].code == DbcDiagnosticCode::FileReadError,
549 "Linux directory input exercises the read-error path");
550 QCOMPARE(directory.
diagnostics[0].message,
"Failed to read DBC file: " + directoryPath);
554 CHECK(
false,
"Legacy API throws on unreadable input");
555 }
catch (
const std::runtime_error &error) {
556 QCOMPARE(std::string(error.what()), directory.
diagnostics[0].message);
558 fixture.Write(
"BO_ 1 Frame: 1 ECU\n");
559 CHECK(
DbcParser().ParseFileWithDiagnostics(fixture.path.string()).diagnostics.empty(),
560 "Diagnostics belong to an independent parse session");
563void DbcParserTest::diagnosticUnsupportedStatements() {
565 const std::string definitions =
566 "BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (2,10) [10|520] \"V\" ECU\n"
567 "CM_ SG_ 1 Value \"Signal comment\";\nVAL_ 1 Value 1 \"On\";\n";
568 const std::vector<std::string> statements = {
569 "BA_REL_ \"SignalType\" SG_ 1 Value 1;",
570 "BA_DEF_REL_ BU_SG_REL_ \"Relation\" INT 0 1;",
571 "BA_REL_ \"Relation\" BU_SG_REL_ ECU SG_ 1 Value 1;",
572 "SGTYPE_ Type : 8@1+ (1,0) [0|255] \"\" 0, Table;",
573 "SIG_TYPE_REF_ 1 Value : Type;",
574 "EV_ Environment : 0 [0|1] \"\" 0 1 DUMMY_NODE_VECTOR0 ECU;",
575 "ENVVAR_DATA_ Environment : 4;",
576 "VAL_TABLE_ Table 0 \"Off\" 1 \"On\";",
577 "BO_TX_BU_ 1 : ECU;",
"VENDOR_EXTENSION_ data;",
"UNKNOWN_"
579 for (
const std::string &statement : statements) {
580 fixture.Write(definitions + statement +
'\n');
583 QCOMPARE(result.
diagnostics.size(), std::size_t(1U));
585 CHECK(warning.
severity == DbcDiagnosticSeverity::Warning &&
586 warning.
code == DbcDiagnosticCode::UnsupportedStatement, statement);
587 QCOMPARE(warning.
filePath, fixture.path.string());
588 QCOMPARE(warning.
lineNumber, std::size_t(5U));
589 const std::string keyword = statement.substr(0U, statement.find(
' '));
590 CHECK(warning.
message.find(
"'" + keyword +
"'") != std::string::npos, statement);
596 CHECK(decoded.
valid && decoded.signals.size() == 1U && decoded.signals[0].
valid,
597 "Warnings preserve supported signal decoding");
598 QCOMPARE(decoded.signals[0].physicalValue, 12.0);
599 CHECK(decoded.signals[0].GetValueDescription() &&
600 *decoded.signals[0].GetValueDescription() ==
"On",
601 "Warnings preserve VAL_ descriptions");
602 QCOMPARE(runtime.
GetFrames()[0].signals[0].comment, std::string(
"Signal comment"));
606void DbcParserTest::unsupportedSignalValueTypes_data() {
607 QTest::addColumn<std::string>(
"statement");
608 QTest::addColumn<bool>(
"beforeDefinitions");
609 QTest::newRow(
"float32") << std::string(
"SIG_VALTYPE_ 1 Value : 1;") <<
false;
610 QTest::newRow(
"float64") << std::string(
"SIG_VALTYPE_ 1 Value : 2;") <<
false;
611 QTest::newRow(
"float32-before-definitions") << std::string(
"SIG_VALTYPE_ 1 Value : 1;") <<
true;
612 QTest::newRow(
"float64-before-definitions") << std::string(
"SIG_VALTYPE_ 1 Value : 2;") <<
true;
613 QTest::newRow(
"extended-frame") << std::string(
"SIG_VALTYPE_ 2147483649 Value : 1;") <<
false;
614 QTest::newRow(
"tabs-crlf") << std::string(
"\tSIG_VALTYPE_\t1\tValue : 2;\r") <<
false;
615 QTest::newRow(
"integer-declaration") << std::string(
"SIG_VALTYPE_ 1 Value : 0;") <<
false;
616 QTest::newRow(
"unknown-type") << std::string(
"SIG_VALTYPE_ 1 Value : 3;") <<
false;
617 QTest::newRow(
"malformed") << std::string(
"SIG_VALTYPE_") <<
false;
620void DbcParserTest::unsupportedSignalValueTypes() {
621 QFETCH(std::string, statement);
622 QFETCH(
bool, beforeDefinitions);
623 const std::string definitions =
"BO_ 1 Frame: 8 ECU\n"
624 "SG_ Value : 0|64@1+ (1,0) [0|100] \"\" ECU\n";
626 fixture.Write(
"VERSION \"\"\nNS_:\n\tSIG_VALTYPE_\nBS_:\nVENDOR_EXTENSION_ data;\n" +
627 (beforeDefinitions ? statement +
"\n" + definitions : definitions + statement));
628 const std::size_t expectedLine = beforeDefinitions ? 6U : 8U;
633 QCOMPARE(result.
diagnostics.size(), std::size_t(2));
634 QCOMPARE(result.
diagnostics[0].severity, DbcDiagnosticSeverity::Warning);
635 QCOMPARE(result.
diagnostics[0].code, DbcDiagnosticCode::UnsupportedStatement);
637 QCOMPARE(error.
severity, DbcDiagnosticSeverity::Error);
638 QCOMPARE(error.
code, DbcDiagnosticCode::UnsupportedSignalType);
640 QCOMPARE(error.
filePath, fixture.path.string());
641 QVERIFY(error.
message.find(
"SIG_VALTYPE_") != std::string::npos);
644 QFAIL(
"ParseFile must reject explicit signal value types");
645 }
catch (
const std::runtime_error &exception) {
646 QCOMPARE(std::string(exception.what()), fixture.path.string() +
":" +
647 std::to_string(expectedLine) +
": " + error.
message);
651void DbcParserTest::diagnosticNamespaceBoundaries() {
653 const std::vector<std::string> endings = {
654 "BS_\n",
"BS_:\n",
"BS_ :\n",
"BU_\n",
"BU_:\n",
"BU_ : ECU\n",
655 "BO_ 1 Frame: 1 ECU\n",
"VENDOR_EXTENSION_ data;\n"
657 for (
const std::string &ending : endings) {
658 fixture.Write(
"NS_ :\n\tSG_MUL_VAL_\n\nBA_\n" + ending +
"SIG_TYPE_REF_\n");
661 const std::size_t expectedWarnings = ending.find(
"VENDOR_") == 0U ? 2U : 1U;
662 QCOMPARE(result.
diagnostics.size(), expectedWarnings);
663 QCOMPARE(result.
diagnostics.back().lineNumber, std::size_t(6U));
664 CHECK(result.
diagnostics.back().message.find(
"SIG_TYPE_REF_") != std::string::npos,
665 "Unsupported keyword outside NS_ must produce a warning");
667 for (
const std::string &definition : {std::string(
"BO_"), std::string(
"SG_")}) {
668 fixture.Write(
"NS_:\n\tBA_\n\t" + definition +
'\n');
670 CHECK(result.
HasErrors(),
"Malformed definition ends NS_ instead of being hidden");
671 QCOMPARE(result.
diagnostics.size(), std::size_t(1U));
672 QCOMPARE(result.
diagnostics[0].lineNumber, std::size_t(3U));
676void DbcParserTest::diagnosticFormatting() {
678 fixture.path +=
" with spaces";
680 for (
const std::string &text : {std::string(), std::string(
" \t\r\n\n")}) {
684 "Empty and whitespace-only inputs are valid");
686 fixture.Write(
"\r\n\tVENDOR_EXTENSION_ data;\r\n\r\nUNKNOWN_");
687 const std::string relativePath = std::filesystem::relative(fixture.path).string();
690 QCOMPARE(warnings.
diagnostics.size(), std::size_t(2U));
691 QCOMPARE(warnings.
diagnostics[0].lineNumber, std::size_t(2U));
692 QCOMPARE(warnings.
diagnostics[1].lineNumber, std::size_t(4U));
694 QCOMPARE(warning.filePath, relativePath);
696 fixture.Write(
"UNKNOWN_ first;\nBO_ invalid Frame: 1 ECU\n"
697 "UNKNOWN_ after_error;\nSG_ broken\n");
700 QCOMPARE(stopped.
diagnostics.size(), std::size_t(2U));
701 QCOMPARE(stopped.
diagnostics[0].lineNumber, std::size_t(1U));
702 QCOMPARE(stopped.
diagnostics[1].lineNumber, std::size_t(2U));
703 QCOMPARE(stopped.
diagnostics[1].filePath, relativePath);
705 fixture.Write(
"VAL_ 1 Missing 0 \"Off\";\nBO_ 1 Frame: 1 ECU\nUNKNOWN_ data;\n");
708 QCOMPARE(deferred.
diagnostics.size(), std::size_t(2U));
709 CHECK(deferred.
diagnostics[0].severity == DbcDiagnosticSeverity::Warning &&
710 deferred.
diagnostics[1].severity == DbcDiagnosticSeverity::Error,
711 "Deferred errors follow warnings in detection order");
712 QCOMPARE(deferred.
diagnostics[0].lineNumber, std::size_t(3U));
713 QCOMPARE(deferred.
diagnostics[1].lineNumber, std::size_t(1U));
714 QCOMPARE(deferred.
diagnostics[1].filePath, relativePath);
715 fixture.Write(
"BO_ 1 Frame: 1 ECU");
718 "Same parser has clean state after errors and accepts missing final newline");
721void DbcParserTest::validFrames() {
729 const ValidCase valid[] = {
730 {
"BO_ 0 Frame: 0 ECU", 0U,
false, 0U},
731 {
"BO_ 2047 Frame: 8 ECU", 2047U,
false, 8U},
732 {
"BO_ 2048 Frame: 64 ECU", 2048U,
true, 64U},
733 {
"BO_ 536870911 Frame: 255 ECU", 0x1FFFFFFFU,
true, 255U},
734 {
"BO_ 2147483649 Frame: 8 ECU", 1U,
true, 8U},
735 {
"BO_ 2684354559 Frame: 8 ECU", 0x1FFFFFFFU,
true, 8U},
736 {
" BO_\t256\tFrame :\t8\tECU \r", 256U,
false, 8U}};
737 for (
const ValidCase &test: valid) {
738 fixture.Write(std::string(test.line) +
'\n');
741 QCOMPARE(database.
frames.size(), std::size_t(1U));
743 QCOMPARE(frame.
name, std::string(
"Frame"));
744 QCOMPARE(frame.
canId, test.id);
746 QCOMPARE(
static_cast<unsigned>(frame.
dlc), test.length);
748 }
catch (
const std::exception &error) { CHECK(
false, error.what()); }
752void DbcParserTest::invalidFrames() {
754 const char *invalid[] = {
755 "BO_ invalid",
"BO_",
"BO_ 1",
"BO_ 1 : 8 ECU",
"BO_ 1 Frame 8 ECU",
756 "BO_ 1 Frame:",
"BO_ 1 Frame: 8",
"BO_ 1 Frame: 8 ECU extra",
757 "BO_ 1 Two Names: 8 ECU",
"BO_ -1 Frame: 8 ECU",
"BO_ +1 Frame: 8 ECU",
758 "BO_ 1x Frame: 8 ECU",
"BO_ 0x123 Frame: 8 ECU",
"BO_ 1.5 Frame: 8 ECU",
759 "BO_ 4294967296 Frame: 8 ECU",
"BO_ 999999999999999999999 Frame: 8 ECU",
760 "BO_ 536870912 Frame: 8 ECU",
"BO_ 1073741824 Frame: 8 ECU",
761 "BO_ 2684354560 Frame: 8 ECU",
"BO_ 4294967295 Frame: 8 ECU",
762 "BO_ 1 Frame: -1 ECU",
"BO_ 1 Frame: +8 ECU",
"BO_ 1 Frame: 8x ECU",
763 "BO_ 1 Frame: 8.5 ECU",
"BO_ 1 Frame: 256 ECU",
764 "BO_ 1 Frame: 4294967296 ECU",
"BO_\tinvalid"};
765 for (
const char *line: invalid) {
766 fixture.Write(std::string(
"VERSION \"test\"\n\n") + line +
'\n');
770 }
catch (
const std::runtime_error &error) {
771 const std::string prefix = fixture.path.string() +
":3: ";
772 CHECK(std::string(error.what()).find(prefix) == 0U &&
773 std::string(error.what()).size() > prefix.size(),
779void DbcParserTest::duplicateFrames() {
781 struct DuplicateCase {
783 const char *duplicateId;
786 const DuplicateCase cases[] = {
787 {
"1",
"1",
"Duplicate standard frame CAN ID: 1"},
788 {
"2147483649",
"2147483649",
"Duplicate extended frame CAN ID: 1"},
789 {
"2048",
"2147485696",
"Duplicate extended frame CAN ID: 2048"},
790 {
"2147485696",
"2048",
"Duplicate extended frame CAN ID: 2048"},
791 {
"536870911",
"2684354559",
"Duplicate extended frame CAN ID: 536870911"}
794 for (
const DuplicateCase &test : cases) {
795 fixture.Write(
"BO_ " + std::string(test.firstId) +
" First: 8 ECU\n"
796 "BO_ 2 Other: 8 ECU\n\n"
797 " BO_\t" + test.duplicateId +
"\tSecond: 8 ECU\n");
800 CHECK(
false,
"Duplicate frame must be rejected");
801 }
catch (
const std::runtime_error &error) {
802 QCOMPARE(std::string(error.what()), fixture.path.string() +
":4: " + test.message);
805 fixture.Write(
"BO_ 1 First: 8 ECU\n");
806 QCOMPARE(parser.
ParseFile(fixture.path.string()).
frames.size(), std::size_t(1U));
809void DbcParserTest::duplicateSignals() {
811 const std::string first =
"SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n";
812 const std::string duplicates[] = {
814 " SG_\tState : 16|8@1+ (2,1) [0|511] \"V\" ECU\n",
815 "SG_ State m1 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
817 for (
const std::string &duplicate : duplicates) {
818 fixture.Write(
"BO_ 1 Frame: 8 ECU\n" + first +
819 "SG_ Other : 8|8@1+ (1,0) [0|255] \"\" ECU\n\n" + duplicate);
822 CHECK(
false,
"Duplicate signal must be rejected");
823 }
catch (
const std::runtime_error &error) {
824 QCOMPARE(std::string(error.what()), fixture.path.string() +
825 ":5: Duplicate signal name 'State' in frame 'Frame'.");
830void DbcParserTest::distinctDefinitions() {
832 fixture.Write(
"BO_ 1 Shared: 1 ECU\n"
833 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
834 "BO_ 2147483649 Shared: 1 ECU\n"
835 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
836 "BO_ 2 Shared: 2 ECU\n"
837 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
838 "SG_ state : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
839 "CM_ BO_ 1 \"Standard\";\n"
840 "CM_ BO_ 2147483649 \"Extended\";\n"
841 "CM_ SG_ 1 State \"Standard state\";\n"
842 "CM_ SG_ 2147483649 State \"Extended state\";\n");
844 QCOMPARE(parsed.
frames.size(), std::size_t(3U));
845 QCOMPARE(parsed.
frames[0].canId, parsed.
frames[1].canId);
846 CHECK(!parsed.
frames[0].isExtended && parsed.
frames[1].isExtended,
847 "Standard and extended frames with the same numeric ID remain distinct");
848 QCOMPARE(parsed.
frames[0].comment, std::string(
"Standard"));
849 QCOMPARE(parsed.
frames[1].comment, std::string(
"Extended"));
850 QCOMPARE(parsed.
frames[0].signals[0].comment, std::string(
"Standard state"));
851 QCOMPARE(parsed.
frames[1].signals[0].comment, std::string(
"Extended state"));
852 QCOMPARE(parsed.
frames[2].signals.size(), std::size_t(2U));
858 QVERIFY(standard.
valid);
859 QCOMPARE(standard.
definition->comment, std::string(
"Standard"));
862 QVERIFY(extended.
valid);
863 QCOMPARE(extended.
definition->comment, std::string(
"Extended"));
866void DbcParserTest::comments() {
869 "CM_\tBO_\t1\t\"First\";\r\n"
870 "CM_\tSG_\t1\tState\t\" Line one \r\n"
872 "\tBO_ is text; \\\"quoted\\\" and \\\\slash\"\t;\r\n"
873 "CM_ BO_ 2147483649 \"Extended\";\r\n"
874 "CM_ SG_ 2147483649 State \"Extended signal\";\r\n"
875 "BO_ 1 Standard: 1 ECU\r\n"
876 "CM_ BO_ 1 \"Last\";\r\n"
877 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\r\n"
878 "BO_ 2147483649 Extended: 1 ECU\r\n"
879 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU");
886 std::string(
" Line one \n\n\tBO_ is text; \"quoted\" and \\slash"));
887 QCOMPARE(result.
database.
frames[1].comment, std::string(
"Extended"));
888 QCOMPARE(result.
database.
frames[1].signals[0].comment, std::string(
"Extended signal"));
890 const std::string ids[][2] = {
893 {
"2048",
"2147485696"},
894 {
"536870911",
"2684354559"},
895 {
"2684354559",
"536870911"}};
896 for (
const std::string(&
id)[2]: ids) {
897 fixture.Write(
"CM_ BO_ " +
id[0] +
" \"Frame\";\nCM_ SG_ " +
id[0] +
898 " State \"Signal\";\nBO_ " +
id[1] +
" Frame: 1 ECU\n"
899 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n");
901 QCOMPARE(database.
frames[0].comment, std::string(
"Frame"));
902 QCOMPARE(database.
frames[0].signals[0].comment, std::string(
"Signal"));
904 fixture.Write(
"CM_ BO_ 1 \"Line one\nLine two\";\n"
905 "BO_ 1 Frame: 0 ECU\nUNKNOWN_ value\nBO_ bad Frame: 0 ECU\n");
908 QCOMPARE(following.
diagnostics.size(), std::size_t(2));
909 QCOMPARE(following.
diagnostics[0].lineNumber, std::size_t(4));
910 QCOMPARE(following.
diagnostics[1].lineNumber, std::size_t(5));
913void DbcParserTest::invalidComments_data() {
914 QTest::addColumn<QString>(
"statement");
915 const char *ids[] = {
"-1",
"+1",
"1x",
"0x1",
"4294967296",
"536870912",
916 "1073741824",
"2684354560",
"4294967295"};
917 for (
const char *
id: ids) {
918 const std::string frameRow = std::string(
"frame-id-") + id;
919 const std::string signalRow = std::string(
"signal-id-") + id;
920 QTest::newRow(frameRow.c_str()) << QString::fromStdString(
"CM_ BO_ " + std::string(
id) +
" \"Text\";");
921 QTest::newRow(signalRow.c_str()) << QString::fromStdString(
"CM_ SG_ " + std::string(
id) +
" State \"Text\";");
923 QTest::newRow(
"missing-id") << QStringLiteral(
"CM_ BO_");
924 QTest::newRow(
"missing-signal") << QStringLiteral(
"CM_ SG_ 1 \"Text\";");
925 QTest::newRow(
"missing-quote") << QStringLiteral(
"CM_ BO_ 1 Text;");
926 QTest::newRow(
"unclosed") << QStringLiteral(
"CM_ SG_ 1 State \"Text\n More");
927 QTest::newRow(
"invalid-escape") << QStringLiteral(
"CM_ BO_ 1 \"bad\\q\";");
928 QTest::newRow(
"dangling-escape") << QStringLiteral(
"CM_ BO_ 1 \"bad\\");
929 QTest::newRow(
"missing-semicolon") << QStringLiteral(
"CM_ SG_ 1 State \"Text\"");
930 QTest::newRow(
"trailing-text") << QStringLiteral(
"CM_ BO_ 1 \"Text\"; garbage");
931 QTest::newRow(
"extra-header") << QStringLiteral(
"CM_ BO_ 1 extra \"Text\";");
932 QTest::newRow(
"extra-string") << QStringLiteral(
"CM_ BO_ 1 \"Text\" \"More\";");
933 QTest::newRow(
"missing-frame") << QStringLiteral(
"CM_ BO_ 2 \"Text\";");
934 QTest::newRow(
"missing-signal-frame") << QStringLiteral(
"CM_ SG_ 2 State \"Text\";");
935 QTest::newRow(
"missing-signal-reference") << QStringLiteral(
"CM_ SG_ 1 Missing \"Text\";");
936 QTest::newRow(
"different-frame-type") << QStringLiteral(
"CM_ BO_ 2147483649 \"Text\";");
937 QTest::newRow(
"missing-multiline-reference") << QStringLiteral(
"CM_ SG_ 1 Missing \"Text\nMore\";");
940void DbcParserTest::invalidComments() {
941 QFETCH(QString, statement);
943 fixture.Write(
"BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n\n" +
944 statement.toStdString());
948 QCOMPARE(result.
diagnostics.size(), std::size_t(1));
950 QCOMPARE(error.
severity, DbcDiagnosticSeverity::Error);
951 QCOMPARE(error.
code, DbcDiagnosticCode::InvalidStatement);
952 QCOMPARE(error.
filePath, fixture.path.string());
954 QVERIFY(!error.
message.empty());
957 QFAIL(
"Malformed or unresolved comment must throw.");
958 }
catch (
const std::runtime_error &exception) {
959 QVERIFY(std::string(exception.what()).find(fixture.path.string() +
":4: ") == 0U);
963void DbcParserTest::integration() {
966 "BO_TX_BU_ 256 : ECU;\nBO_ 256 Frame: 8 ECU\n"
967 " SG_ Speed : 0|16@1+ (0.5,1) [0|100] \"rpm\" ECU\n"
968 "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Speed \"Signal comment\";\n"
969 "BO_ 257 Second: 8 ECU\n");
971 CHECK(database.
frames.size() == 2U && database.
frames[0].signals.size() == 1U &&
972 database.
frames[0].comment ==
"Frame comment" &&
973 database.
frames[0].signals[0].comment ==
"Signal comment",
975 fixture.Write(
"\nSG_ Speed : invalid\n");
978 CHECK(
false,
"Signal error location");
979 }
catch (
const std::runtime_error &error) {
980 CHECK(std::string(error.what()).find(fixture.path.string() +
":2: ") == 0U,
981 "Signal error location");
985void DbcParserTest::multiplexing() {
987 const std::string selector =
"SG_ Mode M : 0|8@1+ (10,3) [0|255] \"\" ECU\n";
988 const std::string first =
"SG_ First m1 : 8|8@1- (2,0) [-128|127] \"V\" ECU\n";
989 const std::string second =
"SG_ Second m2 : 15|8@0+ (1,0) [0|255] \"\" ECU\n";
990 const std::string common =
"SG_ Common : 16|8@1+ (1,0) [0|255] \"\" ECU\n";
991 fixture.Write(
"BO_ 1 Frame: 3 ECU\n" + first + second + common + selector +
992 "VAL_ 1 First -1 \"Negative\";\n");
995 CHECK(parsed.
frames[0].signals[3].multiplexing.role == MultiplexRole::Selector,
996 "Selector declared after branches");
998 CHECK(tree->GetChild(0U)->GetChild(0U)->GetSignal()->multiplexing.value == 1U,
999 "Tree preserves multiplexing");
1002 raw.
data = {1U, 255U, 7U};
1004 CHECK(result.signals[0].
valid && std::get<std::int64_t>(result.signals[0].rawValue) == -1 &&
1005 result.signals[0].physicalValue == -2 && *result.signals[0].GetValueDescription() ==
"Negative",
1006 "Active signed branch and VAL_ use exact raw selector");
1007 CHECK(!result.signals[1].
valid && result.signals[1].
status == SignalDecodeStatus::Inactive &&
1008 result.signals[1].GetValueDescription() ==
nullptr && result.signals[2].
valid,
1009 "Inactive branch and unconditional signal");
1012 CHECK(result.signals[0].
status == SignalDecodeStatus::Inactive && result.signals[1].
valid &&
1013 std::get<std::uint64_t>(result.signals[1].rawValue) == 255U,
1014 "Motorola branch becomes active");
1017 CHECK(result.signals[0].
status == SignalDecodeStatus::Inactive &&
1018 result.signals[1].
status == SignalDecodeStatus::Inactive &&
1019 result.signals[2].
status == SignalDecodeStatus::InsufficientPayload,
1020 "Unmapped selector leaves branches inactive even with missing bytes");
1023 CHECK(result.signals[0].
status == SignalDecodeStatus::InsufficientPayload,
1024 "Active branch requires payload");
1027 CHECK(result.signals[0].
status == SignalDecodeStatus::SelectorUnavailable &&
1028 result.signals[3].
status == SignalDecodeStatus::InsufficientPayload,
1029 "Unavailable selector does not imply inactive branch");
1031 const std::vector<std::string> invalid = {
1033 selector +
"SG_ Other M : 16|8@1- (1,0) [-128|127] \"\" ECU\n",
1034 "SG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
1035 selector +
"SG_ TooLarge m256 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
1036 selector + first +
"SG_ Same m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n",
1037 selector +
"SG_ Collision m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
1038 selector + first +
"SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
1039 "SG_MUL_VAL_ 1 First Mode 1-2;\n"};
1040 for (
const std::string &signals : invalid) {
1041 fixture.Write(
"BO_ 1 Frame: 3 ECU\n" + signals);
1042 bool rejected =
false;
1045 }
catch (
const std::runtime_error &error) {
1046 rejected = std::string(error.what()).find(fixture.path.string() +
":") == 0U;
1048 CHECK(rejected,
"Invalid multiplexing rejected with location");
1050 for (
const std::string marker : {
"m",
"m-1",
"m+1",
"m1M",
"m18446744073709551616",
"M m1"})
1051 RejectSignal(fixture,
"SG_ Bad " + marker +
" : 8|8@1+ (1,0) [0|255] \"\" ECU");
1054void DbcParserTest::multiplexingBoundaries() {
1056 fixture.Write(
"BO_ 1 Frame: 9 ECU\n"
1057 "SG_ Branch m18446744073709551615 : 64|8@1+ (1,0) [0|255] \"\" ECU\n"
1058 "SG_ Mode M : 7|64@0+ (0,5) [0|1] \"\" ECU\n");
1061 CHECK(runtime.
GetFrames()[0].signals[0].multiplexing.value == std::numeric_limits<std::uint64_t>::max(),
1062 "Exact uint64 branch metadata");
1065 raw.
data = std::vector<std::uint8_t>(9U, 255U);
1067 CHECK(result.signals[0].
valid && result.signals[1].
valid &&
1068 std::get<std::uint64_t>(result.signals[1].rawValue) == std::numeric_limits<std::uint64_t>::max(),
1069 "Motorola uint64 selector compares raw value before zero scaling");
1072 CHECK(result.signals[0].
status == SignalDecodeStatus::Inactive,
1073 "Adjacent large integers remain distinct");
1074 std::vector<DecodeFrame> definitions = runtime.
GetFrames();
1075 definitions[0].signals[1].length = 0U;
1076 QVERIFY_THROWS_EXCEPTION(std::invalid_argument, runtime =
DecodeDatabase(definitions));
1077 definitions[0].signals.pop_back();
1078 QVERIFY_THROWS_EXCEPTION(std::invalid_argument, runtime =
DecodeDatabase(definitions));
1079 fixture.Write(
"BO_ 1 Frame: 2 ECU\n\n"
1080 "SG_ Branch m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1081 "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n");
1082 bool rejected =
false;
1085 }
catch (
const std::runtime_error &error) {
1086 rejected = std::string(error.what()).find(fixture.path.string() +
":3: ") == 0U;
1088 CHECK(rejected,
"Deferred validation reports branch source line");
1089 fixture.Write(
"BO_ 1 Frame: 2 ECU\n"
1090 "SG_ A : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1091 "SG_ B : 15|8@0+ (1,0) [0|255] \"\" ECU\n");
1095 }
catch (
const std::runtime_error &) {
1098 CHECK(rejected,
"Ordinary overlapping signals rejected");
1101void DbcParserTest::invalidSignals() {
1103 const std::string ordinary = SignalLine(
"0",
"8",
"1+",
"1",
"0",
"0",
"255");
1104 for (
const std::string value: {
"-1",
"+1",
"0junk",
"0x10",
"1.5",
1105 "4294967296",
"999999999999999999999"}) {
1106 RejectSignal(fixture, SignalLine(value,
"8",
"1+",
"1",
"0",
"0",
"255"));
1107 RejectSignal(fixture, SignalLine(
"0", value,
"1+",
"1",
"0",
"0",
"255"));
1109 for (
const std::string length: {
"0",
"65"})
1110 RejectSignal(fixture, SignalLine(
"0", length,
"1+",
"1",
"0",
"0",
"255"));
1111 for (
const std::string order: {
"2+",
"9-",
"1",
"1x",
"1++",
"01+"})
1112 RejectSignal(fixture, SignalLine(
"0",
"8", order,
"1",
"0",
"0",
"255"));
1113 for (
const std::string value: {
"1junk",
"nan",
"NaN",
"inf",
"-inf",
"1e999",
"1e-999",
"0x1p2",
"",
"1e",
"1.2.3"}) {
1114 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+", value,
"0",
"0",
"255"));
1115 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1", value,
"0",
"255"));
1116 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1",
"0", value,
"255"));
1117 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1",
"0",
"0", value));
1119 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1",
"0",
"2",
"1"));
1120 for (
const std::string name: {
"",
"Two Names",
"Value m1M"}) {
1121 std::string line = ordinary;
1122 line.replace(4U, 5U, name);
1123 RejectSignal(fixture, line);
1125 for (
const std::string broken: {
1126 "SG_",
"SG_ Value",
"SG_ Value : 0|8@1+ (1,0) [0|255] ECU",
1127 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\"",
1128 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU,",
1129 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU extra"})
1130 RejectSignal(fixture, broken);
1131 RejectSignal(fixture, SignalLine(
"64",
"1",
"1+",
"1",
"0",
"0",
"1"));
1132 RejectSignal(fixture, SignalLine(
"63",
"2",
"1+",
"1",
"0",
"0",
"3"));
1133 RejectSignal(fixture, SignalLine(
"56",
"2",
"0+",
"1",
"0",
"0",
"3"));
1134 RejectSignal(fixture, SignalLine(
"4294967295",
"64",
"1+",
"1",
"0",
"0",
"1"));
1135 RejectSignal(fixture, ordinary, 0U);
1136 for (
const char delimiter: {
'|',
'@',
'(',
')',
'[',
']',
'"'}) {
1137 std::string line = ordinary;
1138 line.erase(line.find(delimiter), 1U);
1139 RejectSignal(fixture, line);
1143void DbcParserTest::validSignals() {
1146 fixture.Write(
"BO_ 1 Frame: 8 ECU\n" +
1147 SignalLine(
"0",
"8",
"1+",
"0",
"0",
"0",
"0") +
"\n" +
1148 SignalLine(
"8",
"8",
"1-",
"-1",
"0",
"-255",
"0",
"Negative") +
"\n");
1150 CHECK(factors.
frames[0].signals.size() == 2U &&
1151 factors.
frames[0].signals[0].factor == 0.0 &&
1152 factors.
frames[0].signals[1].factor == -1.0,
1153 "Zero and negative factors and equal range boundaries");
1161 const SignalCase validSignals[] = {
1162 {
"0",
"1",
"1+", 1U},
1163 {
"7",
"1",
"0-", 1U},
1164 {
"0",
"64",
"1+", 8U},
1165 {
"7",
"64",
"0-", 8U},
1166 {
"63",
"1",
"1+", 8U},
1167 {
"56",
"1",
"0+", 8U},
1168 {
"3",
"12",
"0+", 2U},
1169 {
"2039",
"1",
"1+", 255U}};
1170 for (
const SignalCase &test: validSignals) {
1171 fixture.Write(
"BO_ 1 Frame: " + std::to_string(test.payload) +
" ECU\n" +
1172 SignalLine(test.start, test.length, test.order,
".5e+1",
"-2.5E-1",
"-1.",
"+2e2") +
"\n");
1176 QCOMPARE(signal.
startBit,
static_cast<std::uint32_t
>(std::stoul(test.start)));
1177 QCOMPARE(signal.
length,
static_cast<std::uint32_t
>(std::stoul(test.length)));
1179 QCOMPARE(signal.
isSigned, test.order[1] ==
'-');
1180 QCOMPARE(signal.
factor, 5.0);
1181 QCOMPARE(signal.
offset, -0.25);
1182 QCOMPARE(signal.
minimum, -1.0);
1183 QCOMPARE(signal.
maximum, 200.0);
1184 }
catch (
const std::exception &error) { CHECK(
false, error.what()); }
1186 fixture.Write(
"BO_ 1 Frame: 8 ECU\n"
1187 "SG_\tValue\t:\t0 | 8 @1+ ( 1 , 0 ) [ 0 | 255 ] \"rpm\" ECU1,\tECU2\n");
1191 CHECK(signal.
name ==
"Value" && signal.
unit ==
"rpm" &&
1192 signal.
receivers == std::vector<std::string>({
"ECU1",
"ECU2"}),
1193 "Signal tabs, spaces, and receiver list");
1194 }
catch (
const std::exception &error) { CHECK(
false, error.what()); }
1197void DbcParserTest::longSignalDefinitions_data() {
1198 QTest::addColumn<std::string>(
"line");
1199 QTest::addColumn<bool>(
"valid");
1200 QTest::addColumn<std::string>(
"unit");
1201 const std::string longText(50000U,
'x');
1202 const std::string prefix =
"SG_ Value : 0|8@1+ (1,0) [0|255] ";
1203 QTest::newRow(
"long-unit") << prefix +
"\"" + longText +
"\" ECU" <<
true << longText;
1204 QTest::newRow(
"unterminated-unit") << prefix +
"\"" + longText <<
false << std::string();
1205 QTest::newRow(
"missing-receiver-separator") << prefix +
"\"" + longText +
"\"ECU" <<
false << std::string();
1206 QTest::newRow(
"long-name") << SignalLine(
"0",
"8",
"1+",
"1",
"0",
"0",
"255", longText) <<
true << std::string();
1207 QTest::newRow(
"long-receiver") << prefix +
"\"\" " + longText <<
true << std::string();
1208 std::string receivers =
"ECU";
1209 for (
unsigned index = 0U; index < 10000U; ++index)
1210 receivers +=
",ECU";
1211 QTest::newRow(
"many-receivers") << prefix +
"\"\" " + receivers <<
true << std::string();
1212 QTest::newRow(
"trailing-receiver-comma") << prefix +
"\"\" " + receivers +
"," <<
false << std::string();
1213 QTest::newRow(
"long-factor") << SignalLine(
"0",
"8",
"1+", std::string(50000U,
'0') +
"1",
"0",
"0",
"255") <<
true << std::string();
1214 QTest::newRow(
"invalid-long-factor") << SignalLine(
"0",
"8",
"1+", std::string(50000U,
'0') +
"1x",
"0",
"0",
"255") <<
false << std::string();
1215 QTest::newRow(
"overflowing-start-bit") << SignalLine(std::string(50000U,
'9'),
"8",
"1+",
"1",
"0",
"0",
"255") <<
false << std::string();
1216 QTest::newRow(
"missing-colon") <<
"SG_ " + longText <<
false << std::string();
1219void DbcParserTest::longSignalDefinitions() {
1220 QFETCH(std::string, line);
1221 QFETCH(
bool, valid);
1222 QFETCH(std::string, unit);
1224 fixture.Write(
"BO_ 1 Frame: 1 ECU\n" + line +
"\n");
1231 QCOMPARE(database.
frames.size(), std::size_t(1));
1232 QCOMPARE(database.
frames[0].signals.size(), std::size_t(1));
1233 QCOMPARE(database.
frames[0].signals[0].unit, unit);
1235 QCOMPARE(database.
frames[0].signals[0].factor, 1.0);
1236 QVERIFY(!database.
frames[0].signals[0].receivers.empty());
1239 QCOMPARE(result.
diagnostics.size(), std::size_t(1));
1240 QCOMPARE(result.
diagnostics[0].code, DbcDiagnosticCode::InvalidStatement);
1241 QCOMPARE(result.
diagnostics[0].lineNumber, std::size_t(2));
1242 QCOMPARE(result.
diagnostics[0].filePath, fixture.path.string());
1243 RejectSignal(fixture, line, 1U);
1247void DbcParserTest::CheckPhysicalNumbers() {
1249 struct PhysicalCase {
1252 const char *minimum;
1253 const char *maximum;
1254 double expectedFactor;
1255 double expectedOffset;
1256 double expectedMinimum;
1257 double expectedMaximum;
1259 const PhysicalCase valid[] = {
1260 {
"+1.25",
"-0.5",
"-2.75",
"255.5", 1.25, -0.5, -2.75, 255.5},
1261 {
"+.5e+1",
"-2.5E-1",
"-1.",
"+2e2", 5.0, -0.25, -1.0, 200.0},
1262 {
"0e-999",
"+0.",
"-.0",
".0e+999", 0.0, 0.0, 0.0, 0.0},
1263 {
"1.7976931348623157e308",
"2.2250738585072014e-308",
"-1e308",
"+1e308",
1264 std::numeric_limits<double>::max(), std::numeric_limits<double>::min(), -1e308, 1e308}};
1265 for (
const PhysicalCase &test: valid) {
1266 fixture.Write(
"BO_ 1 Frame: 1 ECU\n" +
1267 SignalLine(
"0",
"8",
"1+", test.factor, test.offset, test.minimum, test.maximum) +
"\n");
1273 QCOMPARE(signal.
factor, test.expectedFactor);
1274 QCOMPARE(signal.
offset, test.expectedOffset);
1275 QCOMPARE(signal.
minimum, test.expectedMinimum);
1276 QCOMPARE(signal.
maximum, test.expectedMaximum);
1279 const char *invalid[] = {
"1,5",
"1.234,5",
"1,234.5",
"1junk",
"nan",
"inf",
"-inf",
1280 "1e999",
"1e-999",
"1e-320",
"-1e-320",
"0x1p2",
"1e",
"1.2.3"};
1281 for (
const char *value: invalid) {
1282 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+", value,
"0",
"0",
"255"));
1283 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1", value,
"0",
"255"));
1284 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1",
"0", value,
"255"));
1285 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1",
"0",
"0", value));
1287 RejectSignal(fixture, SignalLine(
"0",
"8",
"1+",
"1.25",
"0",
"2.5",
"1.5"));
1290void DbcParserTest::physicalNumbersCppLocale() {
1291 const ApplicationLocaleGuard guard;
1292 std::locale::global(std::locale(std::locale::classic(),
new CommaDecimalPunctuation));
1293 QCOMPARE(std::use_facet<std::numpunct<char>>(std::locale()).decimal_point(),
',');
1294 CheckPhysicalNumbers();
1295 QCOMPARE(std::use_facet<std::numpunct<char>>(std::locale()).decimal_point(),
',');
1298void DbcParserTest::physicalNumbersCLocale() {
1299 const ApplicationLocaleGuard guard;
1301 const char *names[] = {
"de_DE.UTF-8",
"de_DE.utf8",
"fr_FR.UTF-8",
"fr_FR.utf8",
1302 "German_Germany.1252",
"French_France.1252"};
1303 for (
const char *name: names) {
1304 if (std::setlocale(LC_NUMERIC, name) && std::string(std::localeconv()->decimal_point) ==
",") {
1310 QSKIP(
"No named comma-decimal C locale is installed; generate de_DE.UTF-8 and set LOCPATH.");
1311 QCOMPARE(std::string(std::localeconv()->decimal_point), std::string(
","));
1312 CheckPhysicalNumbers();
1313 QCOMPARE(std::string(std::localeconv()->decimal_point), std::string(
","));
1316void DbcParserTest::dispatch() {
1317 CountingHandler::instances = 0;
1318 CountingHandler::calls = 0;
1323 CHECK(dispatcher.
Dispatch(
"TEST_ first", context) &&
1324 dispatcher.
Dispatch(
"TEST_ second", context) &&
1325 CountingHandler::instances == 1 && CountingHandler::calls == 2 &&
1326 dispatchDatabase.
frames.size() == 2U,
1327 "Factory dispatch and handler reuse");
1328 CHECK(!dispatcher.
Dispatch(
"TEST_OTHER data", context) &&
1329 !dispatcher.
Dispatch(
"BO_TX_BU_ 1 : ECU;", context) &&
1330 !dispatcher.
Dispatch(
"VAL_TABLE_ States 0 \"Stopped\";", context) &&
1331 CountingHandler::calls == 2,
1332 "Exact and unknown keyword dispatch");
1335void DbcParserTest::independentSessions() {
1338 fixture.Write(
"BO_ 1 First: 8 ECU\n");
1339 CHECK(parser.
ParseFile(fixture.path.string()).
frames.size() == 1U,
"First parse");
1340 fixture.Write(
"SG_ Speed : invalid\n");
1342 parser.
ParseFile(fixture.path.string());
1343 CHECK(
false,
"Independent parse context");
1344 }
catch (
const std::runtime_error &error) {
1345 CHECK(std::string(error.what()).find(
"Signal found before any frame") !=
1347 "Independent parse context");
1349 fixture.Write(
"BO_ 2 Second: 8 ECU\n");
1351 CHECK(recovered.
frames.size() == 1U && recovered.
frames[0].canId == 2U,
1352 "Fresh parse after error");
1356void DbcParserTest::valueDescriptions() {
1359 "VAL_\t256\tState\t0\t\"Off\"\t1 \"On\" 2 \"Error with spaces; and \\\"quotes\\\"\";\n"
1360 "BO_ 256 Status: 8 ECU\n"
1361 "SG_ State : 0|8@1+ (2,10) [0|520] \"\" ECU\n"
1362 "VAL_ 256 State 3 \"\";\n"
1363 "VAL_ 2147483904 State -9223372036854775808 \"Minimum\" -1 \"Negative\" 9223372036854775807 \"Maximum\";\n"
1364 "BO_ 2147483904 Extended: 8 ECU\n"
1365 "SG_ State : 0|64@1- (1,0) [-1e20|1e20] \"\" ECU\n"
1366 "BO_ 2048 Unsigned: 8 ECU\n"
1367 "SG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n"
1368 "VAL_ 2147485696 State 9223372036854775808 \"High\" 18446744073709551615 \"Full\";\n");
1372 CHECK(tree->GetChild(0)->GetSignal() ==
nullptr,
"Frame node");
1373 for (std::size_t index = 0; index < parsed.
frames.size(); ++index) {
1374 CHECK(tree->GetChild(index)->GetChild(0)->GetSignal()->valueDescriptions ==
1375 parsed.
frames[index].signals[0].valueDescriptions &&
1376 runtime.
GetFrames()[index].signals[0].valueDescriptions ==
1377 parsed.
frames[index].signals[0].valueDescriptions,
1378 "All mappings survive both builders");
1380 CHECK(tree->GetChild(0)->GetChild(0)->GetSignal()->valueDescriptions ==
1381 runtime.
GetFrames()[0].signals[0].valueDescriptions,
1382 "Both builders preserve descriptions");
1385 frame.
data.resize(8);
1386 const std::string expected[] = {
"Off",
"On",
"Error with spaces; and \"quotes\"",
""};
1387 for (
unsigned value = 0; value < 5; ++value) {
1388 frame.
data[0] = value;
1390 CHECK(decoded.
valid && decoded.
physicalValue == value * 2 + 10,
"Numeric scaling unchanged");
1392 CHECK(value < 4 ? description && *description == expected[value] : !description,
1393 "Raw lookup and unknown value");
1398 const char *description;
1400 for (
const Boundary &test: {Boundary{0x8000000000000000ULL,
"Minimum"},
1401 Boundary{0xFFFFFFFFFFFFFFFFULL,
"Negative"},
1402 Boundary{0x7FFFFFFFFFFFFFFFULL,
"Maximum"}}) {
1403 for (
unsigned byte = 0;
byte < 8; ++byte)
1404 frame.
data[
byte] =
static_cast<std::uint8_t
>(test.bits >> (
byte * 8));
1407 "Signed boundaries and distinct extended frame");
1410 for (
const Boundary &test: {Boundary{0x8000000000000000ULL,
"High"},
1411 Boundary{0xFFFFFFFFFFFFFFFFULL,
"Full"}}) {
1412 for (
unsigned byte = 0;
byte < 8; ++byte)
1413 frame.
data[
byte] =
static_cast<std::uint8_t
>(test.bits >> (
byte * 8));
1416 "Unsigned boundaries");
1418 CHECK(FindValueDescription(runtime.
GetFrames()[0].signals[0].valueDescriptions, std::int64_t(1)) !=
nullptr,
1419 "Nonnegative signed lookup matches unsigned key");
1421 CHECK(!
DbcDecoder().Decode(runtime, frame).signals[0].GetValueDescription(),
"Invalid decode has no description");
1424void DbcParserTest::invalidValueDescriptions() {
1426 const std::string definitions =
"BO_ 1 Frame: 8 ECU\nSG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n";
1427 for (
const std::string statement: {
1428 "VAL_",
"VAL_ 1 State;",
"VAL_ 1 State ;",
"VAL_ 1 State 0 \"Off\"",
1429 "VAL_ 1 State 0 \"Off\"; junk",
"VAL_ 1 State 0 Off;",
1430 "VAL_ 1 State 0 \"Unclosed;",
"VAL_ 1 State 0 \"Off\"1 \"On\";",
1431 "VAL_ 1 State 0 \"Off\" 00 \"Duplicate\";",
1432 "VAL_ 1 State 0 \"Off\" -0 \"Duplicate\";",
1433 "VAL_ 1 State 0 \"Off\";\nVAL_ 1 State 0 \"Duplicate\";",
1434 "VAL_ 2 State 0 \"Missing frame\";",
"VAL_ 1 Missing 0 \"Missing signal\";",
1435 "VAL_ -1 State 0 \"Bad ID\";",
"VAL_ 536870912 State 0 \"Bad ID\";",
1436 "VAL_ 4294967296 State 0 \"Bad ID\";",
"VAL_ Environment 0 \"Unsupported\";",
1437 "VAL_ 1 State 0 \"Bad\\q escape\";"}) {
1438 fixture.Write(definitions + statement +
'\n');
1439 const int line = statement.find(
'\n') == std::string::npos ? 3 : 4;
1442 CHECK(
false, statement);
1443 }
catch (
const std::runtime_error &error) {
1444 CHECK(std::string(error.what()).find(fixture.path.string() +
":" + std::to_string(line) +
": ") == 0,
1445 statement +
" " + error.what());
1448 for (
const std::string suffix: {
1449 "BO_ 1 Other: 8 ECU\n",
1450 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
1452 fixture.Write(
"VAL_ 1 " + std::string(suffix.empty() ?
"Missing" :
"State") +
1453 " 0 \"Description\";\n" + definitions + suffix);
1456 CHECK(
false,
"Missing reference or duplicate definition");
1457 }
catch (
const std::runtime_error &error) {
1458 const std::string line = suffix.empty() ?
"1" :
"4";
1459 CHECK(std::string(error.what()).find(fixture.path.string() +
":" + line +
": ") == 0,
1460 "Error retains the reference or duplicate definition source location");
1463 for (
const std::string number: {
"+1",
"1x",
"0x10",
"1.5",
"1e2",
"--1",
1464 "-9223372036854775809",
"18446744073709551616"}) {
1465 fixture.Write(
"VAL_ 1 State " + number +
" \"Bad\";\n" + definitions);
1468 CHECK(
false, number);
1469 }
catch (
const std::runtime_error &error) {
1470 CHECK(std::string(error.what()).find(fixture.path.string() +
":1: ") == 0, number);
1476void DbcParserTest::extendedMultiplexing() {
1480 "SG_MUL_VAL_ 1 Child Root 1-2, 4-4;\n"
1481 "SG_MUL_VAL_ 1 Leaf Child 3-5;\n"
1482 "SG_MUL_VAL_ 1 Leaf Child 7-7;\n"
1483 "SG_MUL_VAL_ 1 Sibling Root 6-6;\n"
1484 "SG_MUL_VAL_ 1 Other Sibling 3-5;\n"
1485 "BO_ 1 Frame: 4 ECU\n"
1486 "SG_ Leaf m99 : 16|8@1- (2,1) [-128|127] \"\" ECU\n"
1487 "SG_ Other m0 : 23|8@0+ (1,0) [0|255] \"\" ECU\n"
1488 "SG_ Child m1M : 8|8@1+ (10,3) [0|255] \"\" ECU\n"
1489 "SG_ Sibling m6M : 15|8@0+ (1,0) [0|255] \"\" ECU\n"
1490 "SG_ Root M : 0|8@1+ (10,3) [0|255] \"\" ECU\n"
1491 "SG_ Common : 24|8@1+ (1,0) [0|255] \"\" ECU\n");
1495 const Multiplexing &metadata = tree->GetChild(0U)->GetChild(0U)->GetSignal()->multiplexing;
1496 CHECK(metadata.
selector ==
"Child" && metadata.
ranges.size() == 2U &&
1497 metadata.
ranges[1].minimum == 7U,
1498 "Tree preserves ranges and selector name");
1499 CHECK(database.
GetFrames()[0].signals[2].multiplexing.role == MultiplexRole::BranchSelector,
1500 "Runtime preserves nested selector role");
1503 raw.
data = {2U, 4U, 255U, 8U};
1505 CHECK(result.signals[0].
valid && result.signals[0].physicalValue == -1.0 &&
1506 result.signals[1].
status == SignalDecodeStatus::Inactive && result.signals[2].
valid &&
1507 result.signals[3].
status == SignalDecodeStatus::Inactive && result.signals[5].
valid,
1508 "Nested raw conditions select one path and preserve definition order");
1509 const std::vector<SignalKey> selection = {
SignalKey(
FrameKey(1U,
false),
"Leaf")};
1511 CHECK(result.signals.size() == 1U && result.signals[0].
valid,
1512 "Selection reads all ancestors internally");
1513 for (std::uint8_t root: {1U, 2U, 4U}) {
1514 for (std::uint8_t child: {3U, 5U, 7U}) {
1515 raw.
data = {root, child, 9U, 8U};
1516 CHECK(
DbcDecoder().Decode(database, raw, selection).signals[0].valid,
1517 "Inclusive endpoints and separated ranges");
1520 raw.
data = {6U, 4U, 9U, 8U};
1522 CHECK(result.signals[0].
status == SignalDecodeStatus::Inactive && result.signals[1].
valid &&
1523 result.signals[3].
valid,
1524 "Overlapping sibling selector path becomes active");
1526 CHECK(
DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::Inactive,
1527 "Inactive root does not require child selector bytes");
1529 CHECK(
DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::SelectorUnavailable,
1530 "Active root with missing child selector");
1531 raw.
data = {1U, 3U};
1532 CHECK(
DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::InsufficientPayload,
1533 "Active nested leaf needs its bytes");
1534 raw.
data = {1U, 6U};
1535 CHECK(
DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::Inactive,
1536 "Gap between explicit ranges does not fall back to mN");
1538 CHECK(
DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::SelectorUnavailable,
1539 "Missing root selector");
1541 fixture.Write(
"BO_ 1 Frame: 9 ECU\n"
1542 "SG_ Root M : 0|64@1+ (1,0) [0|1] \"\" ECU\n"
1543 "SG_ Leaf m0 : 64|8@1+ (1,0) [0|255] \"\" ECU\n"
1544 "SG_MUL_VAL_ 1 Leaf Root 18446744073709551614-18446744073709551615;\n");
1545 raw.
data.assign(9U, 255U);
1547 "64-bit ranges retain exact UINT64_MAX without expansion");
1549 fixture.Write(
"BO_ 1 Frame: 4 ECU\n"
1550 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1551 "SG_ Second M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1552 "SG_ A m0 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
1553 "SG_ B m0 : 24|8@1+ (1,0) [0|255] \"\" ECU\n"
1554 "SG_MUL_VAL_ 1 A Root 1-1;\nSG_MUL_VAL_ 1 B Second 2-2;\n");
1555 raw.
data = {1U, 2U, 9U, 10U};
1557 CHECK(result.signals[2].
valid && result.signals[3].
valid,
"Independent explicit selectors");
1559 fixture.Write(
"BO_ 1 Deep: 5 ECU\n"
1560 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1561 "SG_ Child m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1562 "SG_ Grandchild m2M : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
1563 "SG_ Leaf : 24|8@1+ (1,0) [0|255] \"\" ECU\n"
1564 "SG_MUL_VAL_ 1 Grandchild Child 2-2;\n"
1565 "SG_MUL_VAL_ 1 Leaf Grandchild 3-3;\n");
1567 raw.
data = {1U, 2U, 3U, 9U, 0U};
1568 CHECK(
DbcDecoder().Decode(deep, raw).signals[3].valid,
"Three selector levels and an unmarked extended branch");
1569 raw.
data = {1U, 0U};
1570 CHECK(
DbcDecoder().Decode(deep, raw).signals[3].status == SignalDecodeStatus::Inactive,
1571 "Inactive middle ancestor skips missing descendant bytes");
1574 fixture.Write(
"BO_ 1 Frame: 3 ECU\n"
1575 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1576 "SG_ Child m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1577 "SG_ Leaf m2 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
1578 "SG_MUL_VAL_ 1 Leaf Child 2-2;\n");
1579 raw.
data = {1U, 2U, 9U};
1581 "mNM retains implicit root fallback");
1584void DbcParserTest::extendedMultiplexingErrors() {
1586 const std::string definitions =
1587 "BO_ 1 Frame: 3 ECU\n"
1588 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1589 "SG_ Child m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1590 "SG_ Leaf m2 : 16|8@1+ (1,0) [0|255] \"\" ECU\n";
1591 for (
const std::string statement: {
1592 "SG_MUL_VAL_ 1 Leaf Root 3-2;",
"SG_MUL_VAL_ 1 Leaf Root -1-2;",
1593 "SG_MUL_VAL_ 1 Leaf Root 1-256;",
"SG_MUL_VAL_ 1 Leaf Root 1-2,;",
1594 "SG_MUL_VAL_ 1 Leaf Root 1-2 trailing;",
"SG_MUL_VAL_ 1 Leaf Root 1-2",
1595 "SG_MUL_VAL_ 1 Leaf Root 0-18446744073709551616;",
1596 "SG_MUL_VAL_ 1 Leaf Missing 1-2;",
"SG_MUL_VAL_ 1 Missing Root 1-2;",
1597 "SG_MUL_VAL_ 2 Leaf Root 1-2;",
"SG_MUL_VAL_ 1 Leaf Leaf 1-2;"}) {
1598 fixture.Write(statement +
"\n" + definitions);
1601 CHECK(result.
diagnostics.back().code == DbcDiagnosticCode::InvalidMultiplexing &&
1603 "Extended error retains source statement location");
1605 for (
const std::string statements: {
1606 "SG_MUL_VAL_ 1 Root Child 1-1;\nSG_MUL_VAL_ 1 Child Root 1-1;\n",
1607 "SG_MUL_VAL_ 1 Child Child 1-1;\n",
1608 "SG_MUL_VAL_ 1 Leaf Root 1-1;\nSG_MUL_VAL_ 1 Leaf Child 1-1;\n"}) {
1609 fixture.Write(definitions + statements);
1610 CHECK(
DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
"Cyclic or conflicting parents rejected");
1613 const std::string overlapping =
1614 "BO_ 1 Frame: 4 ECU\n"
1615 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1616 "SG_ Child m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1617 "SG_ A m0 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
1618 "SG_ B m0 : 23|8@0+ (1,0) [0|255] \"\" ECU\n"
1619 "SG_MUL_VAL_ 1 A Child 2-4;\n"
1620 "SG_MUL_VAL_ 1 Child Root 1-1;\n";
1621 for (
const std::string condition: {
"SG_MUL_VAL_ 1 B Child 4-6;\n",
1622 "SG_MUL_VAL_ 1 B Root 1-1;\n"}) {
1623 fixture.Write(overlapping + condition);
1624 CHECK(
DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
1625 "Overlapping ranges or compatible ancestors cannot share bits");
1627 fixture.Write(overlapping +
"SG_MUL_VAL_ 1 B Child 5-6;\n");
1628 CHECK(!
DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
1629 "Disjoint adjacent ranges may share bits");
1630 fixture.Write(overlapping +
"SG_ Second M : 24|8@1+ (1,0) [0|255] \"\" ECU\n"
1631 "SG_MUL_VAL_ 1 B Second 2-4;\n");
1634 "Signals overlap without mutually exclusive multiplexing conditions.",
1635 "Independent selector conditions do not imply exclusion");
1638 fixture.Write(definitions +
"SG_MUL_VAL_ 1 Leaf Child 2-3;\n");
1640 for (
unsigned kind = 0U; kind < 6U; ++kind) {
1641 std::vector<DecodeFrame> frames = valid.
GetFrames();
1642 Multiplexing &leaf = frames[0].signals[2].multiplexing;
1648 leaf.
ranges[0] = {4U, 3U};
1651 leaf.
ranges[0] = {0U, 256U};
1657 frames[0].signals[0].multiplexing.selector =
"Child";
1658 frames[0].signals[0].multiplexing.ranges = {{1U, 1U}};
1661 frames[0].signals[1].valueType = ValueType::Signed;
1664 QVERIFY_THROWS_EXCEPTION(std::invalid_argument, (
void)
DecodeDatabase(frames));
1668int RunParserTests(
int argc,
char **argv) {
1669 DbcParserTest tests;
1670 return QTest::qExec(&tests, argc, argv);
1673#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.
void RegisterType(const std::string &keyword)
Register or replace a handler and create its reusable instance.
bool Dispatch(const std::string &line, DbcParseContext &context) const
Interface for processing a registered DBC statement.
virtual void Handle(const std::string &line, DbcParseContext &context) const =0
Process a trimmed statement using state belonging to this parse.
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
Runtime decode database builder.
CAN frame decoding API and result types.
DBC file parser and interpretation options.
Parsed DBC content stored in a simple internal form.
std::map< std::string, AttributeDefinition > attributeDefinitions
std::vector< FrameInfo > frames
std::vector< AttributeAssignment > attributeAssignments
Diagnostic message and physical source location.
DbcDiagnosticSeverity severity
Parsed database and diagnostics; the database is empty on error.
std::vector< DbcDiagnostic > diagnostics
bool HasErrors() const
Return whether any diagnostic has error severity.
std::shared_ptr< const DecodeFrame > definition
One decoded signal value.
const std::string * GetValueDescription() const
Describes one CAN frame from a DBC file.
Stable frame identifier: normalized CAN ID and frame type.
Basic markers and optional explicit extended multiplexing condition.
std::vector< MultiplexRange > ranges
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