DBC Framework
DBC parsing and CAN signal decoding framework
Loading...
Searching...
No Matches
dbc_parser_test.cpp
2#include "dbc_decoder.h"
3#include "dbc_parser.h"
4#include "dbc_statement_handler.h"
5#include "dbc_tree_builder.h"
6#include <algorithm>
7#include <clocale>
8#include <limits>
9#include <locale>
10
11#include <QtTest>
12#include <chrono>
13#include <filesystem>
14#include <fstream>
15
16#define CHECK(condition, name) QVERIFY2((condition), std::string(name).c_str())
17#include <stdexcept>
18
19struct Fixture {
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");
22 ~Fixture() {
23 std::error_code error;
24 std::filesystem::remove(path, error);
25 }
26 void Write(const std::string &text) const {
27 std::ofstream output(path);
28 output << text;
29 if (!output)
30 throw std::runtime_error("Cannot write test fixture.");
31 }
32};
33
34class CommaDecimalPunctuation : public std::numpunct<char> {
35protected:
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"; }
39};
40
41class ApplicationLocaleGuard {
42public:
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());
48 }
49
50private:
51 std::locale previousCppLocale;
52 std::string previousCLocale;
53};
54
55class CountingHandler : public DbcStatementHandler {
56public:
57 inline static int instances = 0;
58 inline static int calls = 0;
59 CountingHandler() { ++instances; }
60 void Handle(const std::string &, DbcParseContext &context) const override {
61 ++calls;
62 context.database.frames.emplace_back();
63 }
64};
65
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";
72}
73
74class DbcParserTest : public QObject {
75 Q_OBJECT
76private:
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");
80 try {
81 DbcParser().ParseFile(fixture.path.string());
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(),
87 "Reject " + line);
88 }
89 }
90private Q_SLOTS:
91 void attributes();
92 void multilineAttributes();
93 void invalidMultilineAttributes();
94 void invalidAttributes();
95 void attributeNumbers();
96 void invalidAttributeNumbers_data();
97 void invalidAttributeNumbers();
98 void attributeWarnings();
99 void diagnostics();
100 void diagnosticErrors();
101 void diagnosticUnsupportedStatements();
102 void diagnosticNamespaceBoundaries();
103 void diagnosticFormatting();
104 void multiplexing();
105 void multiplexingBoundaries();
106 void extendedMultiplexing();
107 void extendedMultiplexingErrors();
108 void validFrames();
109 void invalidFrames();
110 void duplicateFrames();
111 void duplicateSignals();
112 void distinctDefinitions();
113 void comments();
114 void invalidComments_data();
115 void invalidComments();
116 void integration();
117 void invalidSignals();
118 void validSignals();
119 void longSignalDefinitions_data();
120 void longSignalDefinitions();
121 void physicalNumbersCppLocale();
122 void physicalNumbersCLocale();
123 void dispatch();
124 void independentSessions();
125 void valueDescriptions();
126 void invalidValueDescriptions();
127};
128
129void DbcParserTest::attributes() {
130 Fixture fixture;
131 fixture.Write(
132 "BA_ \"GenMsgCycleTime\" BO_ 1 25;\n"
133 "BA_ \"VFrameFormat\" BO_ 2147483649 1;\n"
134 "BA_ \"Label\" SG_ 1 Value \"a\\\"b\\\\c\";\n"
135 "BA_ \"NodeLabel\" BU_ ECU \"node\";\n"
136 "BA_ \"Environment\" EV_ Env 2;\n"
137 "BA_DEF_DEF_ \"GenMsgCycleTime\" 100;\n"
138 "BA_DEF_DEF_ \"VFrameFormat\" \"StandardCAN\";\n"
139 "BA_DEF_DEF_ \"Label\" \"default\";\n"
140 "BA_DEF_DEF_ \"BusLabel\" \"CAN\";\n"
141 "BA_DEF_DEF_ \"Ratio\" 1.5e-1;\n"
142 "BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 10000;\n"
143 "BA_DEF_ BO_ \"VFrameFormat\" ENUM \"StandardCAN\",\"ExtendedCAN\",\"StandardCAN_FD\";\n"
144 "BA_DEF_ SG_ \"Label\" STRING;\n"
145 "BA_DEF_ \"BusLabel\" STRING;\n"
146 "BA_DEF_ BO_ \"Ratio\" FLOAT -1 1;\n"
147 "BA_DEF_ BU_ \"NodeLabel\" STRING;\n"
148 "BA_DEF_ EV_ \"Environment\" HEX 0 255;\n"
149 "BO_ 1 Standard: 1 ECU\n"
150 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
151 "BO_ 2147483649 Extended: 1 ECU\n"
152 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n");
153 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
154 QVERIFY(!result.HasErrors());
155 QVERIFY(result.diagnostics.empty());
156 const DbcDatabase &database = result.database;
157 QCOMPARE(database.attributeDefinitions.size(), std::size_t(7));
158 QCOMPARE(database.attributeAssignments.size(), std::size_t(5));
159 QCOMPARE(std::get<std::string>(database.attributes.at("BusLabel")), std::string("CAN"));
160 QCOMPARE(database.frames[0].cycleTimeMs.value(), 25U);
161 QCOMPARE(database.frames[1].cycleTimeMs.value(), 100U);
162 QCOMPARE(database.frames[1].frameFormat.value(), std::string("ExtendedCAN"));
163 QVERIFY(!database.frames[1].hasPgn);
164 QCOMPARE(std::get<double>(database.frames[0].attributes.at("Ratio")), 0.15);
165 QCOMPARE(std::get<std::string>(database.frames[0].signals[0].attributes.at("Label")), std::string("a\"b\\c"));
166 QCOMPARE(std::get<std::string>(database.frames[1].signals[0].attributes.at("Label")), std::string("default"));
167 const DecodeDatabase runtime = DbcDecodeBuilder().Build(database);
168 QCOMPARE(runtime.GetFrames()[0].cycleTimeMs.value(), 25U);
169 QCOMPARE(runtime.GetFrames()[1].frameFormat.value(), std::string("ExtendedCAN"));
170 QCOMPARE(runtime.GetFrames()[0].signals[0].attributes, database.frames[0].signals[0].attributes);
171 const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(database);
172 QCOMPARE(tree->GetChild(0)->GetFrame()->cycleTimeMs.value(), 25U);
173 QCOMPARE(tree->GetChild(0)->GetChild(0)->GetSignal()->attributes, database.frames[0].signals[0].attributes);
174 fixture.Write("BA_DEF_ BO_ \"Period\" INT 0 10;\nBA_DEF_DEF_ \"Period\" 1;\n"
175 "BA_ \"Period\" BO_ 1 2;\nBA_ \"Period\" BO_ 1 3;\nBO_ 1 F: 0 ECU\n");
176 const DbcDatabase repeated = DbcParser().ParseFile(fixture.path.string());
177 QCOMPARE(std::get<std::int64_t>(repeated.frames[0].attributes.at("Period")), std::int64_t(3));
178
179 fixture.Write("BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 4294967295;\n"
180 "BA_DEF_DEF_ \"GenMsgCycleTime\" 0;\n"
181 "BA_ \"GenMsgCycleTime\" BO_ 1 4294967295;\n"
182 "BA_DEF_ \"Wide\" INT -9223372036854775808 9223372036854775807;\n"
183 "BA_DEF_DEF_ \"Wide\" -9223372036854775808;\n"
184 "BA_DEF_ SG_ \"State\" ENUM \"off\",\"on\";\n"
185 "BA_DEF_DEF_ \"State\" 1;\n"
186 "BO_ 1 F: 0 ECU\nBO_ 2 Empty: 0 ECU\n");
187 const DbcDatabase boundaries = DbcParser().ParseFile(fixture.path.string());
188 QCOMPARE(boundaries.frames[0].cycleTimeMs.value(), std::numeric_limits<std::uint32_t>::max());
189 QCOMPARE(boundaries.frames[1].cycleTimeMs.value(), 0U);
190 QVERIFY(!boundaries.frames[0].frameFormat);
191 QCOMPARE(std::get<std::int64_t>(boundaries.attributes.at("Wide")), std::numeric_limits<std::int64_t>::min());
192 QCOMPARE(std::get<std::int64_t>(*boundaries.attributeDefinitions.at("State").defaultValue), std::int64_t(1));
193}
194
195void DbcParserTest::multilineAttributes() {
196 Fixture fixture;
197 fixture.Write(R"DBC(BA_
198 "GenMsgCycleTime" BO_ 1
199 25;
200BA_DEF_ BO_ "VFrameFormat" ENUM
201 "StandardCAN",
202 "StandardCAN_FD";
203BA_DEF_DEF_
204 "VFrameFormat"
205 "StandardCAN_FD";
206BA_DEF_ BO_ "GenMsgCycleTime"
207 INT 0 100;
208BA_DEF_DEF_ "GenMsgCycleTime"
209 50;
210BA_DEF_ SG_ "Label" STRING;
211BA_ "Label" SG_ 1 Value
212 "first; \"quoted\"
213second\\path";
214BA_ "VFrameFormat" BO_ 1
215 0;
216BO_ 1 F: 1 ECU
217SG_ Value : 0|8@1+ (1,0) [0|255] "" ECU
218BO_ 2 Default: 0 ECU
219)DBC");
220 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
221 QVERIFY(!result.HasErrors());
222 QCOMPARE(result.database.frames.size(), std::size_t(2));
223 QCOMPARE(result.database.frames[0].cycleTimeMs.value(), 25U);
224 QCOMPARE(result.database.frames[1].cycleTimeMs.value(), 50U);
225 QCOMPARE(result.database.frames[0].frameFormat.value(), std::string("StandardCAN"));
226 QCOMPARE(result.database.frames[1].frameFormat.value(), std::string("StandardCAN_FD"));
227 QCOMPARE(std::get<std::string>(result.database.frames[0].signals[0].attributes.at("Label")),
228 std::string("first; \"quoted\"\nsecond\\path"));
229 QCOMPARE(result.diagnostics.size(), std::size_t(1));
230 QCOMPARE(result.diagnostics[0].code, DbcDiagnosticCode::UnsupportedAttribute);
231 QCOMPARE(result.diagnostics[0].lineNumber, std::size_t(7));
232
233 fixture.Write("BA_DEF_ BO_ \"VFrameFormat\" ENUM\r\n \"StandardCAN\",\r\n \"ExtendedCAN_FD\";\r\n"
234 "BA_\r\n \"VFrameFormat\" BO_ 1\r\n 1;\r\nBO_ 1 F: 0 ECU\r\n");
235 const DbcParseResult explicitFormat = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
236 QVERIFY(!explicitFormat.HasErrors());
237 QCOMPARE(explicitFormat.diagnostics.size(), std::size_t(1));
238 QCOMPARE(explicitFormat.diagnostics[0].code, DbcDiagnosticCode::UnsupportedAttribute);
239 QCOMPARE(explicitFormat.diagnostics[0].lineNumber, std::size_t(4));
240 QCOMPARE(explicitFormat.database.frames[0].frameFormat.value(), std::string("ExtendedCAN_FD"));
241
242 fixture.Write("BA_DEF_ \"Text\" STRING;\n BA_ \"Text\" \"first \nsecond\";\n"
243 "UnsupportedStatement\n");
244 const DbcParseResult whitespace = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
245 QVERIFY(!whitespace.HasErrors());
246 QCOMPARE(std::get<std::string>(whitespace.database.attributes.at("Text")), std::string("first \nsecond"));
247 QCOMPARE(whitespace.diagnostics.size(), std::size_t(1));
248 QCOMPARE(whitespace.diagnostics[0].code, DbcDiagnosticCode::UnsupportedStatement);
249 QCOMPARE(whitespace.diagnostics[0].lineNumber, std::size_t(4));
250}
251
252void DbcParserTest::invalidMultilineAttributes() {
253 Fixture fixture;
254 const std::vector<std::string> invalid = {
255 "BA_DEF_\n BO_ \"A\" INT\n 10 0;\n",
256 "BA_DEF_ BO_ \"A\" ENUM\n \"one\",\n;\n",
257 "BA_DEF_ BO_ \"A\" STRING\n",
258 "BA_DEF_ BO_ \"A\" ENUM\n \"unterminated;\n",
259 "BA_DEF_DEF_\n \"Unknown\"\n 0;\n",
260 "BA_\n \"Unknown\" BO_ 1\n 0;\n",
261 "BA_DEF_ BO_ \"A\" STRING\n ; trailing\n"};
262 for (const std::string &statement: invalid) {
263 fixture.Write("BO_ 1 F: 0 ECU\n" + statement);
264 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
265 QVERIFY2(result.HasErrors(), statement.c_str());
266 QVERIFY(result.database.frames.empty());
267 QCOMPARE(result.diagnostics.back().code, DbcDiagnosticCode::InvalidAttribute);
268 QCOMPARE(result.diagnostics.back().lineNumber, std::size_t(2));
269 QVERIFY_THROWS_EXCEPTION(std::runtime_error, DbcParser().ParseFile(fixture.path.string()));
270 }
271}
272
273void DbcParserTest::invalidAttributes() {
274 Fixture fixture;
275 const std::vector<std::string> invalid = {
276 "BA_DEF_ BO_ \"A\"STRING;",
277 "BA_DEF_ BO_\"A\" STRING;",
278 "BA_DEF_ BO_ \"A\" \"STRING\";",
279 "BA_DEF_ BO_ A STRING;",
280 "BA_DEF_ ZZ_ \"A\" STRING;",
281 "BA_DEF_ BO_ \"A\" BLOB;",
282 "BA_DEF_ BO_ \"A\" FLOAT 2 1;",
283 "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 0;",
284 "BA_DEF_ BO_ \"A\" FLOAT 0 1;\nBA_ \"A\" BO_ 1 2;",
285 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ -1 0;",
286 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 4294967296 0;",
287 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 536870912 0;",
288 "BA_DEF_ BO_ \"A\" INT 2 1;",
289 "BA_DEF_ BO_ \"A\" FLOAT nan 1;",
290 "BA_DEF_ BO_ \"A\" FLOAT 0 inf;",
291 "BA_DEF_ BO_ \"A\" INT 0 9223372036854775808;",
292 "BA_DEF_ BO_ \"A\" STRING; trailing",
293 "BA_DEF_ BO_ \"A\" ENUM;",
294 "BA_DEF_ BO_ \"A\" ENUM \"one\",;",
295 "BA_DEF_ BO_ \"A\" ENUM \"one\",\"one\";",
296 "BA_DEF_ BO_ \"VFrameFormat\" STRING;",
297 "BA_DEF_ SG_ \"GenMsgCycleTime\" INT 0 10;",
298 "BA_ \"Unknown\" BO_ 1 0;",
299 "BA_DEF_DEF_ \"Unknown\" 0;",
300 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 1 2;",
301 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 1 \"0\";",
302 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" SG_ 1 S 0;",
303 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 2 0;",
304 "BA_DEF_ SG_ \"A\" INT 0 1;\nBA_ \"A\" SG_ 1 Missing 0;",
305 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_DEF_ BO_ \"A\" INT 0 1;",
306 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_DEF_DEF_ \"A\" 0;\nBA_DEF_DEF_ \"A\" 1;",
307 "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 \"bad\\q\";",
308 "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 \"unterminated;",
309 "BA_DEF_ BO_ \"A\" ENUM \"one\";\nBA_DEF_DEF_ \"A\" \"missing\";",
310 "BA_DEF_ BO_ \"A\" ENUM \"one\";\nBA_ \"A\" BO_ 1 1;",
311 "BA_DEF_ BO_ \"GenMsgCycleTime\" INT -1 4294967296;\nBA_DEF_DEF_ \"GenMsgCycleTime\" -1;",
312 "BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 4294967296;\nBA_ \"GenMsgCycleTime\" BO_ 1 4294967296;"};
313 for (const std::string &statement: invalid) {
314 fixture.Write("BO_ 1 F: 0 ECU\n" + statement + '\n');
315 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
316 QVERIFY2(result.HasErrors(), statement.c_str());
317 QVERIFY(result.database.frames.empty());
318 QCOMPARE(result.diagnostics.back().code, DbcDiagnosticCode::InvalidAttribute);
319 QCOMPARE(result.diagnostics.back().lineNumber, std::size_t(2 + std::count(statement.begin(), statement.end(), '\n')));
320 QVERIFY_THROWS_EXCEPTION(std::runtime_error, DbcParser().ParseFile(fixture.path.string()));
321 }
322}
323
324void DbcParserTest::attributeNumbers() {
325 ApplicationLocaleGuard guard;
326 std::locale::global(std::locale(std::locale::classic(), new CommaDecimalPunctuation));
327 Fixture fixture;
328 fixture.Write("BA_DEF_ \"I\" INT -9223372036854775808 +9223372036854775807;\n"
329 "BA_DEF_DEF_ \"I\" -9223372036854775808;\n"
330 "BA_ \"I\" +9223372036854775807;\n"
331 "BA_DEF_ \"H\" HEX -10 +10;\n"
332 "BA_DEF_DEF_ \"H\" +0;\n"
333 "BA_ \"H\" -10;\n"
334 "BA_DEF_ \"F\" FLOAT -1.25e+2 +2.5E2;\n"
335 "BA_DEF_DEF_ \"F\" +.5;\n"
336 "BA_ \"F\" -1.25e+2;\n"
337 "BA_DEF_ BU_ \"NodeDefault\" INT 0 10;\n"
338 "BA_DEF_DEF_ \"NodeDefault\" 5;\n"
339 "BA_DEF_ EV_ \"EnvironmentDefault\" STRING;\n"
340 "BA_DEF_DEF_ \"EnvironmentDefault\" \"default\";\n");
341 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
342 QVERIFY(!result.HasErrors());
343 QVERIFY(result.diagnostics.empty());
344 QCOMPARE(std::get<std::int64_t>(*result.database.attributeDefinitions.at("I").defaultValue), std::numeric_limits<std::int64_t>::min());
345 QCOMPARE(std::get<std::int64_t>(result.database.attributes.at("I")), std::numeric_limits<std::int64_t>::max());
346 QCOMPARE(std::get<std::int64_t>(*result.database.attributeDefinitions.at("H").defaultValue), std::int64_t(0));
347 QCOMPARE(std::get<std::int64_t>(result.database.attributes.at("H")), std::int64_t(-10));
348 QCOMPARE(std::get<double>(*result.database.attributeDefinitions.at("F").defaultValue), 0.5);
349 QCOMPARE(std::get<double>(result.database.attributes.at("F")), -125.0);
350 QCOMPARE(result.database.attributes.size(), std::size_t(3));
351 QCOMPARE(std::get<std::int64_t>(*result.database.attributeDefinitions.at("NodeDefault").defaultValue), std::int64_t(5));
352 QCOMPARE(std::get<std::string>(*result.database.attributeDefinitions.at("EnvironmentDefault").defaultValue), std::string("default"));
353}
354
355void DbcParserTest::invalidAttributeNumbers_data() {
356 QTest::addColumn<QString>("type");
357 QTest::addColumn<QString>("number");
358 const char *types[] = {"INT", "HEX", "FLOAT"};
359 const char *invalid[] = {"+", "-", "++1", "+-1", "--1", "1x", "0x10",
360 "nan", "inf", "-inf", "1e9999", "1e-9999"};
361 for (const char *type: types) {
362 for (const char *number: invalid) {
363 const std::string name = std::string(type) + '-' + number;
364 QTest::newRow(name.c_str()) << QString(type) << QString(number);
365 }
366 }
367 QTest::newRow("integer-overflow") << QString("INT") << QString("9223372036854775808");
368 QTest::newRow("integer-underflow") << QString("INT") << QString("-9223372036854775809");
369 QTest::newRow("integer-fraction") << QString("INT") << QString("1.5");
370}
371
372void DbcParserTest::invalidAttributeNumbers() {
373 QFETCH(QString, type);
374 QFETCH(QString, number);
375 const std::string kind = type.toStdString();
376 const std::string text = number.toStdString();
377 const std::string definition = "BA_DEF_ \"A\" " + kind + " -10 10;\n";
378 const std::string statements[] = {
379 "BA_DEF_ \"A\" " + kind + ' ' + text + " 10;\n",
380 "BA_DEF_ \"A\" " + kind + " -10 " + text + ";\n",
381 definition + "BA_DEF_DEF_ \"A\" " + text + ";\n",
382 definition + "BA_ \"A\" " + text + ";\n"};
383 Fixture fixture;
384 for (const std::string &statement: statements) {
385 fixture.Write(statement);
386 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
387 QVERIFY2(result.HasErrors(), statement.c_str());
388 QVERIFY(result.database.attributeDefinitions.empty());
389 QCOMPARE(result.diagnostics.back().code, DbcDiagnosticCode::InvalidAttribute);
390 QCOMPARE(result.diagnostics.back().lineNumber,
391 static_cast<std::size_t>(std::count(statement.begin(), statement.end(), '\n')));
392 QVERIFY_THROWS_EXCEPTION(std::runtime_error, DbcParser().ParseFile(fixture.path.string()));
393 }
394}
395
396void DbcParserTest::attributeWarnings() {
397 Fixture fixture;
398 fixture.Write("BA_DEF_ BO_ \"VFrameFormat\" ENUM \"StandardCAN\",\"ExtendedCAN\",\"J1939PG\",\"StandardCAN_FD\";\n"
399 "BA_DEF_DEF_ \"VFrameFormat\" \"StandardCAN_FD\";\n"
400 "BA_ \"VFrameFormat\" BO_ 1 2;\n"
401 "BA_ \"VFrameFormat\" BO_ 2147483649 0;\n"
402 "BO_ 1 F: 0 ECU\nBO_ 2147483649 E: 0 ECU\nBO_ 2 Default: 0 ECU\n");
403 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
404 QVERIFY(!result.HasErrors());
405 QCOMPARE(result.diagnostics.size(), std::size_t(3));
406 const std::size_t lines[] = {3U, 4U, 2U};
407 for (std::size_t index = 0; index < 3U; ++index) {
408 QCOMPARE(result.diagnostics[index].code, DbcDiagnosticCode::UnsupportedAttribute);
409 QCOMPARE(result.diagnostics[index].severity, DbcDiagnosticSeverity::Warning);
410 QCOMPARE(result.diagnostics[index].lineNumber, lines[index]);
411 QCOMPARE(result.diagnostics[index].filePath, fixture.path.string());
412 }
413 QCOMPARE(result.database.frames[0].frameFormat.value(), std::string("J1939PG"));
414 QVERIFY(!result.database.frames[0].hasPgn);
415 QVERIFY(result.database.frames[1].isExtended);
416}
417
418void DbcParserTest::diagnostics() {
419 Fixture fixture;
420 fixture.Write("VERSION \"test\"\n"
421 "NS_ :\n"
422 " CM_\n"
423 " BA_\n"
424 " SIG_VALTYPE_\n"
425 " SG_MUL_VAL_\n\n"
426 "BS_:\n"
427 "BU_: ECU\n"
428 "BO_ 1 Frame: 1 ECU\n"
429 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
430 "SIG_VALTYPE_ 1 Value : 1;\n"
431 "BA_REL_ \"SignalType\" SG_ 1 Value 1;\n"
432 "VENDOR_EXTENSION_ data;\n");
433 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
434 CHECK(!result.HasErrors() && result.database.frames.size() == 1U,
435 "Warnings preserve the supported database");
436 QCOMPARE(result.diagnostics.size(), std::size_t(3U));
437 for (std::size_t index = 0U; index < result.diagnostics.size(); ++index) {
438 const DbcDiagnostic &diagnostic = result.diagnostics[index];
439 CHECK(diagnostic.severity == DbcDiagnosticSeverity::Warning &&
440 diagnostic.code == DbcDiagnosticCode::UnsupportedStatement,
441 "Unsupported statements have structured warning severity and code");
442 QCOMPARE(diagnostic.filePath, fixture.path.string());
443 QCOMPARE(diagnostic.lineNumber, 12U + index);
444 CHECK(diagnostic.message.find("ignored") != std::string::npos,
445 "Warning explains unsupported syntax");
446 }
447 CHECK(DbcParser().ParseFile(fixture.path.string()).frames.size() == 1U,
448 "Legacy API accepts files with warnings");
449 fixture.Write("VERSION \"\"\nNS_:\nSIG_VALTYPE_\n\tBA_\n BS_ :\n BU_ : ECU\n");
450 const DbcParseResult headers = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
451 CHECK(headers.diagnostics.empty(), "Common header variants stay quiet");
452 fixture.Write("NS_ :\n BA_\n BO_\n");
453 CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
454 "Namespace handling does not swallow a malformed frame");
455}
456
457void DbcParserTest::diagnosticErrors() {
458 Fixture fixture;
459 struct ErrorCase {
460 const char *text;
461 DbcDiagnosticCode code;
462 std::size_t line;
463 };
464 const ErrorCase cases[] = {
465 {"UNKNOWN_ data;\n\nBO_ invalid Frame: 1 ECU\n",
466 DbcDiagnosticCode::InvalidStatement, 3U},
467 {"BO_ 1 Frame: 1 ECU\nSG_MUL_VAL_ 1 Value Mode 1-2;\n",
468 DbcDiagnosticCode::InvalidMultiplexing, 2U},
469 {"BO_ 1 Frame: 1 ECU\nVAL_ 1 Missing 0 \"Off\";\n",
470 DbcDiagnosticCode::InvalidValueDescription, 2U},
471 {"BO_ 1 Frame: 1 ECU\nSG_ Value m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
472 DbcDiagnosticCode::InvalidMultiplexing, 2U},
473 {"BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
474 DbcDiagnosticCode::InvalidMultiplexing, 2U},
475 {"VAL_ broken\n", DbcDiagnosticCode::InvalidValueDescription, 1U},
476 {"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
477 DbcDiagnosticCode::InvalidStatement, 1U},
478 {"BO_ 1 Frame: 1 ECU\nBO_ 1 Duplicate: 1 ECU\n",
479 DbcDiagnosticCode::InvalidStatement, 2U},
480 {"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
481 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
482 DbcDiagnosticCode::InvalidStatement, 3U},
483 {"BO_ 1 Frame: 1 ECU\nSG_ Value : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
484 DbcDiagnosticCode::InvalidStatement, 2U},
485 {"BO_ 1 Frame: 1 ECU\nVAL_ 2 Missing 0 \"Off\";\n",
486 DbcDiagnosticCode::InvalidValueDescription, 2U},
487 {"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
488 "VAL_ 1 Value 0 \"Off\";\nVAL_ 1 Value 0 \"Duplicate\";\n",
489 DbcDiagnosticCode::InvalidValueDescription, 4U},
490 {"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"
491 "SG_ Value m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
492 DbcDiagnosticCode::InvalidMultiplexing, 3U},
493 {"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
494 "SG_ Other M : 8|8@1- (1,0) [-128|127] \"\" ECU\n",
495 DbcDiagnosticCode::InvalidMultiplexing, 3U},
496 {"BO_ 1 Frame: 1 ECU\nSG_ First : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
497 "SG_ Second : 7|8@0+ (1,0) [0|255] \"\" ECU\n",
498 DbcDiagnosticCode::InvalidMultiplexing, 3U}};
499 for (const ErrorCase &test : cases) {
500 fixture.Write(test.text);
501 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
502 CHECK(result.HasErrors() && result.database.frames.empty(),
503 "Failed parse never returns a partial database");
504 const DbcDiagnostic &diagnostic = result.diagnostics.back();
505 CHECK(diagnostic.severity == DbcDiagnosticSeverity::Error && diagnostic.code == test.code,
506 "Error has expected severity and code");
507 QCOMPARE(diagnostic.filePath, fixture.path.string());
508 QCOMPARE(diagnostic.lineNumber, test.line);
509 CHECK(!diagnostic.message.empty(), "Error includes a message");
510 const bool hasWarning = std::string(test.text).find("UNKNOWN_") == 0U;
511 QCOMPARE(result.diagnostics.size(), hasWarning ? std::size_t(2U) : std::size_t(1U));
512 try {
513 DbcParser().ParseFile(fixture.path.string());
514 CHECK(false, "Legacy parser must throw");
515 } catch (const std::runtime_error &error) {
516 QCOMPARE(std::string(error.what()), fixture.path.string() + ":" +
517 std::to_string(test.line) + ": " + diagnostic.message);
518 }
519 }
520 const std::string missingPath = fixture.path.string() + ".missing";
521 const DbcParseResult missing = DbcParser().ParseFileWithDiagnostics(missingPath);
522 CHECK(missing.HasErrors() && missing.database.frames.empty(), "Missing file returns error");
523 QCOMPARE(missing.diagnostics.size(), std::size_t(1U));
524 CHECK(missing.diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
525 "Missing file has a distinct code");
526 QCOMPARE(missing.diagnostics[0].lineNumber, std::size_t(0U));
527 QCOMPARE(missing.diagnostics[0].filePath, missingPath);
528 QCOMPARE(missing.diagnostics[0].message, "Failed to open DBC file: " + missingPath);
529 try {
530 DbcParser().ParseFile(missingPath);
531 CHECK(false, "Legacy API throws on missing file");
532 } catch (const std::runtime_error &error) {
533 QCOMPARE(std::string(error.what()), missing.diagnostics[0].message);
534 }
535 const std::string directoryPath = fixture.path.parent_path().string();
536 const DbcParseResult directory = DbcParser().ParseFileWithDiagnostics(directoryPath);
537 CHECK(directory.HasErrors() && directory.database.frames.empty(),
538 "Directory cannot be parsed as a file");
539 QCOMPARE(directory.diagnostics.size(), std::size_t(1U));
540 CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError ||
541 directory.diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
542 "Directory produces a file-level error on supported platforms");
543 QCOMPARE(directory.diagnostics[0].lineNumber, std::size_t(0U));
544 QCOMPARE(directory.diagnostics[0].filePath, directoryPath);
545#ifdef __linux__
546 CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError,
547 "Linux directory input exercises the read-error path");
548 QCOMPARE(directory.diagnostics[0].message, "Failed to read DBC file: " + directoryPath);
549#endif
550 try {
551 DbcParser().ParseFile(directoryPath);
552 CHECK(false, "Legacy API throws on unreadable input");
553 } catch (const std::runtime_error &error) {
554 QCOMPARE(std::string(error.what()), directory.diagnostics[0].message);
555 }
556 fixture.Write("BO_ 1 Frame: 1 ECU\n");
557 CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).diagnostics.empty(),
558 "Diagnostics belong to an independent parse session");
559}
560
561void DbcParserTest::diagnosticUnsupportedStatements() {
562 Fixture fixture;
563 const std::string definitions =
564 "BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (2,10) [10|520] \"V\" ECU\n"
565 "CM_ SG_ 1 Value \"Signal comment\";\nVAL_ 1 Value 1 \"On\";\n";
566 const std::vector<std::string> statements = {
567 "SIG_VALTYPE_ 1 Value : 1;", "SIG_VALTYPE_ 1 Value : 2;",
568 "BA_REL_ \"SignalType\" SG_ 1 Value 1;",
569 "BA_DEF_REL_ BU_SG_REL_ \"Relation\" INT 0 1;",
570 "BA_REL_ \"Relation\" BU_SG_REL_ ECU SG_ 1 Value 1;",
571 "SGTYPE_ Type : 8@1+ (1,0) [0|255] \"\" 0, Table;",
572 "SIG_TYPE_REF_ 1 Value : Type;",
573 "EV_ Environment : 0 [0|1] \"\" 0 1 DUMMY_NODE_VECTOR0 ECU;",
574 "ENVVAR_DATA_ Environment : 4;",
575 "VAL_TABLE_ Table 0 \"Off\" 1 \"On\";",
576 "BO_TX_BU_ 1 : ECU;", "VENDOR_EXTENSION_ data;", "UNKNOWN_"
577 };
578 for (const std::string &statement : statements) {
579 fixture.Write(definitions + statement + '\n');
580 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
581 CHECK(!result.HasErrors(), statement);
582 QCOMPARE(result.diagnostics.size(), std::size_t(1U));
583 const DbcDiagnostic &warning = result.diagnostics[0];
584 CHECK(warning.severity == DbcDiagnosticSeverity::Warning &&
585 warning.code == DbcDiagnosticCode::UnsupportedStatement, statement);
586 QCOMPARE(warning.filePath, fixture.path.string());
587 QCOMPARE(warning.lineNumber, std::size_t(5U));
588 const std::string keyword = statement.substr(0U, statement.find(' '));
589 CHECK(warning.message.find("'" + keyword + "'") != std::string::npos, statement);
590 const DecodeDatabase runtime = DbcDecodeBuilder().Build(result.database);
591 RawCanFrame raw;
592 raw.canId = 1U;
593 raw.data = {1U};
594 const DecodedFrameValue decoded = DbcDecoder().Decode(runtime, raw);
595 CHECK(decoded.valid && decoded.signals.size() == 1U && decoded.signals[0].valid,
596 "Warnings preserve supported signal decoding");
597 QCOMPARE(decoded.signals[0].physicalValue, 12.0);
598 CHECK(decoded.signals[0].GetValueDescription() &&
599 *decoded.signals[0].GetValueDescription() == "On",
600 "Warnings preserve VAL_ descriptions");
601 QCOMPARE(runtime.GetFrames()[0].signals[0].comment, std::string("Signal comment"));
602 }
603}
604
605void DbcParserTest::diagnosticNamespaceBoundaries() {
606 Fixture fixture;
607 const std::vector<std::string> endings = {
608 "BS_\n", "BS_:\n", "BS_ :\n", "BU_\n", "BU_:\n", "BU_ : ECU\n",
609 "BO_ 1 Frame: 1 ECU\n", "VENDOR_EXTENSION_ data;\n"
610 };
611 for (const std::string &ending : endings) {
612 fixture.Write("NS_ :\n\tSG_MUL_VAL_\n\nBA_\n" + ending + "SIG_VALTYPE_\n");
613 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
614 CHECK(!result.HasErrors(), ending);
615 const std::size_t expectedWarnings = ending.find("VENDOR_") == 0U ? 2U : 1U;
616 QCOMPARE(result.diagnostics.size(), expectedWarnings);
617 QCOMPARE(result.diagnostics.back().lineNumber, std::size_t(6U));
618 CHECK(result.diagnostics.back().message.find("SIG_VALTYPE_") != std::string::npos,
619 "Unsupported keyword outside NS_ must produce a warning");
620 }
621 for (const std::string &definition : {std::string("BO_"), std::string("SG_")}) {
622 fixture.Write("NS_:\n\tBA_\n\t" + definition + '\n');
623 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
624 CHECK(result.HasErrors(), "Malformed definition ends NS_ instead of being hidden");
625 QCOMPARE(result.diagnostics.size(), std::size_t(1U));
626 QCOMPARE(result.diagnostics[0].lineNumber, std::size_t(3U));
627 }
628}
629
630void DbcParserTest::diagnosticFormatting() {
631 Fixture fixture;
632 fixture.path += " with spaces";
633 const DbcParser parser;
634 for (const std::string &text : {std::string(), std::string(" \t\r\n\n")}) {
635 fixture.Write(text);
636 const DbcParseResult empty = parser.ParseFileWithDiagnostics(fixture.path.string());
637 CHECK(!empty.HasErrors() && empty.diagnostics.empty() && empty.database.frames.empty(),
638 "Empty and whitespace-only inputs are valid");
639 }
640 fixture.Write("\r\n\tVENDOR_EXTENSION_ data;\r\n\r\nUNKNOWN_");
641 const std::string relativePath = std::filesystem::relative(fixture.path).string();
642 const DbcParseResult warnings = parser.ParseFileWithDiagnostics(relativePath);
643 CHECK(!warnings.HasErrors() && warnings.database.frames.empty(), "Warnings-only input");
644 QCOMPARE(warnings.diagnostics.size(), std::size_t(2U));
645 QCOMPARE(warnings.diagnostics[0].lineNumber, std::size_t(2U));
646 QCOMPARE(warnings.diagnostics[1].lineNumber, std::size_t(4U));
647 for (const DbcDiagnostic &warning : warnings.diagnostics)
648 QCOMPARE(warning.filePath, relativePath);
649
650 fixture.Write("UNKNOWN_ first;\nBO_ invalid Frame: 1 ECU\n"
651 "UNKNOWN_ after_error;\nSG_ broken\n");
652 const DbcParseResult stopped = parser.ParseFileWithDiagnostics(relativePath);
653 CHECK(stopped.HasErrors() && stopped.database.frames.empty(), "First error stops parsing");
654 QCOMPARE(stopped.diagnostics.size(), std::size_t(2U));
655 QCOMPARE(stopped.diagnostics[0].lineNumber, std::size_t(1U));
656 QCOMPARE(stopped.diagnostics[1].lineNumber, std::size_t(2U));
657 QCOMPARE(stopped.diagnostics[1].filePath, relativePath);
658
659 fixture.Write("VAL_ 1 Missing 0 \"Off\";\nBO_ 1 Frame: 1 ECU\nUNKNOWN_ data;\n");
660 const DbcParseResult deferred = parser.ParseFileWithDiagnostics(relativePath);
661 CHECK(deferred.HasErrors() && deferred.database.frames.empty(), "Deferred error rejects database");
662 QCOMPARE(deferred.diagnostics.size(), std::size_t(2U));
663 CHECK(deferred.diagnostics[0].severity == DbcDiagnosticSeverity::Warning &&
664 deferred.diagnostics[1].severity == DbcDiagnosticSeverity::Error,
665 "Deferred errors follow warnings in detection order");
666 QCOMPARE(deferred.diagnostics[0].lineNumber, std::size_t(3U));
667 QCOMPARE(deferred.diagnostics[1].lineNumber, std::size_t(1U));
668 QCOMPARE(deferred.diagnostics[1].filePath, relativePath);
669 fixture.Write("BO_ 1 Frame: 1 ECU");
670 const DbcParseResult recovered = parser.ParseFileWithDiagnostics(relativePath);
671 CHECK(!recovered.HasErrors() && recovered.diagnostics.empty() && recovered.database.frames.size() == 1U,
672 "Same parser has clean state after errors and accepts missing final newline");
673}
674
675void DbcParserTest::validFrames() {
676 Fixture fixture;
677 struct ValidCase {
678 const char *line;
679 std::uint32_t id;
680 bool extended;
681 unsigned length;
682 };
683 const ValidCase valid[] = {
684 {"BO_ 0 Frame: 0 ECU", 0U, false, 0U},
685 {"BO_ 2047 Frame: 8 ECU", 2047U, false, 8U},
686 {"BO_ 2048 Frame: 64 ECU", 2048U, true, 64U},
687 {"BO_ 536870911 Frame: 255 ECU", 0x1FFFFFFFU, true, 255U},
688 {"BO_ 2147483649 Frame: 8 ECU", 1U, true, 8U},
689 {"BO_ 2684354559 Frame: 8 ECU", 0x1FFFFFFFU, true, 8U},
690 {" BO_\t256\tFrame :\t8\tECU \r", 256U, false, 8U}};
691 for (const ValidCase &test: valid) {
692 fixture.Write(std::string(test.line) + '\n');
693 try {
694 const DbcDatabase database = DbcParser().ParseFile(fixture.path.string());
695 QCOMPARE(database.frames.size(), std::size_t(1U));
696 const FrameInfo &frame = database.frames[0];
697 QCOMPARE(frame.name, std::string("Frame"));
698 QCOMPARE(frame.canId, test.id);
699 QCOMPARE(frame.isExtended, test.extended);
700 QCOMPARE(static_cast<unsigned>(frame.dlc), test.length);
701 QCOMPARE(frame.transmitter, std::string("ECU"));
702 } catch (const std::exception &error) { CHECK(false, error.what()); }
703 }
704}
705
706void DbcParserTest::invalidFrames() {
707 Fixture fixture;
708 const char *invalid[] = {
709 "BO_ invalid", "BO_", "BO_ 1", "BO_ 1 : 8 ECU", "BO_ 1 Frame 8 ECU",
710 "BO_ 1 Frame:", "BO_ 1 Frame: 8", "BO_ 1 Frame: 8 ECU extra",
711 "BO_ 1 Two Names: 8 ECU", "BO_ -1 Frame: 8 ECU", "BO_ +1 Frame: 8 ECU",
712 "BO_ 1x Frame: 8 ECU", "BO_ 0x123 Frame: 8 ECU", "BO_ 1.5 Frame: 8 ECU",
713 "BO_ 4294967296 Frame: 8 ECU", "BO_ 999999999999999999999 Frame: 8 ECU",
714 "BO_ 536870912 Frame: 8 ECU", "BO_ 1073741824 Frame: 8 ECU",
715 "BO_ 2684354560 Frame: 8 ECU", "BO_ 4294967295 Frame: 8 ECU",
716 "BO_ 1 Frame: -1 ECU", "BO_ 1 Frame: +8 ECU", "BO_ 1 Frame: 8x ECU",
717 "BO_ 1 Frame: 8.5 ECU", "BO_ 1 Frame: 256 ECU",
718 "BO_ 1 Frame: 4294967296 ECU", "BO_\tinvalid"};
719 for (const char *line: invalid) {
720 fixture.Write(std::string("VERSION \"test\"\n\n") + line + '\n');
721 try {
722 DbcParser().ParseFile(fixture.path.string());
723 CHECK(false, line);
724 } catch (const std::runtime_error &error) {
725 const std::string prefix = fixture.path.string() + ":3: ";
726 CHECK(std::string(error.what()).find(prefix) == 0U &&
727 std::string(error.what()).size() > prefix.size(),
728 line);
729 }
730 }
731}
732
733void DbcParserTest::duplicateFrames() {
734 Fixture fixture;
735 struct DuplicateCase {
736 const char *firstId;
737 const char *duplicateId;
738 const char *message;
739 };
740 const DuplicateCase cases[] = {
741 {"1", "1", "Duplicate standard frame CAN ID: 1"},
742 {"2147483649", "2147483649", "Duplicate extended frame CAN ID: 1"},
743 {"2048", "2147485696", "Duplicate extended frame CAN ID: 2048"},
744 {"2147485696", "2048", "Duplicate extended frame CAN ID: 2048"},
745 {"536870911", "2684354559", "Duplicate extended frame CAN ID: 536870911"}
746 };
747 const DbcParser parser;
748 for (const DuplicateCase &test : cases) {
749 fixture.Write("BO_ " + std::string(test.firstId) + " First: 8 ECU\n"
750 "BO_ 2 Other: 8 ECU\n\n"
751 " BO_\t" + test.duplicateId + "\tSecond: 8 ECU\n");
752 try {
753 parser.ParseFile(fixture.path.string());
754 CHECK(false, "Duplicate frame must be rejected");
755 } catch (const std::runtime_error &error) {
756 QCOMPARE(std::string(error.what()), fixture.path.string() + ":4: " + test.message);
757 }
758 }
759 fixture.Write("BO_ 1 First: 8 ECU\n");
760 QCOMPARE(parser.ParseFile(fixture.path.string()).frames.size(), std::size_t(1U));
761}
762
763void DbcParserTest::duplicateSignals() {
764 Fixture fixture;
765 const std::string first = "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n";
766 const std::string duplicates[] = {
767 first,
768 " SG_\tState : 16|8@1+ (2,1) [0|511] \"V\" ECU\n",
769 "SG_ State m1 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
770 };
771 for (const std::string &duplicate : duplicates) {
772 fixture.Write("BO_ 1 Frame: 8 ECU\n" + first +
773 "SG_ Other : 8|8@1+ (1,0) [0|255] \"\" ECU\n\n" + duplicate);
774 try {
775 DbcParser().ParseFile(fixture.path.string());
776 CHECK(false, "Duplicate signal must be rejected");
777 } catch (const std::runtime_error &error) {
778 QCOMPARE(std::string(error.what()), fixture.path.string() +
779 ":5: Duplicate signal name 'State' in frame 'Frame'.");
780 }
781 }
782}
783
784void DbcParserTest::distinctDefinitions() {
785 Fixture fixture;
786 fixture.Write("BO_ 1 Shared: 1 ECU\n"
787 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
788 "BO_ 2147483649 Shared: 1 ECU\n"
789 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
790 "BO_ 2 Shared: 2 ECU\n"
791 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
792 "SG_ state : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
793 "CM_ BO_ 1 \"Standard\";\n"
794 "CM_ BO_ 2147483649 \"Extended\";\n"
795 "CM_ SG_ 1 State \"Standard state\";\n"
796 "CM_ SG_ 2147483649 State \"Extended state\";\n");
797 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
798 QCOMPARE(parsed.frames.size(), std::size_t(3U));
799 QCOMPARE(parsed.frames[0].canId, parsed.frames[1].canId);
800 CHECK(!parsed.frames[0].isExtended && parsed.frames[1].isExtended,
801 "Standard and extended frames with the same numeric ID remain distinct");
802 QCOMPARE(parsed.frames[0].comment, std::string("Standard"));
803 QCOMPARE(parsed.frames[1].comment, std::string("Extended"));
804 QCOMPARE(parsed.frames[0].signals[0].comment, std::string("Standard state"));
805 QCOMPARE(parsed.frames[1].signals[0].comment, std::string("Extended state"));
806 QCOMPARE(parsed.frames[2].signals.size(), std::size_t(2U));
807 const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
808 RawCanFrame raw;
809 raw.canId = 1U;
810 raw.data = {7U};
811 const DecodedFrameValue standard = DbcDecoder().Decode(runtime, raw);
812 QVERIFY(standard.valid);
813 QCOMPARE(standard.definition->comment, std::string("Standard"));
814 raw.isExtended = true;
815 const DecodedFrameValue extended = DbcDecoder().Decode(runtime, raw);
816 QVERIFY(extended.valid);
817 QCOMPARE(extended.definition->comment, std::string("Extended"));
818}
819
820void DbcParserTest::comments() {
821 Fixture fixture;
822 fixture.Write(
823 "CM_\tBO_\t1\t\"First\";\r\n"
824 "CM_\tSG_\t1\tState\t\" Line one \r\n"
825 "\r\n"
826 "\tBO_ is text; \\\"quoted\\\" and \\\\slash\"\t;\r\n"
827 "CM_ BO_ 2147483649 \"Extended\";\r\n"
828 "CM_ SG_ 2147483649 State \"Extended signal\";\r\n"
829 "BO_ 1 Standard: 1 ECU\r\n"
830 "CM_ BO_ 1 \"Last\";\r\n"
831 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\r\n"
832 "BO_ 2147483649 Extended: 1 ECU\r\n"
833 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU");
834 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
835 QVERIFY(!result.HasErrors());
836 QVERIFY(result.diagnostics.empty());
837 QCOMPARE(result.database.frames.size(), std::size_t(2));
838 QCOMPARE(result.database.frames[0].comment, std::string("Last"));
839 QCOMPARE(result.database.frames[0].signals[0].comment,
840 std::string(" Line one \n\n\tBO_ is text; \"quoted\" and \\slash"));
841 QCOMPARE(result.database.frames[1].comment, std::string("Extended"));
842 QCOMPARE(result.database.frames[1].signals[0].comment, std::string("Extended signal"));
843
844 const std::string ids[][2] = {
845 {"0", "0"},
846 {"2047", "2047"},
847 {"2048", "2147485696"},
848 {"536870911", "2684354559"},
849 {"2684354559", "536870911"}};
850 for (const std::string(&id)[2]: ids) {
851 fixture.Write("CM_ BO_ " + id[0] + " \"Frame\";\nCM_ SG_ " + id[0] +
852 " State \"Signal\";\nBO_ " + id[1] + " Frame: 1 ECU\n"
853 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n");
854 const DbcDatabase database = DbcParser().ParseFile(fixture.path.string());
855 QCOMPARE(database.frames[0].comment, std::string("Frame"));
856 QCOMPARE(database.frames[0].signals[0].comment, std::string("Signal"));
857 }
858 fixture.Write("CM_ BO_ 1 \"Line one\nLine two\";\n"
859 "BO_ 1 Frame: 0 ECU\nUNKNOWN_ value\nBO_ bad Frame: 0 ECU\n");
860 const DbcParseResult following = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
861 QVERIFY(following.HasErrors());
862 QCOMPARE(following.diagnostics.size(), std::size_t(2));
863 QCOMPARE(following.diagnostics[0].lineNumber, std::size_t(4));
864 QCOMPARE(following.diagnostics[1].lineNumber, std::size_t(5));
865}
866
867void DbcParserTest::invalidComments_data() {
868 QTest::addColumn<QString>("statement");
869 const char *ids[] = {"-1", "+1", "1x", "0x1", "4294967296", "536870912",
870 "1073741824", "2684354560", "4294967295"};
871 for (const char *id: ids) {
872 const std::string frameRow = std::string("frame-id-") + id;
873 const std::string signalRow = std::string("signal-id-") + id;
874 QTest::newRow(frameRow.c_str()) << QString::fromStdString("CM_ BO_ " + std::string(id) + " \"Text\";");
875 QTest::newRow(signalRow.c_str()) << QString::fromStdString("CM_ SG_ " + std::string(id) + " State \"Text\";");
876 }
877 QTest::newRow("missing-id") << QStringLiteral("CM_ BO_");
878 QTest::newRow("missing-signal") << QStringLiteral("CM_ SG_ 1 \"Text\";");
879 QTest::newRow("missing-quote") << QStringLiteral("CM_ BO_ 1 Text;");
880 QTest::newRow("unclosed") << QStringLiteral("CM_ SG_ 1 State \"Text\n More");
881 QTest::newRow("invalid-escape") << QStringLiteral("CM_ BO_ 1 \"bad\\q\";");
882 QTest::newRow("dangling-escape") << QStringLiteral("CM_ BO_ 1 \"bad\\");
883 QTest::newRow("missing-semicolon") << QStringLiteral("CM_ SG_ 1 State \"Text\"");
884 QTest::newRow("trailing-text") << QStringLiteral("CM_ BO_ 1 \"Text\"; garbage");
885 QTest::newRow("extra-header") << QStringLiteral("CM_ BO_ 1 extra \"Text\";");
886 QTest::newRow("extra-string") << QStringLiteral("CM_ BO_ 1 \"Text\" \"More\";");
887 QTest::newRow("missing-frame") << QStringLiteral("CM_ BO_ 2 \"Text\";");
888 QTest::newRow("missing-signal-frame") << QStringLiteral("CM_ SG_ 2 State \"Text\";");
889 QTest::newRow("missing-signal-reference") << QStringLiteral("CM_ SG_ 1 Missing \"Text\";");
890 QTest::newRow("different-frame-type") << QStringLiteral("CM_ BO_ 2147483649 \"Text\";");
891 QTest::newRow("missing-multiline-reference") << QStringLiteral("CM_ SG_ 1 Missing \"Text\nMore\";");
892}
893
894void DbcParserTest::invalidComments() {
895 QFETCH(QString, statement);
896 Fixture fixture;
897 fixture.Write("BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n\n" +
898 statement.toStdString());
899 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
900 QVERIFY(result.HasErrors());
901 QVERIFY(result.database.frames.empty());
902 QCOMPARE(result.diagnostics.size(), std::size_t(1));
903 const DbcDiagnostic &error = result.diagnostics[0];
904 QCOMPARE(error.severity, DbcDiagnosticSeverity::Error);
905 QCOMPARE(error.code, DbcDiagnosticCode::InvalidStatement);
906 QCOMPARE(error.filePath, fixture.path.string());
907 QCOMPARE(error.lineNumber, std::size_t(4));
908 QVERIFY(!error.message.empty());
909 try {
910 DbcParser().ParseFile(fixture.path.string());
911 QFAIL("Malformed or unresolved comment must throw.");
912 } catch (const std::runtime_error &exception) {
913 QVERIFY(std::string(exception.what()).find(fixture.path.string() + ":4: ") == 0U);
914 }
915}
916
917void DbcParserTest::integration() {
918 Fixture fixture;
919 fixture.Write(
920 "BO_TX_BU_ 256 : ECU;\nBO_ 256 Frame: 8 ECU\n"
921 " SG_ Speed : 0|16@1+ (0.5,1) [0|100] \"rpm\" ECU\n"
922 "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Speed \"Signal comment\";\n"
923 "BO_ 257 Second: 8 ECU\n");
924 const DbcDatabase database = DbcParser().ParseFile(fixture.path.string());
925 CHECK(database.frames.size() == 2U && database.frames[0].signals.size() == 1U &&
926 database.frames[0].comment == "Frame comment" &&
927 database.frames[0].signals[0].comment == "Signal comment",
928 "Integration");
929 fixture.Write("\nSG_ Speed : invalid\n");
930 try {
931 DbcParser().ParseFile(fixture.path.string());
932 CHECK(false, "Signal error location");
933 } catch (const std::runtime_error &error) {
934 CHECK(std::string(error.what()).find(fixture.path.string() + ":2: ") == 0U,
935 "Signal error location");
936 }
937}
938
939void DbcParserTest::multiplexing() {
940 Fixture fixture;
941 const std::string selector = "SG_ Mode M : 0|8@1+ (10,3) [0|255] \"\" ECU\n";
942 const std::string first = "SG_ First m1 : 8|8@1- (2,0) [-128|127] \"V\" ECU\n";
943 const std::string second = "SG_ Second m2 : 15|8@0+ (1,0) [0|255] \"\" ECU\n";
944 const std::string common = "SG_ Common : 16|8@1+ (1,0) [0|255] \"\" ECU\n";
945 fixture.Write("BO_ 1 Frame: 3 ECU\n" + first + second + common + selector +
946 "VAL_ 1 First -1 \"Negative\";\n");
947 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
948 const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
949 CHECK(parsed.frames[0].signals[3].multiplexing.role == MultiplexRole::Selector,
950 "Selector declared after branches");
951 const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed);
952 CHECK(tree->GetChild(0U)->GetChild(0U)->GetSignal()->multiplexing.value == 1U,
953 "Tree preserves multiplexing");
954 RawCanFrame raw;
955 raw.canId = 1U;
956 raw.data = {1U, 255U, 7U};
957 DecodedFrameValue result = DbcDecoder().Decode(runtime, raw);
958 CHECK(result.signals[0].valid && std::get<std::int64_t>(result.signals[0].rawValue) == -1 &&
959 result.signals[0].physicalValue == -2 && *result.signals[0].GetValueDescription() == "Negative",
960 "Active signed branch and VAL_ use exact raw selector");
961 CHECK(!result.signals[1].valid && result.signals[1].status == SignalDecodeStatus::Inactive &&
962 result.signals[1].GetValueDescription() == nullptr && result.signals[2].valid,
963 "Inactive branch and unconditional signal");
964 raw.data[0] = 2U;
965 result = DbcDecoder().Decode(runtime, raw);
966 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive && result.signals[1].valid &&
967 std::get<std::uint64_t>(result.signals[1].rawValue) == 255U,
968 "Motorola branch becomes active");
969 raw.data = {3U};
970 result = DbcDecoder().Decode(runtime, raw);
971 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive &&
972 result.signals[1].status == SignalDecodeStatus::Inactive &&
973 result.signals[2].status == SignalDecodeStatus::InsufficientPayload,
974 "Unmapped selector leaves branches inactive even with missing bytes");
975 raw.data = {1U};
976 result = DbcDecoder().Decode(runtime, raw);
977 CHECK(result.signals[0].status == SignalDecodeStatus::InsufficientPayload,
978 "Active branch requires payload");
979 raw.data.clear();
980 result = DbcDecoder().Decode(runtime, raw);
981 CHECK(result.signals[0].status == SignalDecodeStatus::SelectorUnavailable &&
982 result.signals[3].status == SignalDecodeStatus::InsufficientPayload,
983 "Unavailable selector does not imply inactive branch");
984
985 const std::vector<std::string> invalid = {
986 first,
987 selector + "SG_ Other M : 16|8@1- (1,0) [-128|127] \"\" ECU\n",
988 "SG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
989 selector + "SG_ TooLarge m256 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
990 selector + first + "SG_ Same m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n",
991 selector + "SG_ Collision m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
992 selector + first + "SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
993 "SG_MUL_VAL_ 1 First Mode 1-2;\n"};
994 for (const std::string &signals : invalid) {
995 fixture.Write("BO_ 1 Frame: 3 ECU\n" + signals);
996 bool rejected = false;
997 try {
998 DbcParser().ParseFile(fixture.path.string());
999 } catch (const std::runtime_error &error) {
1000 rejected = std::string(error.what()).find(fixture.path.string() + ":") == 0U;
1001 }
1002 CHECK(rejected, "Invalid multiplexing rejected with location");
1003 }
1004 for (const std::string marker : {"m", "m-1", "m+1", "m1M", "m18446744073709551616", "M m1"})
1005 RejectSignal(fixture, "SG_ Bad " + marker + " : 8|8@1+ (1,0) [0|255] \"\" ECU");
1006}
1007
1008void DbcParserTest::multiplexingBoundaries() {
1009 Fixture fixture;
1010 fixture.Write("BO_ 1 Frame: 9 ECU\n"
1011 "SG_ Branch m18446744073709551615 : 64|8@1+ (1,0) [0|255] \"\" ECU\n"
1012 "SG_ Mode M : 7|64@0+ (0,5) [0|1] \"\" ECU\n");
1013 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
1014 DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
1015 CHECK(runtime.GetFrames()[0].signals[0].multiplexing.value == std::numeric_limits<std::uint64_t>::max(),
1016 "Exact uint64 branch metadata");
1017 RawCanFrame raw;
1018 raw.canId = 1U;
1019 raw.data = std::vector<std::uint8_t>(9U, 255U);
1020 DecodedFrameValue result = DbcDecoder().Decode(runtime, raw);
1021 CHECK(result.signals[0].valid && result.signals[1].valid &&
1022 std::get<std::uint64_t>(result.signals[1].rawValue) == std::numeric_limits<std::uint64_t>::max(),
1023 "Motorola uint64 selector compares raw value before zero scaling");
1024 raw.data[7] = 254U;
1025 result = DbcDecoder().Decode(runtime, raw);
1026 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive,
1027 "Adjacent large integers remain distinct");
1028 std::vector<DecodeFrame> definitions = runtime.GetFrames();
1029 definitions[0].signals[1].length = 0U;
1030 QVERIFY_THROWS_EXCEPTION(std::invalid_argument, runtime = DecodeDatabase(definitions));
1031 definitions[0].signals.pop_back();
1032 QVERIFY_THROWS_EXCEPTION(std::invalid_argument, runtime = DecodeDatabase(definitions));
1033 fixture.Write("BO_ 1 Frame: 2 ECU\n\n"
1034 "SG_ Branch m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1035 "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n");
1036 bool rejected = false;
1037 try {
1038 DbcParser().ParseFile(fixture.path.string());
1039 } catch (const std::runtime_error &error) {
1040 rejected = std::string(error.what()).find(fixture.path.string() + ":3: ") == 0U;
1041 }
1042 CHECK(rejected, "Deferred validation reports branch source line");
1043 fixture.Write("BO_ 1 Frame: 2 ECU\n"
1044 "SG_ A : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1045 "SG_ B : 15|8@0+ (1,0) [0|255] \"\" ECU\n");
1046 rejected = false;
1047 try {
1048 DbcParser().ParseFile(fixture.path.string());
1049 } catch (const std::runtime_error &) {
1050 rejected = true;
1051 }
1052 CHECK(rejected, "Ordinary overlapping signals rejected");
1053}
1054
1055void DbcParserTest::invalidSignals() {
1056 Fixture fixture;
1057 const std::string ordinary = SignalLine("0", "8", "1+", "1", "0", "0", "255");
1058 for (const std::string value: {"-1", "+1", "0junk", "0x10", "1.5",
1059 "4294967296", "999999999999999999999"}) {
1060 RejectSignal(fixture, SignalLine(value, "8", "1+", "1", "0", "0", "255"));
1061 RejectSignal(fixture, SignalLine("0", value, "1+", "1", "0", "0", "255"));
1062 }
1063 for (const std::string length: {"0", "65"})
1064 RejectSignal(fixture, SignalLine("0", length, "1+", "1", "0", "0", "255"));
1065 for (const std::string order: {"2+", "9-", "1", "1x", "1++", "01+"})
1066 RejectSignal(fixture, SignalLine("0", "8", order, "1", "0", "0", "255"));
1067 for (const std::string value: {"1junk", "nan", "NaN", "inf", "-inf", "1e999", "1e-999", "0x1p2", "", "1e", "1.2.3"}) {
1068 RejectSignal(fixture, SignalLine("0", "8", "1+", value, "0", "0", "255"));
1069 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", value, "0", "255"));
1070 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", value, "255"));
1071 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "0", value));
1072 }
1073 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "2", "1"));
1074 for (const std::string name: {"", "Two Names", "Value m1M"}) {
1075 std::string line = ordinary;
1076 line.replace(4U, 5U, name);
1077 RejectSignal(fixture, line);
1078 }
1079 for (const std::string broken: {
1080 "SG_", "SG_ Value", "SG_ Value : 0|8@1+ (1,0) [0|255] ECU",
1081 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\"",
1082 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU,",
1083 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU extra"})
1084 RejectSignal(fixture, broken);
1085 RejectSignal(fixture, SignalLine("64", "1", "1+", "1", "0", "0", "1"));
1086 RejectSignal(fixture, SignalLine("63", "2", "1+", "1", "0", "0", "3"));
1087 RejectSignal(fixture, SignalLine("56", "2", "0+", "1", "0", "0", "3"));
1088 RejectSignal(fixture, SignalLine("4294967295", "64", "1+", "1", "0", "0", "1"));
1089 RejectSignal(fixture, ordinary, 0U);
1090 for (const char delimiter: {'|', '@', '(', ')', '[', ']', '"'}) {
1091 std::string line = ordinary;
1092 line.erase(line.find(delimiter), 1U);
1093 RejectSignal(fixture, line);
1094 }
1095}
1096
1097void DbcParserTest::validSignals() {
1098 Fixture fixture;
1099 const DbcParser signalParser;
1100 fixture.Write("BO_ 1 Frame: 8 ECU\n" +
1101 SignalLine("0", "8", "1+", "0", "0", "0", "0") + "\n" +
1102 SignalLine("8", "8", "1-", "-1", "0", "-255", "0", "Negative") + "\n");
1103 const DbcDatabase factors = signalParser.ParseFile(fixture.path.string());
1104 CHECK(factors.frames[0].signals.size() == 2U &&
1105 factors.frames[0].signals[0].factor == 0.0 &&
1106 factors.frames[0].signals[1].factor == -1.0,
1107 "Zero and negative factors and equal range boundaries");
1108
1109 struct SignalCase {
1110 const char *start;
1111 const char *length;
1112 const char *order;
1113 unsigned payload;
1114 };
1115 const SignalCase validSignals[] = {
1116 {"0", "1", "1+", 1U},
1117 {"7", "1", "0-", 1U},
1118 {"0", "64", "1+", 8U},
1119 {"7", "64", "0-", 8U},
1120 {"63", "1", "1+", 8U},
1121 {"56", "1", "0+", 8U},
1122 {"3", "12", "0+", 2U},
1123 {"2039", "1", "1+", 255U}};
1124 for (const SignalCase &test: validSignals) {
1125 fixture.Write("BO_ 1 Frame: " + std::to_string(test.payload) + " ECU\n" +
1126 SignalLine(test.start, test.length, test.order, ".5e+1", "-2.5E-1", "-1.", "+2e2") + "\n");
1127 try {
1128 const DbcDatabase parsed = signalParser.ParseFile(fixture.path.string());
1129 const SignalInfo &signal = parsed.frames.at(0).signals.at(0);
1130 QCOMPARE(signal.startBit, static_cast<std::uint32_t>(std::stoul(test.start)));
1131 QCOMPARE(signal.length, static_cast<std::uint32_t>(std::stoul(test.length)));
1132 QCOMPARE(signal.isLittleEndian, test.order[0] == '1');
1133 QCOMPARE(signal.isSigned, test.order[1] == '-');
1134 QCOMPARE(signal.factor, 5.0);
1135 QCOMPARE(signal.offset, -0.25);
1136 QCOMPARE(signal.minimum, -1.0);
1137 QCOMPARE(signal.maximum, 200.0);
1138 } catch (const std::exception &error) { CHECK(false, error.what()); }
1139 }
1140 fixture.Write("BO_ 1 Frame: 8 ECU\n"
1141 "SG_\tValue\t:\t0 | 8 @1+ ( 1 , 0 ) [ 0 | 255 ] \"rpm\" ECU1,\tECU2\n");
1142 try {
1143 const DbcDatabase parsed = signalParser.ParseFile(fixture.path.string());
1144 const SignalInfo &signal = parsed.frames.at(0).signals.at(0);
1145 CHECK(signal.name == "Value" && signal.unit == "rpm" &&
1146 signal.receivers == std::vector<std::string>({"ECU1", "ECU2"}),
1147 "Signal tabs, spaces, and receiver list");
1148 } catch (const std::exception &error) { CHECK(false, error.what()); }
1149}
1150
1151void DbcParserTest::longSignalDefinitions_data() {
1152 QTest::addColumn<std::string>("line");
1153 QTest::addColumn<bool>("valid");
1154 QTest::addColumn<std::string>("unit");
1155 const std::string longText(50000U, 'x');
1156 const std::string prefix = "SG_ Value : 0|8@1+ (1,0) [0|255] ";
1157 QTest::newRow("long-unit") << prefix + "\"" + longText + "\" ECU" << true << longText;
1158 QTest::newRow("unterminated-unit") << prefix + "\"" + longText << false << std::string();
1159 QTest::newRow("missing-receiver-separator") << prefix + "\"" + longText + "\"ECU" << false << std::string();
1160 QTest::newRow("long-name") << SignalLine("0", "8", "1+", "1", "0", "0", "255", longText) << true << std::string();
1161 QTest::newRow("long-receiver") << prefix + "\"\" " + longText << true << std::string();
1162 std::string receivers = "ECU";
1163 for (unsigned index = 0U; index < 10000U; ++index)
1164 receivers += ",ECU";
1165 QTest::newRow("many-receivers") << prefix + "\"\" " + receivers << true << std::string();
1166 QTest::newRow("trailing-receiver-comma") << prefix + "\"\" " + receivers + "," << false << std::string();
1167 QTest::newRow("long-factor") << SignalLine("0", "8", "1+", std::string(50000U, '0') + "1", "0", "0", "255") << true << std::string();
1168 QTest::newRow("invalid-long-factor") << SignalLine("0", "8", "1+", std::string(50000U, '0') + "1x", "0", "0", "255") << false << std::string();
1169 QTest::newRow("overflowing-start-bit") << SignalLine(std::string(50000U, '9'), "8", "1+", "1", "0", "0", "255") << false << std::string();
1170 QTest::newRow("missing-colon") << "SG_ " + longText << false << std::string();
1171}
1172
1173void DbcParserTest::longSignalDefinitions() {
1174 QFETCH(std::string, line);
1175 QFETCH(bool, valid);
1176 QFETCH(std::string, unit);
1177 Fixture fixture;
1178 fixture.Write("BO_ 1 Frame: 1 ECU\n" + line + "\n");
1179 const DbcParser parser;
1180 const DbcParseResult result = parser.ParseFileWithDiagnostics(fixture.path.string());
1181 QCOMPARE(result.HasErrors(), !valid);
1182 if (valid) {
1183 QVERIFY(result.diagnostics.empty());
1184 const DbcDatabase database = parser.ParseFile(fixture.path.string());
1185 QCOMPARE(database.frames.size(), std::size_t(1));
1186 QCOMPARE(database.frames[0].signals.size(), std::size_t(1));
1187 QCOMPARE(database.frames[0].signals[0].unit, unit);
1188 QCOMPARE(result.database.frames[0].signals[0].unit, unit);
1189 QCOMPARE(database.frames[0].signals[0].factor, 1.0);
1190 QVERIFY(!database.frames[0].signals[0].receivers.empty());
1191 } else {
1192 QVERIFY(result.database.frames.empty());
1193 QCOMPARE(result.diagnostics.size(), std::size_t(1));
1194 QCOMPARE(result.diagnostics[0].code, DbcDiagnosticCode::InvalidStatement);
1195 QCOMPARE(result.diagnostics[0].lineNumber, std::size_t(2));
1196 QCOMPARE(result.diagnostics[0].filePath, fixture.path.string());
1197 RejectSignal(fixture, line, 1U);
1198 }
1199}
1200
1201void DbcParserTest::CheckPhysicalNumbers() {
1202 Fixture fixture;
1203 struct PhysicalCase {
1204 const char *factor;
1205 const char *offset;
1206 const char *minimum;
1207 const char *maximum;
1208 double expectedFactor;
1209 double expectedOffset;
1210 double expectedMinimum;
1211 double expectedMaximum;
1212 };
1213 const PhysicalCase valid[] = {
1214 {"+1.25", "-0.5", "-2.75", "255.5", 1.25, -0.5, -2.75, 255.5},
1215 {"+.5e+1", "-2.5E-1", "-1.", "+2e2", 5.0, -0.25, -1.0, 200.0},
1216 {"0e-999", "+0.", "-.0", ".0e+999", 0.0, 0.0, 0.0, 0.0},
1217 {"1.7976931348623157e308", "2.2250738585072014e-308", "-1e308", "+1e308",
1218 std::numeric_limits<double>::max(), std::numeric_limits<double>::min(), -1e308, 1e308}};
1219 for (const PhysicalCase &test: valid) {
1220 fixture.Write("BO_ 1 Frame: 1 ECU\n" +
1221 SignalLine("0", "8", "1+", test.factor, test.offset, test.minimum, test.maximum) + "\n");
1222 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
1223 QVERIFY(!result.HasErrors());
1224 QVERIFY(result.diagnostics.empty());
1225 QCOMPARE(result.database.frames.size(), std::size_t(1));
1226 const SignalInfo &signal = result.database.frames[0].signals[0];
1227 QCOMPARE(signal.factor, test.expectedFactor);
1228 QCOMPARE(signal.offset, test.expectedOffset);
1229 QCOMPARE(signal.minimum, test.expectedMinimum);
1230 QCOMPARE(signal.maximum, test.expectedMaximum);
1231 }
1232
1233 const char *invalid[] = {"1,5", "1.234,5", "1,234.5", "1junk", "nan", "inf", "-inf",
1234 "1e999", "1e-999", "1e-320", "-1e-320", "0x1p2", "1e", "1.2.3"};
1235 for (const char *value: invalid) {
1236 RejectSignal(fixture, SignalLine("0", "8", "1+", value, "0", "0", "255"));
1237 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", value, "0", "255"));
1238 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", value, "255"));
1239 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "0", value));
1240 }
1241 RejectSignal(fixture, SignalLine("0", "8", "1+", "1.25", "0", "2.5", "1.5"));
1242}
1243
1244void DbcParserTest::physicalNumbersCppLocale() {
1245 const ApplicationLocaleGuard guard;
1246 std::locale::global(std::locale(std::locale::classic(), new CommaDecimalPunctuation));
1247 QCOMPARE(std::use_facet<std::numpunct<char>>(std::locale()).decimal_point(), ',');
1248 CheckPhysicalNumbers();
1249 QCOMPARE(std::use_facet<std::numpunct<char>>(std::locale()).decimal_point(), ',');
1250}
1251
1252void DbcParserTest::physicalNumbersCLocale() {
1253 const ApplicationLocaleGuard guard;
1254 bool found = false;
1255 const char *names[] = {"de_DE.UTF-8", "de_DE.utf8", "fr_FR.UTF-8", "fr_FR.utf8",
1256 "German_Germany.1252", "French_France.1252"};
1257 for (const char *name: names) {
1258 if (std::setlocale(LC_NUMERIC, name) && std::string(std::localeconv()->decimal_point) == ",") {
1259 found = true;
1260 break;
1261 }
1262 }
1263 if (!found)
1264 QSKIP("No named comma-decimal C locale is installed; generate de_DE.UTF-8 and set LOCPATH.");
1265 QCOMPARE(std::string(std::localeconv()->decimal_point), std::string(","));
1266 CheckPhysicalNumbers();
1267 QCOMPARE(std::string(std::localeconv()->decimal_point), std::string(","));
1268}
1269
1270void DbcParserTest::dispatch() {
1271 CountingHandler::instances = 0;
1272 CountingHandler::calls = 0;
1273 DbcDatabase dispatchDatabase;
1274 DbcParseContext context{dispatchDatabase, nullptr};
1275 DbcStatementDispatcher dispatcher;
1276 dispatcher.RegisterType<CountingHandler>("TEST_");
1277 CHECK(dispatcher.Dispatch("TEST_ first", context) &&
1278 dispatcher.Dispatch("TEST_ second", context) &&
1279 CountingHandler::instances == 1 && CountingHandler::calls == 2 &&
1280 dispatchDatabase.frames.size() == 2U,
1281 "Factory dispatch and handler reuse");
1282 CHECK(!dispatcher.Dispatch("TEST_OTHER data", context) &&
1283 !dispatcher.Dispatch("BO_TX_BU_ 1 : ECU;", context) &&
1284 !dispatcher.Dispatch("VAL_TABLE_ States 0 \"Stopped\";", context) &&
1285 CountingHandler::calls == 2,
1286 "Exact and unknown keyword dispatch");
1287}
1288
1289void DbcParserTest::independentSessions() {
1290 Fixture fixture;
1291 const DbcParser parser;
1292 fixture.Write("BO_ 1 First: 8 ECU\n");
1293 CHECK(parser.ParseFile(fixture.path.string()).frames.size() == 1U, "First parse");
1294 fixture.Write("SG_ Speed : invalid\n");
1295 try {
1296 parser.ParseFile(fixture.path.string());
1297 CHECK(false, "Independent parse context");
1298 } catch (const std::runtime_error &error) {
1299 CHECK(std::string(error.what()).find("Signal found before any frame") !=
1300 std::string::npos,
1301 "Independent parse context");
1302 }
1303 fixture.Write("BO_ 2 Second: 8 ECU\n");
1304 const DbcDatabase recovered = parser.ParseFile(fixture.path.string());
1305 CHECK(recovered.frames.size() == 1U && recovered.frames[0].canId == 2U,
1306 "Fresh parse after error");
1307}
1308
1309
1310void DbcParserTest::valueDescriptions() {
1311 Fixture fixture;
1312 fixture.Write(
1313 "VAL_\t256\tState\t0\t\"Off\"\t1 \"On\" 2 \"Error with spaces; and \\\"quotes\\\"\";\n"
1314 "BO_ 256 Status: 8 ECU\n"
1315 "SG_ State : 0|8@1+ (2,10) [0|520] \"\" ECU\n"
1316 "VAL_ 256 State 3 \"\";\n"
1317 "VAL_ 2147483904 State -9223372036854775808 \"Minimum\" -1 \"Negative\" 9223372036854775807 \"Maximum\";\n"
1318 "BO_ 2147483904 Extended: 8 ECU\n"
1319 "SG_ State : 0|64@1- (1,0) [-1e20|1e20] \"\" ECU\n"
1320 "BO_ 2048 Unsigned: 8 ECU\n"
1321 "SG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n"
1322 "VAL_ 2147485696 State 9223372036854775808 \"High\" 18446744073709551615 \"Full\";\n");
1323 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
1324 const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
1325 const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed);
1326 CHECK(tree->GetChild(0)->GetSignal() == nullptr, "Frame node");
1327 for (std::size_t index = 0; index < parsed.frames.size(); ++index) {
1328 CHECK(tree->GetChild(index)->GetChild(0)->GetSignal()->valueDescriptions ==
1329 parsed.frames[index].signals[0].valueDescriptions &&
1330 runtime.GetFrames()[index].signals[0].valueDescriptions ==
1331 parsed.frames[index].signals[0].valueDescriptions,
1332 "All mappings survive both builders");
1333 }
1334 CHECK(tree->GetChild(0)->GetChild(0)->GetSignal()->valueDescriptions ==
1335 runtime.GetFrames()[0].signals[0].valueDescriptions,
1336 "Both builders preserve descriptions");
1337 RawCanFrame frame;
1338 frame.canId = 256;
1339 frame.data.resize(8);
1340 const std::string expected[] = {"Off", "On", "Error with spaces; and \"quotes\"", ""};
1341 for (unsigned value = 0; value < 5; ++value) {
1342 frame.data[0] = value;
1343 const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0];
1344 CHECK(decoded.valid && decoded.physicalValue == value * 2 + 10, "Numeric scaling unchanged");
1345 const std::string *description = decoded.GetValueDescription();
1346 CHECK(value < 4 ? description && *description == expected[value] : !description,
1347 "Raw lookup and unknown value");
1348 }
1349 frame.isExtended = true;
1350 struct Boundary {
1351 std::uint64_t bits;
1352 const char *description;
1353 };
1354 for (const Boundary &test: {Boundary{0x8000000000000000ULL, "Minimum"},
1355 Boundary{0xFFFFFFFFFFFFFFFFULL, "Negative"},
1356 Boundary{0x7FFFFFFFFFFFFFFFULL, "Maximum"}}) {
1357 for (unsigned byte = 0; byte < 8; ++byte)
1358 frame.data[byte] = static_cast<std::uint8_t>(test.bits >> (byte * 8));
1359 const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0];
1360 CHECK(decoded.GetValueDescription() && *decoded.GetValueDescription() == test.description,
1361 "Signed boundaries and distinct extended frame");
1362 }
1363 frame.canId = 2048;
1364 for (const Boundary &test: {Boundary{0x8000000000000000ULL, "High"},
1365 Boundary{0xFFFFFFFFFFFFFFFFULL, "Full"}}) {
1366 for (unsigned byte = 0; byte < 8; ++byte)
1367 frame.data[byte] = static_cast<std::uint8_t>(test.bits >> (byte * 8));
1368 const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0];
1369 CHECK(decoded.GetValueDescription() && *decoded.GetValueDescription() == test.description,
1370 "Unsigned boundaries");
1371 }
1372 CHECK(FindValueDescription(runtime.GetFrames()[0].signals[0].valueDescriptions, std::int64_t(1)) != nullptr,
1373 "Nonnegative signed lookup matches unsigned key");
1374 frame.data.clear();
1375 CHECK(!DbcDecoder().Decode(runtime, frame).signals[0].GetValueDescription(), "Invalid decode has no description");
1376}
1377
1378void DbcParserTest::invalidValueDescriptions() {
1379 Fixture fixture;
1380 const std::string definitions = "BO_ 1 Frame: 8 ECU\nSG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n";
1381 for (const std::string statement: {
1382 "VAL_", "VAL_ 1 State;", "VAL_ 1 State ;", "VAL_ 1 State 0 \"Off\"",
1383 "VAL_ 1 State 0 \"Off\"; junk", "VAL_ 1 State 0 Off;",
1384 "VAL_ 1 State 0 \"Unclosed;", "VAL_ 1 State 0 \"Off\"1 \"On\";",
1385 "VAL_ 1 State 0 \"Off\" 00 \"Duplicate\";",
1386 "VAL_ 1 State 0 \"Off\" -0 \"Duplicate\";",
1387 "VAL_ 1 State 0 \"Off\";\nVAL_ 1 State 0 \"Duplicate\";",
1388 "VAL_ 2 State 0 \"Missing frame\";", "VAL_ 1 Missing 0 \"Missing signal\";",
1389 "VAL_ -1 State 0 \"Bad ID\";", "VAL_ 536870912 State 0 \"Bad ID\";",
1390 "VAL_ 4294967296 State 0 \"Bad ID\";", "VAL_ Environment 0 \"Unsupported\";",
1391 "VAL_ 1 State 0 \"Bad\\q escape\";"}) {
1392 fixture.Write(definitions + statement + '\n');
1393 const int line = statement.find('\n') == std::string::npos ? 3 : 4;
1394 try {
1395 DbcParser().ParseFile(fixture.path.string());
1396 CHECK(false, statement);
1397 } catch (const std::runtime_error &error) {
1398 CHECK(std::string(error.what()).find(fixture.path.string() + ":" + std::to_string(line) + ": ") == 0,
1399 statement + " " + error.what());
1400 }
1401 }
1402 for (const std::string suffix: {
1403 "BO_ 1 Other: 8 ECU\n", // Duplicate frame definition.
1404 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n",// Duplicate signal definition.
1405 ""}) {
1406 fixture.Write("VAL_ 1 " + std::string(suffix.empty() ? "Missing" : "State") +
1407 " 0 \"Description\";\n" + definitions + suffix);
1408 try {
1409 DbcParser().ParseFile(fixture.path.string());
1410 CHECK(false, "Missing reference or duplicate definition");
1411 } catch (const std::runtime_error &error) {
1412 const std::string line = suffix.empty() ? "1" : "4";
1413 CHECK(std::string(error.what()).find(fixture.path.string() + ":" + line + ": ") == 0,
1414 "Error retains the reference or duplicate definition source location");
1415 }
1416 }
1417 for (const std::string number: {"+1", "1x", "0x10", "1.5", "1e2", "--1",
1418 "-9223372036854775809", "18446744073709551616"}) {
1419 fixture.Write("VAL_ 1 State " + number + " \"Bad\";\n" + definitions);
1420 try {
1421 DbcParser().ParseFile(fixture.path.string());
1422 CHECK(false, number);
1423 } catch (const std::runtime_error &error) {
1424 CHECK(std::string(error.what()).find(fixture.path.string() + ":1: ") == 0, number);
1425 }
1426 }
1427}
1428
1429
1430void DbcParserTest::extendedMultiplexing() {
1431 Fixture fixture;
1432 // Forward references, multiple ranges, repeated range statements, and nested siblings.
1433 fixture.Write(
1434 "SG_MUL_VAL_ 1 Child Root 1-2, 4-4;\n"
1435 "SG_MUL_VAL_ 1 Leaf Child 3-5;\n"
1436 "SG_MUL_VAL_ 1 Leaf Child 7-7;\n"
1437 "SG_MUL_VAL_ 1 Sibling Root 6-6;\n"
1438 "SG_MUL_VAL_ 1 Other Sibling 3-5;\n"
1439 "BO_ 1 Frame: 4 ECU\n"
1440 "SG_ Leaf m99 : 16|8@1- (2,1) [-128|127] \"\" ECU\n"
1441 "SG_ Other m0 : 23|8@0+ (1,0) [0|255] \"\" ECU\n"
1442 "SG_ Child m1M : 8|8@1+ (10,3) [0|255] \"\" ECU\n"
1443 "SG_ Sibling m6M : 15|8@0+ (1,0) [0|255] \"\" ECU\n"
1444 "SG_ Root M : 0|8@1+ (10,3) [0|255] \"\" ECU\n"
1445 "SG_ Common : 24|8@1+ (1,0) [0|255] \"\" ECU\n");
1446 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
1447 const DecodeDatabase database = DbcDecodeBuilder().Build(parsed);
1448 const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed);
1449 const Multiplexing &metadata = tree->GetChild(0U)->GetChild(0U)->GetSignal()->multiplexing;
1450 CHECK(metadata.selector == "Child" && metadata.ranges.size() == 2U &&
1451 metadata.ranges[1].minimum == 7U,
1452 "Tree preserves ranges and selector name");
1453 CHECK(database.GetFrames()[0].signals[2].multiplexing.role == MultiplexRole::BranchSelector,
1454 "Runtime preserves nested selector role");
1455 RawCanFrame raw;
1456 raw.canId = 1U;
1457 raw.data = {2U, 4U, 255U, 8U};
1458 DecodedFrameValue result = DbcDecoder().Decode(database, raw);
1459 CHECK(result.signals[0].valid && result.signals[0].physicalValue == -1.0 &&
1460 result.signals[1].status == SignalDecodeStatus::Inactive && result.signals[2].valid &&
1461 result.signals[3].status == SignalDecodeStatus::Inactive && result.signals[5].valid,
1462 "Nested raw conditions select one path and preserve definition order");
1463 const std::vector<SignalKey> selection = {SignalKey(FrameKey(1U, false), "Leaf")};
1464 result = DbcDecoder().Decode(database, raw, selection);
1465 CHECK(result.signals.size() == 1U && result.signals[0].valid,
1466 "Selection reads all ancestors internally");
1467 for (std::uint8_t root: {1U, 2U, 4U}) {
1468 for (std::uint8_t child: {3U, 5U, 7U}) {
1469 raw.data = {root, child, 9U, 8U};
1470 CHECK(DbcDecoder().Decode(database, raw, selection).signals[0].valid,
1471 "Inclusive endpoints and separated ranges");
1472 }
1473 }
1474 raw.data = {6U, 4U, 9U, 8U};
1475 result = DbcDecoder().Decode(database, raw);
1476 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive && result.signals[1].valid &&
1477 result.signals[3].valid,
1478 "Overlapping sibling selector path becomes active");
1479 raw.data = {0U};
1480 CHECK(DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::Inactive,
1481 "Inactive root does not require child selector bytes");
1482 raw.data = {1U};
1483 CHECK(DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::SelectorUnavailable,
1484 "Active root with missing child selector");
1485 raw.data = {1U, 3U};
1486 CHECK(DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::InsufficientPayload,
1487 "Active nested leaf needs its bytes");
1488 raw.data = {1U, 6U};
1489 CHECK(DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::Inactive,
1490 "Gap between explicit ranges does not fall back to mN");
1491 raw.data.clear();
1492 CHECK(DbcDecoder().Decode(database, raw, selection).signals[0].status == SignalDecodeStatus::SelectorUnavailable,
1493 "Missing root selector");
1494
1495 fixture.Write("BO_ 1 Frame: 9 ECU\n"
1496 "SG_ Root M : 0|64@1+ (1,0) [0|1] \"\" ECU\n"
1497 "SG_ Leaf m0 : 64|8@1+ (1,0) [0|255] \"\" ECU\n"
1498 "SG_MUL_VAL_ 1 Leaf Root 18446744073709551614-18446744073709551615;\n");
1499 raw.data.assign(9U, 255U);
1500 CHECK(DbcDecoder().Decode(DbcDecodeBuilder().Build(DbcParser().ParseFile(fixture.path.string())), raw).signals[1].valid,
1501 "64-bit ranges retain exact UINT64_MAX without expansion");
1502
1503 fixture.Write("BO_ 1 Frame: 4 ECU\n"
1504 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1505 "SG_ Second M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1506 "SG_ A m0 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
1507 "SG_ B m0 : 24|8@1+ (1,0) [0|255] \"\" ECU\n"
1508 "SG_MUL_VAL_ 1 A Root 1-1;\nSG_MUL_VAL_ 1 B Second 2-2;\n");
1509 raw.data = {1U, 2U, 9U, 10U};
1510 result = DbcDecoder().Decode(DbcDecodeBuilder().Build(DbcParser().ParseFile(fixture.path.string())), raw);
1511 CHECK(result.signals[2].valid && result.signals[3].valid, "Independent explicit selectors");
1512
1513 fixture.Write("BO_ 1 Deep: 5 ECU\n"
1514 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1515 "SG_ Child m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1516 "SG_ Grandchild m2M : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
1517 "SG_ Leaf : 24|8@1+ (1,0) [0|255] \"\" ECU\n"
1518 "SG_MUL_VAL_ 1 Grandchild Child 2-2;\n"
1519 "SG_MUL_VAL_ 1 Leaf Grandchild 3-3;\n");
1520 const DecodeDatabase deep = DbcDecodeBuilder().Build(DbcParser().ParseFile(fixture.path.string()));
1521 raw.data = {1U, 2U, 3U, 9U, 0U};
1522 CHECK(DbcDecoder().Decode(deep, raw).signals[3].valid, "Three selector levels and an unmarked extended branch");
1523 raw.data = {1U, 0U};
1524 CHECK(DbcDecoder().Decode(deep, raw).signals[3].status == SignalDecodeStatus::Inactive,
1525 "Inactive middle ancestor skips missing descendant bytes");
1526
1527 // Marker fallback also works on a nested selector without its own SG_MUL_VAL_.
1528 fixture.Write("BO_ 1 Frame: 3 ECU\n"
1529 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1530 "SG_ Child m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1531 "SG_ Leaf m2 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
1532 "SG_MUL_VAL_ 1 Leaf Child 2-2;\n");
1533 raw.data = {1U, 2U, 9U};
1534 CHECK(DbcDecoder().Decode(DbcDecodeBuilder().Build(DbcParser().ParseFile(fixture.path.string())), raw).signals[2].valid,
1535 "mNM retains implicit root fallback");
1536}
1537
1538void DbcParserTest::extendedMultiplexingErrors() {
1539 Fixture fixture;
1540 const std::string definitions =
1541 "BO_ 1 Frame: 3 ECU\n"
1542 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1543 "SG_ Child m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1544 "SG_ Leaf m2 : 16|8@1+ (1,0) [0|255] \"\" ECU\n";
1545 for (const std::string statement: {
1546 "SG_MUL_VAL_ 1 Leaf Root 3-2;", "SG_MUL_VAL_ 1 Leaf Root -1-2;",
1547 "SG_MUL_VAL_ 1 Leaf Root 1-256;", "SG_MUL_VAL_ 1 Leaf Root 1-2,;",
1548 "SG_MUL_VAL_ 1 Leaf Root 1-2 trailing;", "SG_MUL_VAL_ 1 Leaf Root 1-2",
1549 "SG_MUL_VAL_ 1 Leaf Root 0-18446744073709551616;",
1550 "SG_MUL_VAL_ 1 Leaf Missing 1-2;", "SG_MUL_VAL_ 1 Missing Root 1-2;",
1551 "SG_MUL_VAL_ 2 Leaf Root 1-2;", "SG_MUL_VAL_ 1 Leaf Leaf 1-2;"}) {
1552 fixture.Write(statement + "\n" + definitions);
1553 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
1554 CHECK(result.HasErrors() && result.database.frames.empty(), statement);
1555 CHECK(result.diagnostics.back().code == DbcDiagnosticCode::InvalidMultiplexing &&
1556 result.diagnostics.back().lineNumber == 1U,
1557 "Extended error retains source statement location");
1558 }
1559 for (const std::string statements: {
1560 "SG_MUL_VAL_ 1 Root Child 1-1;\nSG_MUL_VAL_ 1 Child Root 1-1;\n",
1561 "SG_MUL_VAL_ 1 Child Child 1-1;\n",
1562 "SG_MUL_VAL_ 1 Leaf Root 1-1;\nSG_MUL_VAL_ 1 Leaf Child 1-1;\n"}) {
1563 fixture.Write(definitions + statements);
1564 CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(), "Cyclic or conflicting parents rejected");
1565 }
1566 // Range intersection and inherited root conditions must both protect overlaps.
1567 const std::string overlapping =
1568 "BO_ 1 Frame: 4 ECU\n"
1569 "SG_ Root M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
1570 "SG_ Child m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
1571 "SG_ A m0 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
1572 "SG_ B m0 : 23|8@0+ (1,0) [0|255] \"\" ECU\n"
1573 "SG_MUL_VAL_ 1 A Child 2-4;\n"
1574 "SG_MUL_VAL_ 1 Child Root 1-1;\n";
1575 for (const std::string condition: {"SG_MUL_VAL_ 1 B Child 4-6;\n",
1576 "SG_MUL_VAL_ 1 B Root 1-1;\n"}) {
1577 fixture.Write(overlapping + condition);
1578 CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
1579 "Overlapping ranges or compatible ancestors cannot share bits");
1580 }
1581 fixture.Write(overlapping + "SG_MUL_VAL_ 1 B Child 5-6;\n");
1582 CHECK(!DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
1583 "Disjoint adjacent ranges may share bits");
1584 fixture.Write(overlapping + "SG_ Second M : 24|8@1+ (1,0) [0|255] \"\" ECU\n"
1585 "SG_MUL_VAL_ 1 B Second 2-4;\n");
1586 const DbcParseResult independent = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
1587 CHECK(independent.HasErrors() && independent.diagnostics.back().message ==
1588 "Signals overlap without mutually exclusive multiplexing conditions.",
1589 "Independent selector conditions do not imply exclusion");
1590
1591 // Validate manually constructed runtime graphs with the same rules.
1592 fixture.Write(definitions + "SG_MUL_VAL_ 1 Leaf Child 2-3;\n");
1593 const DecodeDatabase valid = DbcDecodeBuilder().Build(DbcParser().ParseFile(fixture.path.string()));
1594 for (unsigned kind = 0U; kind < 6U; ++kind) {
1595 std::vector<DecodeFrame> frames = valid.GetFrames();
1596 Multiplexing &leaf = frames[0].signals[2].multiplexing;
1597 switch (kind) {
1598 case 0:
1599 leaf.selector = "Missing";
1600 break;
1601 case 1:
1602 leaf.ranges[0] = {4U, 3U};
1603 break;
1604 case 2:
1605 leaf.ranges[0] = {0U, 256U};
1606 break;
1607 case 3:
1608 leaf.ranges.clear();
1609 break;
1610 case 4:
1611 frames[0].signals[0].multiplexing.selector = "Child";
1612 frames[0].signals[0].multiplexing.ranges = {{1U, 1U}};
1613 break;
1614 case 5:
1615 frames[0].signals[1].valueType = ValueType::Signed;
1616 break;
1617 }
1618 QVERIFY_THROWS_EXCEPTION(std::invalid_argument, (void) DecodeDatabase(frames));
1619 }
1620}
1621
1622int RunParserTests(int argc, char **argv) {
1623 DbcParserTest tests;
1624 return QTest::qExec(&tests, argc, argv);
1625}
1626
1627#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.
Definition dbc_decoder.h:87
DecodedFrameValue Decode(const DecodeDatabase &database, const RawCanFrame &frame) const
Decode one raw CAN frame.
Minimal DBC parser.
Definition dbc_parser.h:43
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.
Metadata tree builder.
Parsed DBC content stored in a simple internal form.
std::map< std::string, AttributeDefinition > attributeDefinitions
std::vector< FrameInfo > frames
AttributeMap attributes
std::vector< AttributeAssignment > attributeAssignments
Diagnostic message and physical source location.
std::string message
DbcDiagnosticSeverity severity
DbcDiagnosticCode code
std::string filePath
std::size_t lineNumber
Parsed database and diagnostics; the database is empty on error.
std::vector< DbcDiagnostic > diagnostics
bool HasErrors() const
Return whether any diagnostic has error severity.
DbcDatabase database
Fully decoded frame.
Definition dbc_decoder.h:74
std::shared_ptr< const DecodeFrame > definition
Definition dbc_decoder.h:75
FrameDecodeStatus status
Definition dbc_decoder.h:79
One decoded signal value.
Definition dbc_decoder.h:55
const std::string * GetValueDescription() const
Definition dbc_decoder.h:64
Describes one CAN frame from a DBC file.
Definition frame_info.h:25
bool isExtended
Definition frame_info.h:28
std::string transmitter
Definition frame_info.h:32
std::uint8_t dlc
Definition frame_info.h:31
std::string name
Definition frame_info.h:26
std::uint32_t canId
Definition frame_info.h:27
Stable frame identifier: normalized CAN ID and frame type.
Basic markers and optional explicit extended multiplexing condition.
std::vector< MultiplexRange > ranges
std::string selector
Raw CAN frame used for runtime or trace decoding.
Definition dbc_decoder.h:25
std::vector< std::uint8_t > data
Definition dbc_decoder.h:28
std::uint32_t canId
Definition dbc_decoder.h:26
bool isExtended
Definition dbc_decoder.h:27
Describes one signal inside a DBC frame.
Definition signal_info.h:25
std::uint32_t length
Definition signal_info.h:29
std::string unit
Definition signal_info.h:36
std::vector< std::string > receivers
Definition signal_info.h:37
std::string name
Definition signal_info.h:27
bool isSigned
Definition signal_info.h:31
double offset
Definition signal_info.h:33
std::uint32_t startBit
Definition signal_info.h:28
double minimum
Definition signal_info.h:34
double maximum
Definition signal_info.h:35
double factor
Definition signal_info.h:32
bool isLittleEndian
Definition signal_info.h:30