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 invalidAttributes();
93 void attributeNumbers();
94 void invalidAttributeNumbers_data();
95 void invalidAttributeNumbers();
96 void attributeWarnings();
97 void diagnostics();
98 void diagnosticErrors();
99 void diagnosticUnsupportedStatements();
100 void diagnosticNamespaceBoundaries();
101 void diagnosticFormatting();
102 void multiplexing();
103 void multiplexingBoundaries();
104 void validFrames();
105 void invalidFrames();
106 void duplicateFrames();
107 void duplicateSignals();
108 void distinctDefinitions();
109 void comments();
110 void invalidComments_data();
111 void invalidComments();
112 void integration();
113 void invalidSignals();
114 void validSignals();
115 void physicalNumbersCppLocale();
116 void physicalNumbersCLocale();
117 void dispatch();
118 void independentSessions();
119 void valueDescriptions();
120 void invalidValueDescriptions();
121};
122
123void DbcParserTest::attributes() {
124 Fixture fixture;
125 fixture.Write(
126 "BA_ \"GenMsgCycleTime\" BO_ 1 25;\n"
127 "BA_ \"VFrameFormat\" BO_ 2147483649 1;\n"
128 "BA_ \"Label\" SG_ 1 Value \"a\\\"b\\\\c\";\n"
129 "BA_ \"NodeLabel\" BU_ ECU \"node\";\n"
130 "BA_ \"Environment\" EV_ Env 2;\n"
131 "BA_DEF_DEF_ \"GenMsgCycleTime\" 100;\n"
132 "BA_DEF_DEF_ \"VFrameFormat\" \"StandardCAN\";\n"
133 "BA_DEF_DEF_ \"Label\" \"default\";\n"
134 "BA_DEF_DEF_ \"BusLabel\" \"CAN\";\n"
135 "BA_DEF_DEF_ \"Ratio\" 1.5e-1;\n"
136 "BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 10000;\n"
137 "BA_DEF_ BO_ \"VFrameFormat\" ENUM \"StandardCAN\",\"ExtendedCAN\",\"StandardCAN_FD\";\n"
138 "BA_DEF_ SG_ \"Label\" STRING;\n"
139 "BA_DEF_ \"BusLabel\" STRING;\n"
140 "BA_DEF_ BO_ \"Ratio\" FLOAT -1 1;\n"
141 "BA_DEF_ BU_ \"NodeLabel\" STRING;\n"
142 "BA_DEF_ EV_ \"Environment\" HEX 0 255;\n"
143 "BO_ 1 Standard: 1 ECU\n"
144 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
145 "BO_ 2147483649 Extended: 1 ECU\n"
146 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n");
147 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
148 QVERIFY(!result.HasErrors());
149 QVERIFY(result.diagnostics.empty());
150 const DbcDatabase &database = result.database;
151 QCOMPARE(database.attributeDefinitions.size(), std::size_t(7));
152 QCOMPARE(database.attributeAssignments.size(), std::size_t(5));
153 QCOMPARE(std::get<std::string>(database.attributes.at("BusLabel")), std::string("CAN"));
154 QCOMPARE(database.frames[0].cycleTimeMs.value(), 25U);
155 QCOMPARE(database.frames[1].cycleTimeMs.value(), 100U);
156 QCOMPARE(database.frames[1].frameFormat.value(), std::string("ExtendedCAN"));
157 QVERIFY(!database.frames[1].hasPgn);
158 QCOMPARE(std::get<double>(database.frames[0].attributes.at("Ratio")), 0.15);
159 QCOMPARE(std::get<std::string>(database.frames[0].signals[0].attributes.at("Label")), std::string("a\"b\\c"));
160 QCOMPARE(std::get<std::string>(database.frames[1].signals[0].attributes.at("Label")), std::string("default"));
161 const DecodeDatabase runtime = DbcDecodeBuilder().Build(database);
162 QCOMPARE(runtime.GetFrames()[0].cycleTimeMs.value(), 25U);
163 QCOMPARE(runtime.GetFrames()[1].frameFormat.value(), std::string("ExtendedCAN"));
164 QCOMPARE(runtime.GetFrames()[0].signals[0].attributes, database.frames[0].signals[0].attributes);
165 const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(database);
166 QCOMPARE(tree->GetChild(0)->GetFrame()->cycleTimeMs.value(), 25U);
167 QCOMPARE(tree->GetChild(0)->GetChild(0)->GetSignal()->attributes, database.frames[0].signals[0].attributes);
168 fixture.Write("BA_DEF_ BO_ \"Period\" INT 0 10;\nBA_DEF_DEF_ \"Period\" 1;\n"
169 "BA_ \"Period\" BO_ 1 2;\nBA_ \"Period\" BO_ 1 3;\nBO_ 1 F: 0 ECU\n");
170 const DbcDatabase repeated = DbcParser().ParseFile(fixture.path.string());
171 QCOMPARE(std::get<std::int64_t>(repeated.frames[0].attributes.at("Period")), std::int64_t(3));
172
173 fixture.Write("BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 4294967295;\n"
174 "BA_DEF_DEF_ \"GenMsgCycleTime\" 0;\n"
175 "BA_ \"GenMsgCycleTime\" BO_ 1 4294967295;\n"
176 "BA_DEF_ \"Wide\" INT -9223372036854775808 9223372036854775807;\n"
177 "BA_DEF_DEF_ \"Wide\" -9223372036854775808;\n"
178 "BA_DEF_ SG_ \"State\" ENUM \"off\",\"on\";\n"
179 "BA_DEF_DEF_ \"State\" 1;\n"
180 "BO_ 1 F: 0 ECU\nBO_ 2 Empty: 0 ECU\n");
181 const DbcDatabase boundaries = DbcParser().ParseFile(fixture.path.string());
182 QCOMPARE(boundaries.frames[0].cycleTimeMs.value(), std::numeric_limits<std::uint32_t>::max());
183 QCOMPARE(boundaries.frames[1].cycleTimeMs.value(), 0U);
184 QVERIFY(!boundaries.frames[0].frameFormat);
185 QCOMPARE(std::get<std::int64_t>(boundaries.attributes.at("Wide")), std::numeric_limits<std::int64_t>::min());
186 QCOMPARE(std::get<std::int64_t>(*boundaries.attributeDefinitions.at("State").defaultValue), std::int64_t(1));
187}
188
189void DbcParserTest::invalidAttributes() {
190 Fixture fixture;
191 const std::vector<std::string> invalid = {
192 "BA_DEF_ BO_ \"A\"STRING;",
193 "BA_DEF_ BO_\"A\" STRING;",
194 "BA_DEF_ BO_ \"A\" \"STRING\";",
195 "BA_DEF_ BO_ A STRING;",
196 "BA_DEF_ ZZ_ \"A\" STRING;",
197 "BA_DEF_ BO_ \"A\" BLOB;",
198 "BA_DEF_ BO_ \"A\" FLOAT 2 1;",
199 "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 0;",
200 "BA_DEF_ BO_ \"A\" FLOAT 0 1;\nBA_ \"A\" BO_ 1 2;",
201 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ -1 0;",
202 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 4294967296 0;",
203 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 536870912 0;",
204 "BA_DEF_ BO_ \"A\" INT 2 1;",
205 "BA_DEF_ BO_ \"A\" FLOAT nan 1;",
206 "BA_DEF_ BO_ \"A\" FLOAT 0 inf;",
207 "BA_DEF_ BO_ \"A\" INT 0 9223372036854775808;",
208 "BA_DEF_ BO_ \"A\" STRING; trailing",
209 "BA_DEF_ BO_ \"A\" ENUM;",
210 "BA_DEF_ BO_ \"A\" ENUM \"one\",;",
211 "BA_DEF_ BO_ \"A\" ENUM \"one\",\"one\";",
212 "BA_DEF_ BO_ \"VFrameFormat\" STRING;",
213 "BA_DEF_ SG_ \"GenMsgCycleTime\" INT 0 10;",
214 "BA_ \"Unknown\" BO_ 1 0;",
215 "BA_DEF_DEF_ \"Unknown\" 0;",
216 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 1 2;",
217 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 1 \"0\";",
218 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" SG_ 1 S 0;",
219 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_ \"A\" BO_ 2 0;",
220 "BA_DEF_ SG_ \"A\" INT 0 1;\nBA_ \"A\" SG_ 1 Missing 0;",
221 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_DEF_ BO_ \"A\" INT 0 1;",
222 "BA_DEF_ BO_ \"A\" INT 0 1;\nBA_DEF_DEF_ \"A\" 0;\nBA_DEF_DEF_ \"A\" 1;",
223 "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 \"bad\\q\";",
224 "BA_DEF_ BO_ \"A\" STRING;\nBA_ \"A\" BO_ 1 \"unterminated;",
225 "BA_DEF_ BO_ \"A\" ENUM \"one\";\nBA_DEF_DEF_ \"A\" \"missing\";",
226 "BA_DEF_ BO_ \"A\" ENUM \"one\";\nBA_ \"A\" BO_ 1 1;",
227 "BA_DEF_ BO_ \"GenMsgCycleTime\" INT -1 4294967296;\nBA_DEF_DEF_ \"GenMsgCycleTime\" -1;",
228 "BA_DEF_ BO_ \"GenMsgCycleTime\" INT 0 4294967296;\nBA_ \"GenMsgCycleTime\" BO_ 1 4294967296;"};
229 for (const std::string &statement: invalid) {
230 fixture.Write("BO_ 1 F: 0 ECU\n" + statement + '\n');
231 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
232 QVERIFY2(result.HasErrors(), statement.c_str());
233 QVERIFY(result.database.frames.empty());
234 QCOMPARE(result.diagnostics.back().code, DbcDiagnosticCode::InvalidAttribute);
235 QCOMPARE(result.diagnostics.back().lineNumber, std::size_t(2 + std::count(statement.begin(), statement.end(), '\n')));
236 QVERIFY_THROWS_EXCEPTION(std::runtime_error, DbcParser().ParseFile(fixture.path.string()));
237 }
238}
239
240void DbcParserTest::attributeNumbers() {
241 ApplicationLocaleGuard guard;
242 std::locale::global(std::locale(std::locale::classic(), new CommaDecimalPunctuation));
243 Fixture fixture;
244 fixture.Write("BA_DEF_ \"I\" INT -9223372036854775808 +9223372036854775807;\n"
245 "BA_DEF_DEF_ \"I\" -9223372036854775808;\n"
246 "BA_ \"I\" +9223372036854775807;\n"
247 "BA_DEF_ \"H\" HEX -10 +10;\n"
248 "BA_DEF_DEF_ \"H\" +0;\n"
249 "BA_ \"H\" -10;\n"
250 "BA_DEF_ \"F\" FLOAT -1.25e+2 +2.5E2;\n"
251 "BA_DEF_DEF_ \"F\" +.5;\n"
252 "BA_ \"F\" -1.25e+2;\n"
253 "BA_DEF_ BU_ \"NodeDefault\" INT 0 10;\n"
254 "BA_DEF_DEF_ \"NodeDefault\" 5;\n"
255 "BA_DEF_ EV_ \"EnvironmentDefault\" STRING;\n"
256 "BA_DEF_DEF_ \"EnvironmentDefault\" \"default\";\n");
257 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
258 QVERIFY(!result.HasErrors());
259 QVERIFY(result.diagnostics.empty());
260 QCOMPARE(std::get<std::int64_t>(*result.database.attributeDefinitions.at("I").defaultValue), std::numeric_limits<std::int64_t>::min());
261 QCOMPARE(std::get<std::int64_t>(result.database.attributes.at("I")), std::numeric_limits<std::int64_t>::max());
262 QCOMPARE(std::get<std::int64_t>(*result.database.attributeDefinitions.at("H").defaultValue), std::int64_t(0));
263 QCOMPARE(std::get<std::int64_t>(result.database.attributes.at("H")), std::int64_t(-10));
264 QCOMPARE(std::get<double>(*result.database.attributeDefinitions.at("F").defaultValue), 0.5);
265 QCOMPARE(std::get<double>(result.database.attributes.at("F")), -125.0);
266 QCOMPARE(result.database.attributes.size(), std::size_t(3));
267 QCOMPARE(std::get<std::int64_t>(*result.database.attributeDefinitions.at("NodeDefault").defaultValue), std::int64_t(5));
268 QCOMPARE(std::get<std::string>(*result.database.attributeDefinitions.at("EnvironmentDefault").defaultValue), std::string("default"));
269}
270
271void DbcParserTest::invalidAttributeNumbers_data() {
272 QTest::addColumn<QString>("type");
273 QTest::addColumn<QString>("number");
274 const char *types[] = {"INT", "HEX", "FLOAT"};
275 const char *invalid[] = {"+", "-", "++1", "+-1", "--1", "1x", "0x10",
276 "nan", "inf", "-inf", "1e9999", "1e-9999"};
277 for (const char *type: types) {
278 for (const char *number: invalid) {
279 const std::string name = std::string(type) + '-' + number;
280 QTest::newRow(name.c_str()) << QString(type) << QString(number);
281 }
282 }
283 QTest::newRow("integer-overflow") << QString("INT") << QString("9223372036854775808");
284 QTest::newRow("integer-underflow") << QString("INT") << QString("-9223372036854775809");
285 QTest::newRow("integer-fraction") << QString("INT") << QString("1.5");
286}
287
288void DbcParserTest::invalidAttributeNumbers() {
289 QFETCH(QString, type);
290 QFETCH(QString, number);
291 const std::string kind = type.toStdString();
292 const std::string text = number.toStdString();
293 const std::string definition = "BA_DEF_ \"A\" " + kind + " -10 10;\n";
294 const std::string statements[] = {
295 "BA_DEF_ \"A\" " + kind + ' ' + text + " 10;\n",
296 "BA_DEF_ \"A\" " + kind + " -10 " + text + ";\n",
297 definition + "BA_DEF_DEF_ \"A\" " + text + ";\n",
298 definition + "BA_ \"A\" " + text + ";\n"};
299 Fixture fixture;
300 for (const std::string &statement: statements) {
301 fixture.Write(statement);
302 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
303 QVERIFY2(result.HasErrors(), statement.c_str());
304 QVERIFY(result.database.attributeDefinitions.empty());
305 QCOMPARE(result.diagnostics.back().code, DbcDiagnosticCode::InvalidAttribute);
306 QCOMPARE(result.diagnostics.back().lineNumber,
307 static_cast<std::size_t>(std::count(statement.begin(), statement.end(), '\n')));
308 QVERIFY_THROWS_EXCEPTION(std::runtime_error, DbcParser().ParseFile(fixture.path.string()));
309 }
310}
311
312void DbcParserTest::attributeWarnings() {
313 Fixture fixture;
314 fixture.Write("BA_DEF_ BO_ \"VFrameFormat\" ENUM \"StandardCAN\",\"ExtendedCAN\",\"J1939PG\",\"StandardCAN_FD\";\n"
315 "BA_DEF_DEF_ \"VFrameFormat\" \"StandardCAN_FD\";\n"
316 "BA_ \"VFrameFormat\" BO_ 1 2;\n"
317 "BA_ \"VFrameFormat\" BO_ 2147483649 0;\n"
318 "BO_ 1 F: 0 ECU\nBO_ 2147483649 E: 0 ECU\nBO_ 2 Default: 0 ECU\n");
319 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
320 QVERIFY(!result.HasErrors());
321 QCOMPARE(result.diagnostics.size(), std::size_t(3));
322 const std::size_t lines[] = {3U, 4U, 2U};
323 for (std::size_t index = 0; index < 3U; ++index) {
324 QCOMPARE(result.diagnostics[index].code, DbcDiagnosticCode::UnsupportedAttribute);
325 QCOMPARE(result.diagnostics[index].severity, DbcDiagnosticSeverity::Warning);
326 QCOMPARE(result.diagnostics[index].lineNumber, lines[index]);
327 QCOMPARE(result.diagnostics[index].filePath, fixture.path.string());
328 }
329 QCOMPARE(result.database.frames[0].frameFormat.value(), std::string("J1939PG"));
330 QVERIFY(!result.database.frames[0].hasPgn);
331 QVERIFY(result.database.frames[1].isExtended);
332}
333
334void DbcParserTest::diagnostics() {
335 Fixture fixture;
336 fixture.Write("VERSION \"test\"\n"
337 "NS_ :\n"
338 " CM_\n"
339 " BA_\n"
340 " SIG_VALTYPE_\n"
341 " SG_MUL_VAL_\n\n"
342 "BS_:\n"
343 "BU_: ECU\n"
344 "BO_ 1 Frame: 1 ECU\n"
345 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
346 "SIG_VALTYPE_ 1 Value : 1;\n"
347 "BA_REL_ \"SignalType\" SG_ 1 Value 1;\n"
348 "VENDOR_EXTENSION_ data;\n");
349 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
350 CHECK(!result.HasErrors() && result.database.frames.size() == 1U,
351 "Warnings preserve the supported database");
352 QCOMPARE(result.diagnostics.size(), std::size_t(3U));
353 for (std::size_t index = 0U; index < result.diagnostics.size(); ++index) {
354 const DbcDiagnostic &diagnostic = result.diagnostics[index];
355 CHECK(diagnostic.severity == DbcDiagnosticSeverity::Warning &&
356 diagnostic.code == DbcDiagnosticCode::UnsupportedStatement,
357 "Unsupported statements have structured warning severity and code");
358 QCOMPARE(diagnostic.filePath, fixture.path.string());
359 QCOMPARE(diagnostic.lineNumber, 12U + index);
360 CHECK(diagnostic.message.find("ignored") != std::string::npos,
361 "Warning explains unsupported syntax");
362 }
363 CHECK(DbcParser().ParseFile(fixture.path.string()).frames.size() == 1U,
364 "Legacy API accepts files with warnings");
365 fixture.Write("VERSION \"\"\nNS_:\nSIG_VALTYPE_\n\tBA_\n BS_ :\n BU_ : ECU\n");
366 const DbcParseResult headers = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
367 CHECK(headers.diagnostics.empty(), "Common header variants stay quiet");
368 fixture.Write("NS_ :\n BA_\n BO_\n");
369 CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(),
370 "Namespace handling does not swallow a malformed frame");
371}
372
373void DbcParserTest::diagnosticErrors() {
374 Fixture fixture;
375 struct ErrorCase {
376 const char *text;
377 DbcDiagnosticCode code;
378 std::size_t line;
379 };
380 const ErrorCase cases[] = {
381 {"UNKNOWN_ data;\n\nBO_ invalid Frame: 1 ECU\n",
382 DbcDiagnosticCode::InvalidStatement, 3U},
383 {"BO_ 1 Frame: 1 ECU\nSG_MUL_VAL_ 1 Value Mode 1-2;\n",
384 DbcDiagnosticCode::UnsupportedStatement, 2U},
385 {"BO_ 1 Frame: 1 ECU\nVAL_ 1 Missing 0 \"Off\";\n",
386 DbcDiagnosticCode::InvalidValueDescription, 2U},
387 {"BO_ 1 Frame: 1 ECU\nSG_ Value m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
388 DbcDiagnosticCode::InvalidMultiplexing, 2U},
389 {"BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
390 DbcDiagnosticCode::InvalidMultiplexing, 2U},
391 {"VAL_ broken\n", DbcDiagnosticCode::InvalidValueDescription, 1U},
392 {"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
393 DbcDiagnosticCode::InvalidStatement, 1U},
394 {"BO_ 1 Frame: 1 ECU\nBO_ 1 Duplicate: 1 ECU\n",
395 DbcDiagnosticCode::InvalidStatement, 2U},
396 {"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
397 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
398 DbcDiagnosticCode::InvalidStatement, 3U},
399 {"BO_ 1 Frame: 1 ECU\nSG_ Value : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
400 DbcDiagnosticCode::InvalidStatement, 2U},
401 {"BO_ 1 Frame: 1 ECU\nVAL_ 2 Missing 0 \"Off\";\n",
402 DbcDiagnosticCode::InvalidValueDescription, 2U},
403 {"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
404 "VAL_ 1 Value 0 \"Off\";\nVAL_ 1 Value 0 \"Duplicate\";\n",
405 DbcDiagnosticCode::InvalidValueDescription, 4U},
406 {"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"
407 "SG_ Value m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
408 DbcDiagnosticCode::InvalidMultiplexing, 3U},
409 {"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
410 "SG_ Other M : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
411 DbcDiagnosticCode::InvalidMultiplexing, 3U},
412 {"BO_ 1 Frame: 1 ECU\nSG_ First : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
413 "SG_ Second : 7|8@0+ (1,0) [0|255] \"\" ECU\n",
414 DbcDiagnosticCode::InvalidMultiplexing, 3U}
415 };
416 for (const ErrorCase &test : cases) {
417 fixture.Write(test.text);
418 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
419 CHECK(result.HasErrors() && result.database.frames.empty(),
420 "Failed parse never returns a partial database");
421 const DbcDiagnostic &diagnostic = result.diagnostics.back();
422 CHECK(diagnostic.severity == DbcDiagnosticSeverity::Error && diagnostic.code == test.code,
423 "Error has expected severity and code");
424 QCOMPARE(diagnostic.filePath, fixture.path.string());
425 QCOMPARE(diagnostic.lineNumber, test.line);
426 CHECK(!diagnostic.message.empty(), "Error includes a message");
427 const bool hasWarning = std::string(test.text).find("UNKNOWN_") == 0U;
428 QCOMPARE(result.diagnostics.size(), hasWarning ? std::size_t(2U) : std::size_t(1U));
429 try {
430 DbcParser().ParseFile(fixture.path.string());
431 CHECK(false, "Legacy parser must throw");
432 } catch (const std::runtime_error &error) {
433 QCOMPARE(std::string(error.what()), fixture.path.string() + ":" +
434 std::to_string(test.line) + ": " + diagnostic.message);
435 }
436 }
437 const std::string missingPath = fixture.path.string() + ".missing";
438 const DbcParseResult missing = DbcParser().ParseFileWithDiagnostics(missingPath);
439 CHECK(missing.HasErrors() && missing.database.frames.empty(), "Missing file returns error");
440 QCOMPARE(missing.diagnostics.size(), std::size_t(1U));
441 CHECK(missing.diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
442 "Missing file has a distinct code");
443 QCOMPARE(missing.diagnostics[0].lineNumber, std::size_t(0U));
444 QCOMPARE(missing.diagnostics[0].filePath, missingPath);
445 QCOMPARE(missing.diagnostics[0].message, "Failed to open DBC file: " + missingPath);
446 try {
447 DbcParser().ParseFile(missingPath);
448 CHECK(false, "Legacy API throws on missing file");
449 } catch (const std::runtime_error &error) {
450 QCOMPARE(std::string(error.what()), missing.diagnostics[0].message);
451 }
452 const std::string directoryPath = fixture.path.parent_path().string();
453 const DbcParseResult directory = DbcParser().ParseFileWithDiagnostics(directoryPath);
454 CHECK(directory.HasErrors() && directory.database.frames.empty(),
455 "Directory cannot be parsed as a file");
456 QCOMPARE(directory.diagnostics.size(), std::size_t(1U));
457 CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError ||
458 directory.diagnostics[0].code == DbcDiagnosticCode::FileOpenError,
459 "Directory produces a file-level error on supported platforms");
460 QCOMPARE(directory.diagnostics[0].lineNumber, std::size_t(0U));
461 QCOMPARE(directory.diagnostics[0].filePath, directoryPath);
462#ifdef __linux__
463 CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError,
464 "Linux directory input exercises the read-error path");
465 QCOMPARE(directory.diagnostics[0].message, "Failed to read DBC file: " + directoryPath);
466#endif
467 try {
468 DbcParser().ParseFile(directoryPath);
469 CHECK(false, "Legacy API throws on unreadable input");
470 } catch (const std::runtime_error &error) {
471 QCOMPARE(std::string(error.what()), directory.diagnostics[0].message);
472 }
473 fixture.Write("BO_ 1 Frame: 1 ECU\n");
474 CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).diagnostics.empty(),
475 "Diagnostics belong to an independent parse session");
476}
477
478void DbcParserTest::diagnosticUnsupportedStatements() {
479 Fixture fixture;
480 const std::string definitions =
481 "BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (2,10) [10|520] \"V\" ECU\n"
482 "CM_ SG_ 1 Value \"Signal comment\";\nVAL_ 1 Value 1 \"On\";\n";
483 const std::vector<std::string> statements = {
484 "SIG_VALTYPE_ 1 Value : 1;", "SIG_VALTYPE_ 1 Value : 2;",
485 "BA_REL_ \"SignalType\" SG_ 1 Value 1;",
486 "BA_DEF_REL_ BU_SG_REL_ \"Relation\" INT 0 1;",
487 "BA_REL_ \"Relation\" BU_SG_REL_ ECU SG_ 1 Value 1;",
488 "SGTYPE_ Type : 8@1+ (1,0) [0|255] \"\" 0, Table;",
489 "SIG_TYPE_REF_ 1 Value : Type;",
490 "EV_ Environment : 0 [0|1] \"\" 0 1 DUMMY_NODE_VECTOR0 ECU;",
491 "ENVVAR_DATA_ Environment : 4;",
492 "VAL_TABLE_ Table 0 \"Off\" 1 \"On\";",
493 "BO_TX_BU_ 1 : ECU;", "VENDOR_EXTENSION_ data;", "UNKNOWN_"
494 };
495 for (const std::string &statement : statements) {
496 fixture.Write(definitions + statement + '\n');
497 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
498 CHECK(!result.HasErrors(), statement);
499 QCOMPARE(result.diagnostics.size(), std::size_t(1U));
500 const DbcDiagnostic &warning = result.diagnostics[0];
501 CHECK(warning.severity == DbcDiagnosticSeverity::Warning &&
502 warning.code == DbcDiagnosticCode::UnsupportedStatement, statement);
503 QCOMPARE(warning.filePath, fixture.path.string());
504 QCOMPARE(warning.lineNumber, std::size_t(5U));
505 const std::string keyword = statement.substr(0U, statement.find(' '));
506 CHECK(warning.message.find("'" + keyword + "'") != std::string::npos, statement);
507 const DecodeDatabase runtime = DbcDecodeBuilder().Build(result.database);
508 RawCanFrame raw;
509 raw.canId = 1U;
510 raw.data = {1U};
511 const DecodedFrameValue decoded = DbcDecoder().Decode(runtime, raw);
512 CHECK(decoded.valid && decoded.signals.size() == 1U && decoded.signals[0].valid,
513 "Warnings preserve supported signal decoding");
514 QCOMPARE(decoded.signals[0].physicalValue, 12.0);
515 CHECK(decoded.signals[0].GetValueDescription() &&
516 *decoded.signals[0].GetValueDescription() == "On",
517 "Warnings preserve VAL_ descriptions");
518 QCOMPARE(runtime.GetFrames()[0].signals[0].comment, std::string("Signal comment"));
519 }
520}
521
522void DbcParserTest::diagnosticNamespaceBoundaries() {
523 Fixture fixture;
524 const std::vector<std::string> endings = {
525 "BS_\n", "BS_:\n", "BS_ :\n", "BU_\n", "BU_:\n", "BU_ : ECU\n",
526 "BO_ 1 Frame: 1 ECU\n", "VENDOR_EXTENSION_ data;\n"
527 };
528 for (const std::string &ending : endings) {
529 fixture.Write("NS_ :\n\tSG_MUL_VAL_\n\nBA_\n" + ending + "SIG_VALTYPE_\n");
530 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
531 CHECK(!result.HasErrors(), ending);
532 const std::size_t expectedWarnings = ending.find("VENDOR_") == 0U ? 2U : 1U;
533 QCOMPARE(result.diagnostics.size(), expectedWarnings);
534 QCOMPARE(result.diagnostics.back().lineNumber, std::size_t(6U));
535 CHECK(result.diagnostics.back().message.find("SIG_VALTYPE_") != std::string::npos,
536 "Unsupported keyword outside NS_ must produce a warning");
537 }
538 for (const std::string &definition : {std::string("BO_"), std::string("SG_")}) {
539 fixture.Write("NS_:\n\tBA_\n\t" + definition + '\n');
540 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
541 CHECK(result.HasErrors(), "Malformed definition ends NS_ instead of being hidden");
542 QCOMPARE(result.diagnostics.size(), std::size_t(1U));
543 QCOMPARE(result.diagnostics[0].lineNumber, std::size_t(3U));
544 }
545}
546
547void DbcParserTest::diagnosticFormatting() {
548 Fixture fixture;
549 fixture.path += " with spaces";
550 const DbcParser parser;
551 for (const std::string &text : {std::string(), std::string(" \t\r\n\n")}) {
552 fixture.Write(text);
553 const DbcParseResult empty = parser.ParseFileWithDiagnostics(fixture.path.string());
554 CHECK(!empty.HasErrors() && empty.diagnostics.empty() && empty.database.frames.empty(),
555 "Empty and whitespace-only inputs are valid");
556 }
557 fixture.Write("\r\n\tVENDOR_EXTENSION_ data;\r\n\r\nUNKNOWN_");
558 const std::string relativePath = std::filesystem::relative(fixture.path).string();
559 const DbcParseResult warnings = parser.ParseFileWithDiagnostics(relativePath);
560 CHECK(!warnings.HasErrors() && warnings.database.frames.empty(), "Warnings-only input");
561 QCOMPARE(warnings.diagnostics.size(), std::size_t(2U));
562 QCOMPARE(warnings.diagnostics[0].lineNumber, std::size_t(2U));
563 QCOMPARE(warnings.diagnostics[1].lineNumber, std::size_t(4U));
564 for (const DbcDiagnostic &warning : warnings.diagnostics)
565 QCOMPARE(warning.filePath, relativePath);
566
567 fixture.Write("UNKNOWN_ first;\nBO_ invalid Frame: 1 ECU\n"
568 "UNKNOWN_ after_error;\nSG_ broken\n");
569 const DbcParseResult stopped = parser.ParseFileWithDiagnostics(relativePath);
570 CHECK(stopped.HasErrors() && stopped.database.frames.empty(), "First error stops parsing");
571 QCOMPARE(stopped.diagnostics.size(), std::size_t(2U));
572 QCOMPARE(stopped.diagnostics[0].lineNumber, std::size_t(1U));
573 QCOMPARE(stopped.diagnostics[1].lineNumber, std::size_t(2U));
574 QCOMPARE(stopped.diagnostics[1].filePath, relativePath);
575
576 fixture.Write("VAL_ 1 Missing 0 \"Off\";\nBO_ 1 Frame: 1 ECU\nUNKNOWN_ data;\n");
577 const DbcParseResult deferred = parser.ParseFileWithDiagnostics(relativePath);
578 CHECK(deferred.HasErrors() && deferred.database.frames.empty(), "Deferred error rejects database");
579 QCOMPARE(deferred.diagnostics.size(), std::size_t(2U));
580 CHECK(deferred.diagnostics[0].severity == DbcDiagnosticSeverity::Warning &&
581 deferred.diagnostics[1].severity == DbcDiagnosticSeverity::Error,
582 "Deferred errors follow warnings in detection order");
583 QCOMPARE(deferred.diagnostics[0].lineNumber, std::size_t(3U));
584 QCOMPARE(deferred.diagnostics[1].lineNumber, std::size_t(1U));
585 QCOMPARE(deferred.diagnostics[1].filePath, relativePath);
586 fixture.Write("BO_ 1 Frame: 1 ECU");
587 const DbcParseResult recovered = parser.ParseFileWithDiagnostics(relativePath);
588 CHECK(!recovered.HasErrors() && recovered.diagnostics.empty() && recovered.database.frames.size() == 1U,
589 "Same parser has clean state after errors and accepts missing final newline");
590}
591
592void DbcParserTest::validFrames() {
593 Fixture fixture;
594 struct ValidCase {
595 const char *line;
596 std::uint32_t id;
597 bool extended;
598 unsigned length;
599 };
600 const ValidCase valid[] = {
601 {"BO_ 0 Frame: 0 ECU", 0U, false, 0U},
602 {"BO_ 2047 Frame: 8 ECU", 2047U, false, 8U},
603 {"BO_ 2048 Frame: 64 ECU", 2048U, true, 64U},
604 {"BO_ 536870911 Frame: 255 ECU", 0x1FFFFFFFU, true, 255U},
605 {"BO_ 2147483649 Frame: 8 ECU", 1U, true, 8U},
606 {"BO_ 2684354559 Frame: 8 ECU", 0x1FFFFFFFU, true, 8U},
607 {" BO_\t256\tFrame :\t8\tECU \r", 256U, false, 8U}};
608 for (const ValidCase &test: valid) {
609 fixture.Write(std::string(test.line) + '\n');
610 try {
611 const DbcDatabase database = DbcParser().ParseFile(fixture.path.string());
612 QCOMPARE(database.frames.size(), std::size_t(1U));
613 const FrameInfo &frame = database.frames[0];
614 QCOMPARE(frame.name, std::string("Frame"));
615 QCOMPARE(frame.canId, test.id);
616 QCOMPARE(frame.isExtended, test.extended);
617 QCOMPARE(static_cast<unsigned>(frame.dlc), test.length);
618 QCOMPARE(frame.transmitter, std::string("ECU"));
619 } catch (const std::exception &error) { CHECK(false, error.what()); }
620 }
621}
622
623void DbcParserTest::invalidFrames() {
624 Fixture fixture;
625 const char *invalid[] = {
626 "BO_ invalid", "BO_", "BO_ 1", "BO_ 1 : 8 ECU", "BO_ 1 Frame 8 ECU",
627 "BO_ 1 Frame:", "BO_ 1 Frame: 8", "BO_ 1 Frame: 8 ECU extra",
628 "BO_ 1 Two Names: 8 ECU", "BO_ -1 Frame: 8 ECU", "BO_ +1 Frame: 8 ECU",
629 "BO_ 1x Frame: 8 ECU", "BO_ 0x123 Frame: 8 ECU", "BO_ 1.5 Frame: 8 ECU",
630 "BO_ 4294967296 Frame: 8 ECU", "BO_ 999999999999999999999 Frame: 8 ECU",
631 "BO_ 536870912 Frame: 8 ECU", "BO_ 1073741824 Frame: 8 ECU",
632 "BO_ 2684354560 Frame: 8 ECU", "BO_ 4294967295 Frame: 8 ECU",
633 "BO_ 1 Frame: -1 ECU", "BO_ 1 Frame: +8 ECU", "BO_ 1 Frame: 8x ECU",
634 "BO_ 1 Frame: 8.5 ECU", "BO_ 1 Frame: 256 ECU",
635 "BO_ 1 Frame: 4294967296 ECU", "BO_\tinvalid"};
636 for (const char *line: invalid) {
637 fixture.Write(std::string("VERSION \"test\"\n\n") + line + '\n');
638 try {
639 DbcParser().ParseFile(fixture.path.string());
640 CHECK(false, line);
641 } catch (const std::runtime_error &error) {
642 const std::string prefix = fixture.path.string() + ":3: ";
643 CHECK(std::string(error.what()).find(prefix) == 0U &&
644 std::string(error.what()).size() > prefix.size(),
645 line);
646 }
647 }
648}
649
650void DbcParserTest::duplicateFrames() {
651 Fixture fixture;
652 struct DuplicateCase {
653 const char *firstId;
654 const char *duplicateId;
655 const char *message;
656 };
657 const DuplicateCase cases[] = {
658 {"1", "1", "Duplicate standard frame CAN ID: 1"},
659 {"2147483649", "2147483649", "Duplicate extended frame CAN ID: 1"},
660 {"2048", "2147485696", "Duplicate extended frame CAN ID: 2048"},
661 {"2147485696", "2048", "Duplicate extended frame CAN ID: 2048"},
662 {"536870911", "2684354559", "Duplicate extended frame CAN ID: 536870911"}
663 };
664 const DbcParser parser;
665 for (const DuplicateCase &test : cases) {
666 fixture.Write("BO_ " + std::string(test.firstId) + " First: 8 ECU\n"
667 "BO_ 2 Other: 8 ECU\n\n"
668 " BO_\t" + test.duplicateId + "\tSecond: 8 ECU\n");
669 try {
670 parser.ParseFile(fixture.path.string());
671 CHECK(false, "Duplicate frame must be rejected");
672 } catch (const std::runtime_error &error) {
673 QCOMPARE(std::string(error.what()), fixture.path.string() + ":4: " + test.message);
674 }
675 }
676 fixture.Write("BO_ 1 First: 8 ECU\n");
677 QCOMPARE(parser.ParseFile(fixture.path.string()).frames.size(), std::size_t(1U));
678}
679
680void DbcParserTest::duplicateSignals() {
681 Fixture fixture;
682 const std::string first = "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n";
683 const std::string duplicates[] = {
684 first,
685 " SG_\tState : 16|8@1+ (2,1) [0|511] \"V\" ECU\n",
686 "SG_ State m1 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
687 };
688 for (const std::string &duplicate : duplicates) {
689 fixture.Write("BO_ 1 Frame: 8 ECU\n" + first +
690 "SG_ Other : 8|8@1+ (1,0) [0|255] \"\" ECU\n\n" + duplicate);
691 try {
692 DbcParser().ParseFile(fixture.path.string());
693 CHECK(false, "Duplicate signal must be rejected");
694 } catch (const std::runtime_error &error) {
695 QCOMPARE(std::string(error.what()), fixture.path.string() +
696 ":5: Duplicate signal name 'State' in frame 'Frame'.");
697 }
698 }
699}
700
701void DbcParserTest::distinctDefinitions() {
702 Fixture fixture;
703 fixture.Write("BO_ 1 Shared: 1 ECU\n"
704 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
705 "BO_ 2147483649 Shared: 1 ECU\n"
706 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
707 "BO_ 2 Shared: 2 ECU\n"
708 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
709 "SG_ state : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
710 "CM_ BO_ 1 \"Standard\";\n"
711 "CM_ BO_ 2147483649 \"Extended\";\n"
712 "CM_ SG_ 1 State \"Standard state\";\n"
713 "CM_ SG_ 2147483649 State \"Extended state\";\n");
714 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
715 QCOMPARE(parsed.frames.size(), std::size_t(3U));
716 QCOMPARE(parsed.frames[0].canId, parsed.frames[1].canId);
717 CHECK(!parsed.frames[0].isExtended && parsed.frames[1].isExtended,
718 "Standard and extended frames with the same numeric ID remain distinct");
719 QCOMPARE(parsed.frames[0].comment, std::string("Standard"));
720 QCOMPARE(parsed.frames[1].comment, std::string("Extended"));
721 QCOMPARE(parsed.frames[0].signals[0].comment, std::string("Standard state"));
722 QCOMPARE(parsed.frames[1].signals[0].comment, std::string("Extended state"));
723 QCOMPARE(parsed.frames[2].signals.size(), std::size_t(2U));
724 const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
725 RawCanFrame raw;
726 raw.canId = 1U;
727 raw.data = {7U};
728 const DecodedFrameValue standard = DbcDecoder().Decode(runtime, raw);
729 QVERIFY(standard.valid);
730 QCOMPARE(standard.definition->comment, std::string("Standard"));
731 raw.isExtended = true;
732 const DecodedFrameValue extended = DbcDecoder().Decode(runtime, raw);
733 QVERIFY(extended.valid);
734 QCOMPARE(extended.definition->comment, std::string("Extended"));
735}
736
737void DbcParserTest::comments() {
738 Fixture fixture;
739 fixture.Write(
740 "CM_\tBO_\t1\t\"First\";\r\n"
741 "CM_\tSG_\t1\tState\t\" Line one \r\n"
742 "\r\n"
743 "\tBO_ is text; \\\"quoted\\\" and \\\\slash\"\t;\r\n"
744 "CM_ BO_ 2147483649 \"Extended\";\r\n"
745 "CM_ SG_ 2147483649 State \"Extended signal\";\r\n"
746 "BO_ 1 Standard: 1 ECU\r\n"
747 "CM_ BO_ 1 \"Last\";\r\n"
748 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\r\n"
749 "BO_ 2147483649 Extended: 1 ECU\r\n"
750 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU");
751 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
752 QVERIFY(!result.HasErrors());
753 QVERIFY(result.diagnostics.empty());
754 QCOMPARE(result.database.frames.size(), std::size_t(2));
755 QCOMPARE(result.database.frames[0].comment, std::string("Last"));
756 QCOMPARE(result.database.frames[0].signals[0].comment,
757 std::string(" Line one \n\n\tBO_ is text; \"quoted\" and \\slash"));
758 QCOMPARE(result.database.frames[1].comment, std::string("Extended"));
759 QCOMPARE(result.database.frames[1].signals[0].comment, std::string("Extended signal"));
760
761 const std::string ids[][2] = {
762 {"0", "0"},
763 {"2047", "2047"},
764 {"2048", "2147485696"},
765 {"536870911", "2684354559"},
766 {"2684354559", "536870911"}};
767 for (const std::string(&id)[2]: ids) {
768 fixture.Write("CM_ BO_ " + id[0] + " \"Frame\";\nCM_ SG_ " + id[0] +
769 " State \"Signal\";\nBO_ " + id[1] + " Frame: 1 ECU\n"
770 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n");
771 const DbcDatabase database = DbcParser().ParseFile(fixture.path.string());
772 QCOMPARE(database.frames[0].comment, std::string("Frame"));
773 QCOMPARE(database.frames[0].signals[0].comment, std::string("Signal"));
774 }
775 fixture.Write("CM_ BO_ 1 \"Line one\nLine two\";\n"
776 "BO_ 1 Frame: 0 ECU\nUNKNOWN_ value\nBO_ bad Frame: 0 ECU\n");
777 const DbcParseResult following = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
778 QVERIFY(following.HasErrors());
779 QCOMPARE(following.diagnostics.size(), std::size_t(2));
780 QCOMPARE(following.diagnostics[0].lineNumber, std::size_t(4));
781 QCOMPARE(following.diagnostics[1].lineNumber, std::size_t(5));
782}
783
784void DbcParserTest::invalidComments_data() {
785 QTest::addColumn<QString>("statement");
786 const char *ids[] = {"-1", "+1", "1x", "0x1", "4294967296", "536870912",
787 "1073741824", "2684354560", "4294967295"};
788 for (const char *id: ids) {
789 const std::string frameRow = std::string("frame-id-") + id;
790 const std::string signalRow = std::string("signal-id-") + id;
791 QTest::newRow(frameRow.c_str()) << QString::fromStdString("CM_ BO_ " + std::string(id) + " \"Text\";");
792 QTest::newRow(signalRow.c_str()) << QString::fromStdString("CM_ SG_ " + std::string(id) + " State \"Text\";");
793 }
794 QTest::newRow("missing-id") << QStringLiteral("CM_ BO_");
795 QTest::newRow("missing-signal") << QStringLiteral("CM_ SG_ 1 \"Text\";");
796 QTest::newRow("missing-quote") << QStringLiteral("CM_ BO_ 1 Text;");
797 QTest::newRow("unclosed") << QStringLiteral("CM_ SG_ 1 State \"Text\n More");
798 QTest::newRow("invalid-escape") << QStringLiteral("CM_ BO_ 1 \"bad\\q\";");
799 QTest::newRow("dangling-escape") << QStringLiteral("CM_ BO_ 1 \"bad\\");
800 QTest::newRow("missing-semicolon") << QStringLiteral("CM_ SG_ 1 State \"Text\"");
801 QTest::newRow("trailing-text") << QStringLiteral("CM_ BO_ 1 \"Text\"; garbage");
802 QTest::newRow("extra-header") << QStringLiteral("CM_ BO_ 1 extra \"Text\";");
803 QTest::newRow("extra-string") << QStringLiteral("CM_ BO_ 1 \"Text\" \"More\";");
804 QTest::newRow("missing-frame") << QStringLiteral("CM_ BO_ 2 \"Text\";");
805 QTest::newRow("missing-signal-frame") << QStringLiteral("CM_ SG_ 2 State \"Text\";");
806 QTest::newRow("missing-signal-reference") << QStringLiteral("CM_ SG_ 1 Missing \"Text\";");
807 QTest::newRow("different-frame-type") << QStringLiteral("CM_ BO_ 2147483649 \"Text\";");
808 QTest::newRow("missing-multiline-reference") << QStringLiteral("CM_ SG_ 1 Missing \"Text\nMore\";");
809}
810
811void DbcParserTest::invalidComments() {
812 QFETCH(QString, statement);
813 Fixture fixture;
814 fixture.Write("BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n\n" +
815 statement.toStdString());
816 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
817 QVERIFY(result.HasErrors());
818 QVERIFY(result.database.frames.empty());
819 QCOMPARE(result.diagnostics.size(), std::size_t(1));
820 const DbcDiagnostic &error = result.diagnostics[0];
821 QCOMPARE(error.severity, DbcDiagnosticSeverity::Error);
822 QCOMPARE(error.code, DbcDiagnosticCode::InvalidStatement);
823 QCOMPARE(error.filePath, fixture.path.string());
824 QCOMPARE(error.lineNumber, std::size_t(4));
825 QVERIFY(!error.message.empty());
826 try {
827 DbcParser().ParseFile(fixture.path.string());
828 QFAIL("Malformed or unresolved comment must throw.");
829 } catch (const std::runtime_error &exception) {
830 QVERIFY(std::string(exception.what()).find(fixture.path.string() + ":4: ") == 0U);
831 }
832}
833
834void DbcParserTest::integration() {
835 Fixture fixture;
836 fixture.Write(
837 "BO_TX_BU_ 256 : ECU;\nBO_ 256 Frame: 8 ECU\n"
838 " SG_ Speed : 0|16@1+ (0.5,1) [0|100] \"rpm\" ECU\n"
839 "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Speed \"Signal comment\";\n"
840 "BO_ 257 Second: 8 ECU\n");
841 const DbcDatabase database = DbcParser().ParseFile(fixture.path.string());
842 CHECK(database.frames.size() == 2U && database.frames[0].signals.size() == 1U &&
843 database.frames[0].comment == "Frame comment" &&
844 database.frames[0].signals[0].comment == "Signal comment",
845 "Integration");
846 fixture.Write("\nSG_ Speed : invalid\n");
847 try {
848 DbcParser().ParseFile(fixture.path.string());
849 CHECK(false, "Signal error location");
850 } catch (const std::runtime_error &error) {
851 CHECK(std::string(error.what()).find(fixture.path.string() + ":2: ") == 0U,
852 "Signal error location");
853 }
854}
855
856void DbcParserTest::multiplexing() {
857 Fixture fixture;
858 const std::string selector = "SG_ Mode M : 0|8@1+ (10,3) [0|255] \"\" ECU\n";
859 const std::string first = "SG_ First m1 : 8|8@1- (2,0) [-128|127] \"V\" ECU\n";
860 const std::string second = "SG_ Second m2 : 15|8@0+ (1,0) [0|255] \"\" ECU\n";
861 const std::string common = "SG_ Common : 16|8@1+ (1,0) [0|255] \"\" ECU\n";
862 fixture.Write("BO_ 1 Frame: 3 ECU\n" + first + second + common + selector +
863 "VAL_ 1 First -1 \"Negative\";\n");
864 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
865 const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
866 CHECK(parsed.frames[0].signals[3].multiplexing.role == MultiplexRole::Selector,
867 "Selector declared after branches");
868 const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed);
869 CHECK(tree->GetChild(0U)->GetChild(0U)->GetSignal()->multiplexing.value == 1U,
870 "Tree preserves multiplexing");
871 RawCanFrame raw;
872 raw.canId = 1U;
873 raw.data = {1U, 255U, 7U};
874 DecodedFrameValue result = DbcDecoder().Decode(runtime, raw);
875 CHECK(result.signals[0].valid && std::get<std::int64_t>(result.signals[0].rawValue) == -1 &&
876 result.signals[0].physicalValue == -2 && *result.signals[0].GetValueDescription() == "Negative",
877 "Active signed branch and VAL_ use exact raw selector");
878 CHECK(!result.signals[1].valid && result.signals[1].status == SignalDecodeStatus::Inactive &&
879 result.signals[1].GetValueDescription() == nullptr && result.signals[2].valid,
880 "Inactive branch and unconditional signal");
881 raw.data[0] = 2U;
882 result = DbcDecoder().Decode(runtime, raw);
883 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive && result.signals[1].valid &&
884 std::get<std::uint64_t>(result.signals[1].rawValue) == 255U,
885 "Motorola branch becomes active");
886 raw.data = {3U};
887 result = DbcDecoder().Decode(runtime, raw);
888 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive &&
889 result.signals[1].status == SignalDecodeStatus::Inactive &&
890 result.signals[2].status == SignalDecodeStatus::InsufficientPayload,
891 "Unmapped selector leaves branches inactive even with missing bytes");
892 raw.data = {1U};
893 result = DbcDecoder().Decode(runtime, raw);
894 CHECK(result.signals[0].status == SignalDecodeStatus::InsufficientPayload,
895 "Active branch requires payload");
896 raw.data.clear();
897 result = DbcDecoder().Decode(runtime, raw);
898 CHECK(result.signals[0].status == SignalDecodeStatus::SelectorUnavailable &&
899 result.signals[3].status == SignalDecodeStatus::InsufficientPayload,
900 "Unavailable selector does not imply inactive branch");
901
902 const std::vector<std::string> invalid = {
903 first,
904 selector + "SG_ Other M : 16|8@1+ (1,0) [0|255] \"\" ECU\n",
905 "SG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
906 selector + "SG_ TooLarge m256 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
907 selector + first + "SG_ Same m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n",
908 selector + "SG_ Collision m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
909 selector + first + "SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
910 "SG_MUL_VAL_ 1 First Mode 1-2;\n"
911 };
912 for (const std::string &signals : invalid) {
913 fixture.Write("BO_ 1 Frame: 3 ECU\n" + signals);
914 bool rejected = false;
915 try {
916 DbcParser().ParseFile(fixture.path.string());
917 } catch (const std::runtime_error &error) {
918 rejected = std::string(error.what()).find(fixture.path.string() + ":") == 0U;
919 }
920 CHECK(rejected, "Invalid multiplexing rejected with location");
921 }
922 for (const std::string marker : {"m", "m-1", "m+1", "m1M", "m18446744073709551616", "M m1"})
923 RejectSignal(fixture, "SG_ Bad " + marker + " : 8|8@1+ (1,0) [0|255] \"\" ECU");
924}
925
926void DbcParserTest::multiplexingBoundaries() {
927 Fixture fixture;
928 fixture.Write("BO_ 1 Frame: 9 ECU\n"
929 "SG_ Branch m18446744073709551615 : 64|8@1+ (1,0) [0|255] \"\" ECU\n"
930 "SG_ Mode M : 7|64@0+ (0,5) [0|1] \"\" ECU\n");
931 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
932 DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
933 CHECK(runtime.GetFrames()[0].signals[0].multiplexing.value == std::numeric_limits<std::uint64_t>::max(),
934 "Exact uint64 branch metadata");
935 RawCanFrame raw;
936 raw.canId = 1U;
937 raw.data = std::vector<std::uint8_t>(9U, 255U);
938 DecodedFrameValue result = DbcDecoder().Decode(runtime, raw);
939 CHECK(result.signals[0].valid && result.signals[1].valid &&
940 std::get<std::uint64_t>(result.signals[1].rawValue) == std::numeric_limits<std::uint64_t>::max(),
941 "Motorola uint64 selector compares raw value before zero scaling");
942 raw.data[7] = 254U;
943 result = DbcDecoder().Decode(runtime, raw);
944 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive,
945 "Adjacent large integers remain distinct");
946 std::vector<DecodeFrame> definitions = runtime.GetFrames();
947 definitions[0].signals[1].length = 0U;
948 QVERIFY_THROWS_EXCEPTION(std::invalid_argument, runtime = DecodeDatabase(definitions));
949 definitions[0].signals.pop_back();
950 QVERIFY_THROWS_EXCEPTION(std::invalid_argument, runtime = DecodeDatabase(definitions));
951 fixture.Write("BO_ 1 Frame: 2 ECU\n\n"
952 "SG_ Branch m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
953 "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n");
954 bool rejected = false;
955 try {
956 DbcParser().ParseFile(fixture.path.string());
957 } catch (const std::runtime_error &error) {
958 rejected = std::string(error.what()).find(fixture.path.string() + ":3: ") == 0U;
959 }
960 CHECK(rejected, "Deferred validation reports branch source line");
961 fixture.Write("BO_ 1 Frame: 2 ECU\n"
962 "SG_ A : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
963 "SG_ B : 15|8@0+ (1,0) [0|255] \"\" ECU\n");
964 rejected = false;
965 try {
966 DbcParser().ParseFile(fixture.path.string());
967 } catch (const std::runtime_error &) {
968 rejected = true;
969 }
970 CHECK(rejected, "Ordinary overlapping signals rejected");
971}
972
973void DbcParserTest::invalidSignals() {
974 Fixture fixture;
975 const std::string ordinary = SignalLine("0", "8", "1+", "1", "0", "0", "255");
976 for (const std::string value: {"-1", "+1", "0junk", "0x10", "1.5",
977 "4294967296", "999999999999999999999"}) {
978 RejectSignal(fixture, SignalLine(value, "8", "1+", "1", "0", "0", "255"));
979 RejectSignal(fixture, SignalLine("0", value, "1+", "1", "0", "0", "255"));
980 }
981 for (const std::string length: {"0", "65"})
982 RejectSignal(fixture, SignalLine("0", length, "1+", "1", "0", "0", "255"));
983 for (const std::string order: {"2+", "9-", "1", "1x", "1++", "01+"})
984 RejectSignal(fixture, SignalLine("0", "8", order, "1", "0", "0", "255"));
985 for (const std::string value: {"1junk", "nan", "NaN", "inf", "-inf", "1e999", "1e-999", "0x1p2", "", "1e", "1.2.3"}) {
986 RejectSignal(fixture, SignalLine("0", "8", "1+", value, "0", "0", "255"));
987 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", value, "0", "255"));
988 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", value, "255"));
989 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "0", value));
990 }
991 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "2", "1"));
992 for (const std::string name: {"", "Two Names", "Value m1M"}) {
993 std::string line = ordinary;
994 line.replace(4U, 5U, name);
995 RejectSignal(fixture, line);
996 }
997 for (const std::string broken: {
998 "SG_", "SG_ Value", "SG_ Value : 0|8@1+ (1,0) [0|255] ECU",
999 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\"",
1000 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU,",
1001 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU extra"})
1002 RejectSignal(fixture, broken);
1003 RejectSignal(fixture, SignalLine("64", "1", "1+", "1", "0", "0", "1"));
1004 RejectSignal(fixture, SignalLine("63", "2", "1+", "1", "0", "0", "3"));
1005 RejectSignal(fixture, SignalLine("56", "2", "0+", "1", "0", "0", "3"));
1006 RejectSignal(fixture, SignalLine("4294967295", "64", "1+", "1", "0", "0", "1"));
1007 RejectSignal(fixture, ordinary, 0U);
1008 for (const char delimiter: {'|', '@', '(', ')', '[', ']', '"'}) {
1009 std::string line = ordinary;
1010 line.erase(line.find(delimiter), 1U);
1011 RejectSignal(fixture, line);
1012 }
1013}
1014
1015void DbcParserTest::validSignals() {
1016 Fixture fixture;
1017 const DbcParser signalParser;
1018 fixture.Write("BO_ 1 Frame: 8 ECU\n" +
1019 SignalLine("0", "8", "1+", "0", "0", "0", "0") + "\n" +
1020 SignalLine("8", "8", "1-", "-1", "0", "-255", "0", "Negative") + "\n");
1021 const DbcDatabase factors = signalParser.ParseFile(fixture.path.string());
1022 CHECK(factors.frames[0].signals.size() == 2U &&
1023 factors.frames[0].signals[0].factor == 0.0 &&
1024 factors.frames[0].signals[1].factor == -1.0,
1025 "Zero and negative factors and equal range boundaries");
1026
1027 struct SignalCase {
1028 const char *start;
1029 const char *length;
1030 const char *order;
1031 unsigned payload;
1032 };
1033 const SignalCase validSignals[] = {
1034 {"0", "1", "1+", 1U},
1035 {"7", "1", "0-", 1U},
1036 {"0", "64", "1+", 8U},
1037 {"7", "64", "0-", 8U},
1038 {"63", "1", "1+", 8U},
1039 {"56", "1", "0+", 8U},
1040 {"3", "12", "0+", 2U},
1041 {"2039", "1", "1+", 255U}};
1042 for (const SignalCase &test: validSignals) {
1043 fixture.Write("BO_ 1 Frame: " + std::to_string(test.payload) + " ECU\n" +
1044 SignalLine(test.start, test.length, test.order, ".5e+1", "-2.5E-1", "-1.", "+2e2") + "\n");
1045 try {
1046 const DbcDatabase parsed = signalParser.ParseFile(fixture.path.string());
1047 const SignalInfo &signal = parsed.frames.at(0).signals.at(0);
1048 QCOMPARE(signal.startBit, static_cast<std::uint32_t>(std::stoul(test.start)));
1049 QCOMPARE(signal.length, static_cast<std::uint32_t>(std::stoul(test.length)));
1050 QCOMPARE(signal.isLittleEndian, test.order[0] == '1');
1051 QCOMPARE(signal.isSigned, test.order[1] == '-');
1052 QCOMPARE(signal.factor, 5.0);
1053 QCOMPARE(signal.offset, -0.25);
1054 QCOMPARE(signal.minimum, -1.0);
1055 QCOMPARE(signal.maximum, 200.0);
1056 } catch (const std::exception &error) { CHECK(false, error.what()); }
1057 }
1058 fixture.Write("BO_ 1 Frame: 8 ECU\n"
1059 "SG_\tValue\t:\t0 | 8 @1+ ( 1 , 0 ) [ 0 | 255 ] \"rpm\" ECU1,\tECU2\n");
1060 try {
1061 const DbcDatabase parsed = signalParser.ParseFile(fixture.path.string());
1062 const SignalInfo &signal = parsed.frames.at(0).signals.at(0);
1063 CHECK(signal.name == "Value" && signal.unit == "rpm" &&
1064 signal.receivers == std::vector<std::string>({"ECU1", "ECU2"}),
1065 "Signal tabs, spaces, and receiver list");
1066 } catch (const std::exception &error) { CHECK(false, error.what()); }
1067}
1068
1069void DbcParserTest::CheckPhysicalNumbers() {
1070 Fixture fixture;
1071 struct PhysicalCase {
1072 const char *factor;
1073 const char *offset;
1074 const char *minimum;
1075 const char *maximum;
1076 double expectedFactor;
1077 double expectedOffset;
1078 double expectedMinimum;
1079 double expectedMaximum;
1080 };
1081 const PhysicalCase valid[] = {
1082 {"+1.25", "-0.5", "-2.75", "255.5", 1.25, -0.5, -2.75, 255.5},
1083 {"+.5e+1", "-2.5E-1", "-1.", "+2e2", 5.0, -0.25, -1.0, 200.0},
1084 {"0e-999", "+0.", "-.0", ".0e+999", 0.0, 0.0, 0.0, 0.0},
1085 {"1.7976931348623157e308", "2.2250738585072014e-308", "-1e308", "+1e308",
1086 std::numeric_limits<double>::max(), std::numeric_limits<double>::min(), -1e308, 1e308}};
1087 for (const PhysicalCase &test: valid) {
1088 fixture.Write("BO_ 1 Frame: 1 ECU\n" +
1089 SignalLine("0", "8", "1+", test.factor, test.offset, test.minimum, test.maximum) + "\n");
1090 const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string());
1091 QVERIFY(!result.HasErrors());
1092 QVERIFY(result.diagnostics.empty());
1093 QCOMPARE(result.database.frames.size(), std::size_t(1));
1094 const SignalInfo &signal = result.database.frames[0].signals[0];
1095 QCOMPARE(signal.factor, test.expectedFactor);
1096 QCOMPARE(signal.offset, test.expectedOffset);
1097 QCOMPARE(signal.minimum, test.expectedMinimum);
1098 QCOMPARE(signal.maximum, test.expectedMaximum);
1099 }
1100
1101 const char *invalid[] = {"1,5", "1.234,5", "1,234.5", "1junk", "nan", "inf", "-inf",
1102 "1e999", "1e-999", "1e-320", "-1e-320", "0x1p2", "1e", "1.2.3"};
1103 for (const char *value: invalid) {
1104 RejectSignal(fixture, SignalLine("0", "8", "1+", value, "0", "0", "255"));
1105 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", value, "0", "255"));
1106 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", value, "255"));
1107 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "0", value));
1108 }
1109 RejectSignal(fixture, SignalLine("0", "8", "1+", "1.25", "0", "2.5", "1.5"));
1110}
1111
1112void DbcParserTest::physicalNumbersCppLocale() {
1113 const ApplicationLocaleGuard guard;
1114 std::locale::global(std::locale(std::locale::classic(), new CommaDecimalPunctuation));
1115 QCOMPARE(std::use_facet<std::numpunct<char>>(std::locale()).decimal_point(), ',');
1116 CheckPhysicalNumbers();
1117 QCOMPARE(std::use_facet<std::numpunct<char>>(std::locale()).decimal_point(), ',');
1118}
1119
1120void DbcParserTest::physicalNumbersCLocale() {
1121 const ApplicationLocaleGuard guard;
1122 bool found = false;
1123 const char *names[] = {"de_DE.UTF-8", "de_DE.utf8", "fr_FR.UTF-8", "fr_FR.utf8",
1124 "German_Germany.1252", "French_France.1252"};
1125 for (const char *name: names) {
1126 if (std::setlocale(LC_NUMERIC, name) && std::string(std::localeconv()->decimal_point) == ",") {
1127 found = true;
1128 break;
1129 }
1130 }
1131 if (!found)
1132 QSKIP("No named comma-decimal C locale is installed; generate de_DE.UTF-8 and set LOCPATH.");
1133 QCOMPARE(std::string(std::localeconv()->decimal_point), std::string(","));
1134 CheckPhysicalNumbers();
1135 QCOMPARE(std::string(std::localeconv()->decimal_point), std::string(","));
1136}
1137
1138void DbcParserTest::dispatch() {
1139 CountingHandler::instances = 0;
1140 CountingHandler::calls = 0;
1141 DbcDatabase dispatchDatabase;
1142 DbcParseContext context{dispatchDatabase, nullptr};
1143 DbcStatementDispatcher dispatcher;
1144 dispatcher.RegisterType<CountingHandler>("TEST_");
1145 CHECK(dispatcher.Dispatch("TEST_ first", context) &&
1146 dispatcher.Dispatch("TEST_ second", context) &&
1147 CountingHandler::instances == 1 && CountingHandler::calls == 2 &&
1148 dispatchDatabase.frames.size() == 2U,
1149 "Factory dispatch and handler reuse");
1150 CHECK(!dispatcher.Dispatch("TEST_OTHER data", context) &&
1151 !dispatcher.Dispatch("BO_TX_BU_ 1 : ECU;", context) &&
1152 !dispatcher.Dispatch("VAL_TABLE_ States 0 \"Stopped\";", context) &&
1153 CountingHandler::calls == 2,
1154 "Exact and unknown keyword dispatch");
1155}
1156
1157void DbcParserTest::independentSessions() {
1158 Fixture fixture;
1159 const DbcParser parser;
1160 fixture.Write("BO_ 1 First: 8 ECU\n");
1161 CHECK(parser.ParseFile(fixture.path.string()).frames.size() == 1U, "First parse");
1162 fixture.Write("SG_ Speed : invalid\n");
1163 try {
1164 parser.ParseFile(fixture.path.string());
1165 CHECK(false, "Independent parse context");
1166 } catch (const std::runtime_error &error) {
1167 CHECK(std::string(error.what()).find("Signal found before any frame") !=
1168 std::string::npos,
1169 "Independent parse context");
1170 }
1171 fixture.Write("BO_ 2 Second: 8 ECU\n");
1172 const DbcDatabase recovered = parser.ParseFile(fixture.path.string());
1173 CHECK(recovered.frames.size() == 1U && recovered.frames[0].canId == 2U,
1174 "Fresh parse after error");
1175}
1176
1177
1178void DbcParserTest::valueDescriptions() {
1179 Fixture fixture;
1180 fixture.Write(
1181 "VAL_\t256\tState\t0\t\"Off\"\t1 \"On\" 2 \"Error with spaces; and \\\"quotes\\\"\";\n"
1182 "BO_ 256 Status: 8 ECU\n"
1183 "SG_ State : 0|8@1+ (2,10) [0|520] \"\" ECU\n"
1184 "VAL_ 256 State 3 \"\";\n"
1185 "VAL_ 2147483904 State -9223372036854775808 \"Minimum\" -1 \"Negative\" 9223372036854775807 \"Maximum\";\n"
1186 "BO_ 2147483904 Extended: 8 ECU\n"
1187 "SG_ State : 0|64@1- (1,0) [-1e20|1e20] \"\" ECU\n"
1188 "BO_ 2048 Unsigned: 8 ECU\n"
1189 "SG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n"
1190 "VAL_ 2147485696 State 9223372036854775808 \"High\" 18446744073709551615 \"Full\";\n");
1191 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
1192 const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
1193 const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed);
1194 CHECK(tree->GetChild(0)->GetSignal() == nullptr, "Frame node");
1195 for (std::size_t index = 0; index < parsed.frames.size(); ++index) {
1196 CHECK(tree->GetChild(index)->GetChild(0)->GetSignal()->valueDescriptions ==
1197 parsed.frames[index].signals[0].valueDescriptions &&
1198 runtime.GetFrames()[index].signals[0].valueDescriptions ==
1199 parsed.frames[index].signals[0].valueDescriptions,
1200 "All mappings survive both builders");
1201 }
1202 CHECK(tree->GetChild(0)->GetChild(0)->GetSignal()->valueDescriptions ==
1203 runtime.GetFrames()[0].signals[0].valueDescriptions,
1204 "Both builders preserve descriptions");
1205 RawCanFrame frame;
1206 frame.canId = 256;
1207 frame.data.resize(8);
1208 const std::string expected[] = {"Off", "On", "Error with spaces; and \"quotes\"", ""};
1209 for (unsigned value = 0; value < 5; ++value) {
1210 frame.data[0] = value;
1211 const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0];
1212 CHECK(decoded.valid && decoded.physicalValue == value * 2 + 10, "Numeric scaling unchanged");
1213 const std::string *description = decoded.GetValueDescription();
1214 CHECK(value < 4 ? description && *description == expected[value] : !description,
1215 "Raw lookup and unknown value");
1216 }
1217 frame.isExtended = true;
1218 struct Boundary {
1219 std::uint64_t bits;
1220 const char *description;
1221 };
1222 for (const Boundary &test: {Boundary{0x8000000000000000ULL, "Minimum"},
1223 Boundary{0xFFFFFFFFFFFFFFFFULL, "Negative"},
1224 Boundary{0x7FFFFFFFFFFFFFFFULL, "Maximum"}}) {
1225 for (unsigned byte = 0; byte < 8; ++byte)
1226 frame.data[byte] = static_cast<std::uint8_t>(test.bits >> (byte * 8));
1227 const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0];
1228 CHECK(decoded.GetValueDescription() && *decoded.GetValueDescription() == test.description,
1229 "Signed boundaries and distinct extended frame");
1230 }
1231 frame.canId = 2048;
1232 for (const Boundary &test: {Boundary{0x8000000000000000ULL, "High"},
1233 Boundary{0xFFFFFFFFFFFFFFFFULL, "Full"}}) {
1234 for (unsigned byte = 0; byte < 8; ++byte)
1235 frame.data[byte] = static_cast<std::uint8_t>(test.bits >> (byte * 8));
1236 const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0];
1237 CHECK(decoded.GetValueDescription() && *decoded.GetValueDescription() == test.description,
1238 "Unsigned boundaries");
1239 }
1240 CHECK(FindValueDescription(runtime.GetFrames()[0].signals[0].valueDescriptions, std::int64_t(1)) != nullptr,
1241 "Nonnegative signed lookup matches unsigned key");
1242 frame.data.clear();
1243 CHECK(!DbcDecoder().Decode(runtime, frame).signals[0].GetValueDescription(), "Invalid decode has no description");
1244}
1245
1246void DbcParserTest::invalidValueDescriptions() {
1247 Fixture fixture;
1248 const std::string definitions = "BO_ 1 Frame: 8 ECU\nSG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n";
1249 for (const std::string statement: {
1250 "VAL_", "VAL_ 1 State;", "VAL_ 1 State ;", "VAL_ 1 State 0 \"Off\"",
1251 "VAL_ 1 State 0 \"Off\"; junk", "VAL_ 1 State 0 Off;",
1252 "VAL_ 1 State 0 \"Unclosed;", "VAL_ 1 State 0 \"Off\"1 \"On\";",
1253 "VAL_ 1 State 0 \"Off\" 00 \"Duplicate\";",
1254 "VAL_ 1 State 0 \"Off\" -0 \"Duplicate\";",
1255 "VAL_ 1 State 0 \"Off\";\nVAL_ 1 State 0 \"Duplicate\";",
1256 "VAL_ 2 State 0 \"Missing frame\";", "VAL_ 1 Missing 0 \"Missing signal\";",
1257 "VAL_ -1 State 0 \"Bad ID\";", "VAL_ 536870912 State 0 \"Bad ID\";",
1258 "VAL_ 4294967296 State 0 \"Bad ID\";", "VAL_ Environment 0 \"Unsupported\";",
1259 "VAL_ 1 State 0 \"Bad\\q escape\";"}) {
1260 fixture.Write(definitions + statement + '\n');
1261 const int line = statement.find('\n') == std::string::npos ? 3 : 4;
1262 try {
1263 DbcParser().ParseFile(fixture.path.string());
1264 CHECK(false, statement);
1265 } catch (const std::runtime_error &error) {
1266 CHECK(std::string(error.what()).find(fixture.path.string() + ":" + std::to_string(line) + ": ") == 0,
1267 statement + " " + error.what());
1268 }
1269 }
1270 for (const std::string suffix: {
1271 "BO_ 1 Other: 8 ECU\n", // Duplicate frame definition.
1272 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n",// Duplicate signal definition.
1273 ""}) {
1274 fixture.Write("VAL_ 1 " + std::string(suffix.empty() ? "Missing" : "State") +
1275 " 0 \"Description\";\n" + definitions + suffix);
1276 try {
1277 DbcParser().ParseFile(fixture.path.string());
1278 CHECK(false, "Missing reference or duplicate definition");
1279 } catch (const std::runtime_error &error) {
1280 const std::string line = suffix.empty() ? "1" : "4";
1281 CHECK(std::string(error.what()).find(fixture.path.string() + ":" + line + ": ") == 0,
1282 "Error retains the reference or duplicate definition source location");
1283 }
1284 }
1285 for (const std::string number: {"+1", "1x", "0x10", "1.5", "1e2", "--1",
1286 "-9223372036854775809", "18446744073709551616"}) {
1287 fixture.Write("VAL_ 1 State " + number + " \"Bad\";\n" + definitions);
1288 try {
1289 DbcParser().ParseFile(fixture.path.string());
1290 CHECK(false, number);
1291 } catch (const std::runtime_error &error) {
1292 CHECK(std::string(error.what()).find(fixture.path.string() + ":1: ") == 0, number);
1293 }
1294 }
1295}
1296
1297int RunParserTests(int argc, char **argv) {
1298 DbcParserTest tests;
1299 return QTest::qExec(&tests, argc, argv);
1300}
1301
1302#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:86
DecodedFrameValue Decode(const DecodeDatabase &database, const RawCanFrame &frame) const
Decode one raw CAN frame.
Minimal DBC parser.
Definition dbc_parser.h:41
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:73
std::shared_ptr< const DecodeFrame > definition
Definition dbc_decoder.h:74
FrameDecodeStatus status
Definition dbc_decoder.h:78
One decoded signal value.
Definition dbc_decoder.h:54
const std::string * GetValueDescription() const
Definition dbc_decoder.h:63
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
Raw CAN frame used for runtime or trace decoding.
Definition dbc_decoder.h:24
std::vector< std::uint8_t > data
Definition dbc_decoder.h:27
std::uint32_t canId
Definition dbc_decoder.h:25
bool isExtended
Definition dbc_decoder.h:26
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