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 <limits>
7
8#include <QtTest>
9#include <chrono>
10#include <filesystem>
11#include <fstream>
12
13#define CHECK(condition, name) QVERIFY2((condition), std::string(name).c_str())
14#include <stdexcept>
15
16struct Fixture {
17 std::filesystem::path path = std::filesystem::temp_directory_path() /
18 ("dbc_parser_test_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".dbc");
19 ~Fixture() {
20 std::error_code error;
21 std::filesystem::remove(path, error);
22 }
23 void Write(const std::string &text) const {
24 std::ofstream output(path);
25 output << text;
26 if (!output)
27 throw std::runtime_error("Cannot write test fixture.");
28 }
29};
30
31class CountingHandler : public DbcStatementHandler {
32public:
33 inline static int instances = 0;
34 inline static int calls = 0;
35 CountingHandler() { ++instances; }
36 void Handle(const std::string &, DbcParseContext &context) const override {
37 ++calls;
38 context.database.frames.emplace_back();
39 }
40};
41
42std::string SignalLine(const std::string &start, const std::string &length,
43 const std::string &order, const std::string &factor,
44 const std::string &offset, const std::string &minimum,
45 const std::string &maximum, const std::string &name = "Value") {
46 return "SG_ " + name + " : " + start + "|" + length + "@" + order +
47 " (" + factor + "," + offset + ") [" + minimum + "|" + maximum + "] \"\" ECU";
48}
49
50class DbcParserTest : public QObject {
51 Q_OBJECT
52private:
53 void RejectSignal(const Fixture &fixture, const std::string &line, unsigned payload = 8U) {
54 fixture.Write("BO_ 1 Frame: " + std::to_string(payload) + " ECU\n\n" + line + "\n");
55 try {
56 DbcParser().ParseFile(fixture.path.string());
57 CHECK(false, "Reject " + line);
58 } catch (const std::runtime_error &error) {
59 const std::string prefix = fixture.path.string() + ":3: ";
60 CHECK(std::string(error.what()).find(prefix) == 0U &&
61 std::string(error.what()).size() > prefix.size(),
62 "Reject " + line);
63 }
64 }
65private Q_SLOTS:
66 void multiplexing();
67 void multiplexingBoundaries();
68 void validFrames();
69 void invalidFrames();
70 void duplicateFrames();
71 void duplicateSignals();
72 void distinctDefinitions();
73 void integration();
74 void invalidSignals();
75 void validSignals();
76 void dispatch();
77 void independentSessions();
78 void valueDescriptions();
79 void invalidValueDescriptions();
80};
81
82void DbcParserTest::validFrames() {
83 Fixture fixture;
84 struct ValidCase {
85 const char *line;
86 std::uint32_t id;
87 bool extended;
88 unsigned length;
89 };
90 const ValidCase valid[] = {
91 {"BO_ 0 Frame: 0 ECU", 0U, false, 0U},
92 {"BO_ 2047 Frame: 8 ECU", 2047U, false, 8U},
93 {"BO_ 2048 Frame: 64 ECU", 2048U, true, 64U},
94 {"BO_ 536870911 Frame: 255 ECU", 0x1FFFFFFFU, true, 255U},
95 {"BO_ 2147483649 Frame: 8 ECU", 1U, true, 8U},
96 {"BO_ 2684354559 Frame: 8 ECU", 0x1FFFFFFFU, true, 8U},
97 {" BO_\t256\tFrame :\t8\tECU \r", 256U, false, 8U}};
98 for (const ValidCase &test: valid) {
99 fixture.Write(std::string(test.line) + '\n');
100 try {
101 const DbcDatabase database = DbcParser().ParseFile(fixture.path.string());
102 QCOMPARE(database.frames.size(), std::size_t(1U));
103 const FrameInfo &frame = database.frames[0];
104 QCOMPARE(frame.name, std::string("Frame"));
105 QCOMPARE(frame.canId, test.id);
106 QCOMPARE(frame.isExtended, test.extended);
107 QCOMPARE(static_cast<unsigned>(frame.dlc), test.length);
108 QCOMPARE(frame.transmitter, std::string("ECU"));
109 } catch (const std::exception &error) { CHECK(false, error.what()); }
110 }
111}
112
113void DbcParserTest::invalidFrames() {
114 Fixture fixture;
115 const char *invalid[] = {
116 "BO_ invalid", "BO_", "BO_ 1", "BO_ 1 : 8 ECU", "BO_ 1 Frame 8 ECU",
117 "BO_ 1 Frame:", "BO_ 1 Frame: 8", "BO_ 1 Frame: 8 ECU extra",
118 "BO_ 1 Two Names: 8 ECU", "BO_ -1 Frame: 8 ECU", "BO_ +1 Frame: 8 ECU",
119 "BO_ 1x Frame: 8 ECU", "BO_ 0x123 Frame: 8 ECU", "BO_ 1.5 Frame: 8 ECU",
120 "BO_ 4294967296 Frame: 8 ECU", "BO_ 999999999999999999999 Frame: 8 ECU",
121 "BO_ 536870912 Frame: 8 ECU", "BO_ 1073741824 Frame: 8 ECU",
122 "BO_ 2684354560 Frame: 8 ECU", "BO_ 4294967295 Frame: 8 ECU",
123 "BO_ 1 Frame: -1 ECU", "BO_ 1 Frame: +8 ECU", "BO_ 1 Frame: 8x ECU",
124 "BO_ 1 Frame: 8.5 ECU", "BO_ 1 Frame: 256 ECU",
125 "BO_ 1 Frame: 4294967296 ECU", "BO_\tinvalid"};
126 for (const char *line: invalid) {
127 fixture.Write(std::string("VERSION \"test\"\n\n") + line + '\n');
128 try {
129 DbcParser().ParseFile(fixture.path.string());
130 CHECK(false, line);
131 } catch (const std::runtime_error &error) {
132 const std::string prefix = fixture.path.string() + ":3: ";
133 CHECK(std::string(error.what()).find(prefix) == 0U &&
134 std::string(error.what()).size() > prefix.size(),
135 line);
136 }
137 }
138}
139
140void DbcParserTest::duplicateFrames() {
141 Fixture fixture;
142 struct DuplicateCase {
143 const char *firstId;
144 const char *duplicateId;
145 const char *message;
146 };
147 const DuplicateCase cases[] = {
148 {"1", "1", "Duplicate standard frame CAN ID: 1"},
149 {"2147483649", "2147483649", "Duplicate extended frame CAN ID: 1"},
150 {"2048", "2147485696", "Duplicate extended frame CAN ID: 2048"},
151 {"2147485696", "2048", "Duplicate extended frame CAN ID: 2048"},
152 {"536870911", "2684354559", "Duplicate extended frame CAN ID: 536870911"}
153 };
154 const DbcParser parser;
155 for (const DuplicateCase &test : cases) {
156 fixture.Write("BO_ " + std::string(test.firstId) + " First: 8 ECU\n"
157 "BO_ 2 Other: 8 ECU\n\n"
158 " BO_\t" + test.duplicateId + "\tSecond: 8 ECU\n");
159 try {
160 parser.ParseFile(fixture.path.string());
161 CHECK(false, "Duplicate frame must be rejected");
162 } catch (const std::runtime_error &error) {
163 QCOMPARE(std::string(error.what()), fixture.path.string() + ":4: " + test.message);
164 }
165 }
166 fixture.Write("BO_ 1 First: 8 ECU\n");
167 QCOMPARE(parser.ParseFile(fixture.path.string()).frames.size(), std::size_t(1U));
168}
169
170void DbcParserTest::duplicateSignals() {
171 Fixture fixture;
172 const std::string first = "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n";
173 const std::string duplicates[] = {
174 first,
175 " SG_\tState : 16|8@1+ (2,1) [0|511] \"V\" ECU\n",
176 "SG_ State m1 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
177 };
178 for (const std::string &duplicate : duplicates) {
179 fixture.Write("BO_ 1 Frame: 8 ECU\n" + first +
180 "SG_ Other : 8|8@1+ (1,0) [0|255] \"\" ECU\n\n" + duplicate);
181 try {
182 DbcParser().ParseFile(fixture.path.string());
183 CHECK(false, "Duplicate signal must be rejected");
184 } catch (const std::runtime_error &error) {
185 QCOMPARE(std::string(error.what()), fixture.path.string() +
186 ":5: Duplicate signal name 'State' in frame 'Frame'.");
187 }
188 }
189}
190
191void DbcParserTest::distinctDefinitions() {
192 Fixture fixture;
193 fixture.Write("BO_ 1 Shared: 1 ECU\n"
194 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
195 "BO_ 2147483649 Shared: 1 ECU\n"
196 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
197 "BO_ 2 Shared: 2 ECU\n"
198 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
199 "SG_ state : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
200 "CM_ BO_ 1 \"Standard\";\n"
201 "CM_ BO_ 2147483649 \"Extended\";\n"
202 "CM_ SG_ 1 State \"Standard state\";\n"
203 "CM_ SG_ 2147483649 State \"Extended state\";\n");
204 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
205 QCOMPARE(parsed.frames.size(), std::size_t(3U));
206 QCOMPARE(parsed.frames[0].canId, parsed.frames[1].canId);
207 CHECK(!parsed.frames[0].isExtended && parsed.frames[1].isExtended,
208 "Standard and extended frames with the same numeric ID remain distinct");
209 QCOMPARE(parsed.frames[0].comment, std::string("Standard"));
210 QCOMPARE(parsed.frames[1].comment, std::string("Extended"));
211 QCOMPARE(parsed.frames[0].signals[0].comment, std::string("Standard state"));
212 QCOMPARE(parsed.frames[1].signals[0].comment, std::string("Extended state"));
213 QCOMPARE(parsed.frames[2].signals.size(), std::size_t(2U));
214 const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
215 RawCanFrame raw;
216 raw.canId = 1U;
217 raw.data = {7U};
218 const DecodedFrameValue standard = DbcDecoder().Decode(runtime, raw);
219 QVERIFY(standard.valid);
220 QCOMPARE(standard.definition->comment, std::string("Standard"));
221 raw.isExtended = true;
222 const DecodedFrameValue extended = DbcDecoder().Decode(runtime, raw);
223 QVERIFY(extended.valid);
224 QCOMPARE(extended.definition->comment, std::string("Extended"));
225}
226
227void DbcParserTest::integration() {
228 Fixture fixture;
229 fixture.Write(
230 "BO_TX_BU_ 256 : ECU;\nBO_ 256 Frame: 8 ECU\n"
231 " SG_ Speed : 0|16@1+ (0.5,1) [0|100] \"rpm\" ECU\n"
232 "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Speed \"Signal comment\";\n"
233 "BO_ 257 Second: 8 ECU\n");
234 const DbcDatabase database = DbcParser().ParseFile(fixture.path.string());
235 CHECK(database.frames.size() == 2U && database.frames[0].signals.size() == 1U &&
236 database.frames[0].comment == "Frame comment" &&
237 database.frames[0].signals[0].comment == "Signal comment",
238 "Integration");
239 fixture.Write("\nSG_ Speed : invalid\n");
240 try {
241 DbcParser().ParseFile(fixture.path.string());
242 CHECK(false, "Signal error location");
243 } catch (const std::runtime_error &error) {
244 CHECK(std::string(error.what()).find(fixture.path.string() + ":2: ") == 0U,
245 "Signal error location");
246 }
247}
248
249void DbcParserTest::multiplexing() {
250 Fixture fixture;
251 const std::string selector = "SG_ Mode M : 0|8@1+ (10,3) [0|255] \"\" ECU\n";
252 const std::string first = "SG_ First m1 : 8|8@1- (2,0) [-128|127] \"V\" ECU\n";
253 const std::string second = "SG_ Second m2 : 15|8@0+ (1,0) [0|255] \"\" ECU\n";
254 const std::string common = "SG_ Common : 16|8@1+ (1,0) [0|255] \"\" ECU\n";
255 fixture.Write("BO_ 1 Frame: 3 ECU\n" + first + second + common + selector +
256 "VAL_ 1 First -1 \"Negative\";\n");
257 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
258 const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
259 CHECK(parsed.frames[0].signals[3].multiplexing.role == MultiplexRole::Selector,
260 "Selector declared after branches");
261 const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed);
262 CHECK(tree->GetChild(0U)->GetChild(0U)->GetSignal()->multiplexing.value == 1U,
263 "Tree preserves multiplexing");
264 RawCanFrame raw;
265 raw.canId = 1U;
266 raw.data = {1U, 255U, 7U};
267 DecodedFrameValue result = DbcDecoder().Decode(runtime, raw);
268 CHECK(result.signals[0].valid && std::get<std::int64_t>(result.signals[0].rawValue) == -1 &&
269 result.signals[0].physicalValue == -2 && *result.signals[0].GetValueDescription() == "Negative",
270 "Active signed branch and VAL_ use exact raw selector");
271 CHECK(!result.signals[1].valid && result.signals[1].status == SignalDecodeStatus::Inactive &&
272 result.signals[1].GetValueDescription() == nullptr && result.signals[2].valid,
273 "Inactive branch and unconditional signal");
274 raw.data[0] = 2U;
275 result = DbcDecoder().Decode(runtime, raw);
276 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive && result.signals[1].valid &&
277 std::get<std::uint64_t>(result.signals[1].rawValue) == 255U,
278 "Motorola branch becomes active");
279 raw.data = {3U};
280 result = DbcDecoder().Decode(runtime, raw);
281 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive &&
282 result.signals[1].status == SignalDecodeStatus::Inactive &&
283 result.signals[2].status == SignalDecodeStatus::InsufficientPayload,
284 "Unmapped selector leaves branches inactive even with missing bytes");
285 raw.data = {1U};
286 result = DbcDecoder().Decode(runtime, raw);
287 CHECK(result.signals[0].status == SignalDecodeStatus::InsufficientPayload,
288 "Active branch requires payload");
289 raw.data.clear();
290 result = DbcDecoder().Decode(runtime, raw);
291 CHECK(result.signals[0].status == SignalDecodeStatus::SelectorUnavailable &&
292 result.signals[3].status == SignalDecodeStatus::InsufficientPayload,
293 "Unavailable selector does not imply inactive branch");
294
295 const std::vector<std::string> invalid = {
296 first,
297 selector + "SG_ Other M : 16|8@1+ (1,0) [0|255] \"\" ECU\n",
298 "SG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n",
299 selector + "SG_ TooLarge m256 : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
300 selector + first + "SG_ Same m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n",
301 selector + "SG_ Collision m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n",
302 selector + first + "SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n",
303 "SG_MUL_VAL_ 1 First Mode 1-2;\n"
304 };
305 for (const std::string &signals : invalid) {
306 fixture.Write("BO_ 1 Frame: 3 ECU\n" + signals);
307 bool rejected = false;
308 try {
309 DbcParser().ParseFile(fixture.path.string());
310 } catch (const std::runtime_error &error) {
311 rejected = std::string(error.what()).find(fixture.path.string() + ":") == 0U;
312 }
313 CHECK(rejected, "Invalid multiplexing rejected with location");
314 }
315 for (const std::string marker : {"m", "m-1", "m+1", "m1M", "m18446744073709551616", "M m1"})
316 RejectSignal(fixture, "SG_ Bad " + marker + " : 8|8@1+ (1,0) [0|255] \"\" ECU");
317}
318
319void DbcParserTest::multiplexingBoundaries() {
320 Fixture fixture;
321 fixture.Write("BO_ 1 Frame: 9 ECU\n"
322 "SG_ Branch m18446744073709551615 : 64|8@1+ (1,0) [0|255] \"\" ECU\n"
323 "SG_ Mode M : 7|64@0+ (0,5) [0|1] \"\" ECU\n");
324 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
325 DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
326 CHECK(runtime.frames[0].signals[0].multiplexing.value == std::numeric_limits<std::uint64_t>::max(),
327 "Exact uint64 branch metadata");
328 RawCanFrame raw;
329 raw.canId = 1U;
330 raw.data = std::vector<std::uint8_t>(9U, 255U);
331 DecodedFrameValue result = DbcDecoder().Decode(runtime, raw);
332 CHECK(result.signals[0].valid && result.signals[1].valid &&
333 std::get<std::uint64_t>(result.signals[1].rawValue) == std::numeric_limits<std::uint64_t>::max(),
334 "Motorola uint64 selector compares raw value before zero scaling");
335 raw.data[7] = 254U;
336 result = DbcDecoder().Decode(runtime, raw);
337 CHECK(result.signals[0].status == SignalDecodeStatus::Inactive,
338 "Adjacent large integers remain distinct");
339 runtime.frames[0].signals[1].length = 0U;
340 result = DbcDecoder().Decode(runtime, raw);
341 CHECK(result.signals[0].status == SignalDecodeStatus::InvalidDefinition &&
342 result.signals[1].status == SignalDecodeStatus::InvalidDefinition,
343 "Malformed runtime selector distinguished from payload errors");
344 runtime.frames[0].signals.pop_back();
345 result = DbcDecoder().Decode(runtime, raw);
346 CHECK(result.signals[0].status == SignalDecodeStatus::InvalidDefinition,
347 "Missing runtime selector");
348 fixture.Write("BO_ 1 Frame: 2 ECU\n\n"
349 "SG_ Branch m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
350 "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n");
351 bool rejected = false;
352 try {
353 DbcParser().ParseFile(fixture.path.string());
354 } catch (const std::runtime_error &error) {
355 rejected = std::string(error.what()).find(fixture.path.string() + ":3: ") == 0U;
356 }
357 CHECK(rejected, "Deferred validation reports branch source line");
358 fixture.Write("BO_ 1 Frame: 2 ECU\n"
359 "SG_ A : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
360 "SG_ B : 15|8@0+ (1,0) [0|255] \"\" ECU\n");
361 rejected = false;
362 try {
363 DbcParser().ParseFile(fixture.path.string());
364 } catch (const std::runtime_error &) {
365 rejected = true;
366 }
367 CHECK(rejected, "Ordinary overlapping signals rejected");
368}
369
370void DbcParserTest::invalidSignals() {
371 Fixture fixture;
372 const std::string ordinary = SignalLine("0", "8", "1+", "1", "0", "0", "255");
373 for (const std::string value: {"-1", "+1", "0junk", "0x10", "1.5",
374 "4294967296", "999999999999999999999"}) {
375 RejectSignal(fixture, SignalLine(value, "8", "1+", "1", "0", "0", "255"));
376 RejectSignal(fixture, SignalLine("0", value, "1+", "1", "0", "0", "255"));
377 }
378 for (const std::string length: {"0", "65"})
379 RejectSignal(fixture, SignalLine("0", length, "1+", "1", "0", "0", "255"));
380 for (const std::string order: {"2+", "9-", "1", "1x", "1++", "01+"})
381 RejectSignal(fixture, SignalLine("0", "8", order, "1", "0", "0", "255"));
382 for (const std::string value: {"1junk", "nan", "NaN", "inf", "-inf", "1e999", "1e-999", "0x1p2", "", "1e", "1.2.3"}) {
383 RejectSignal(fixture, SignalLine("0", "8", "1+", value, "0", "0", "255"));
384 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", value, "0", "255"));
385 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", value, "255"));
386 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "0", value));
387 }
388 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "2", "1"));
389 for (const std::string name: {"", "Two Names", "Value m1M"}) {
390 std::string line = ordinary;
391 line.replace(4U, 5U, name);
392 RejectSignal(fixture, line);
393 }
394 for (const std::string broken: {
395 "SG_", "SG_ Value", "SG_ Value : 0|8@1+ (1,0) [0|255] ECU",
396 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\"",
397 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU,",
398 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU extra"})
399 RejectSignal(fixture, broken);
400 RejectSignal(fixture, SignalLine("64", "1", "1+", "1", "0", "0", "1"));
401 RejectSignal(fixture, SignalLine("63", "2", "1+", "1", "0", "0", "3"));
402 RejectSignal(fixture, SignalLine("56", "2", "0+", "1", "0", "0", "3"));
403 RejectSignal(fixture, SignalLine("4294967295", "64", "1+", "1", "0", "0", "1"));
404 RejectSignal(fixture, ordinary, 0U);
405 for (const char delimiter: {'|', '@', '(', ')', '[', ']', '"'}) {
406 std::string line = ordinary;
407 line.erase(line.find(delimiter), 1U);
408 RejectSignal(fixture, line);
409 }
410}
411
412void DbcParserTest::validSignals() {
413 Fixture fixture;
414 const DbcParser signalParser;
415 fixture.Write("BO_ 1 Frame: 8 ECU\n" +
416 SignalLine("0", "8", "1+", "0", "0", "0", "0") + "\n" +
417 SignalLine("8", "8", "1-", "-1", "0", "-255", "0", "Negative") + "\n");
418 const DbcDatabase factors = signalParser.ParseFile(fixture.path.string());
419 CHECK(factors.frames[0].signals.size() == 2U &&
420 factors.frames[0].signals[0].factor == 0.0 &&
421 factors.frames[0].signals[1].factor == -1.0,
422 "Zero and negative factors and equal range boundaries");
423
424 struct SignalCase {
425 const char *start;
426 const char *length;
427 const char *order;
428 unsigned payload;
429 };
430 const SignalCase validSignals[] = {
431 {"0", "1", "1+", 1U},
432 {"7", "1", "0-", 1U},
433 {"0", "64", "1+", 8U},
434 {"7", "64", "0-", 8U},
435 {"63", "1", "1+", 8U},
436 {"56", "1", "0+", 8U},
437 {"3", "12", "0+", 2U},
438 {"2039", "1", "1+", 255U}};
439 for (const SignalCase &test: validSignals) {
440 fixture.Write("BO_ 1 Frame: " + std::to_string(test.payload) + " ECU\n" +
441 SignalLine(test.start, test.length, test.order, ".5e+1", "-2.5E-1", "-1.", "+2e2") + "\n");
442 try {
443 const DbcDatabase parsed = signalParser.ParseFile(fixture.path.string());
444 const SignalInfo &signal = parsed.frames.at(0).signals.at(0);
445 QCOMPARE(signal.startBit, static_cast<std::uint32_t>(std::stoul(test.start)));
446 QCOMPARE(signal.length, static_cast<std::uint32_t>(std::stoul(test.length)));
447 QCOMPARE(signal.isLittleEndian, test.order[0] == '1');
448 QCOMPARE(signal.isSigned, test.order[1] == '-');
449 QCOMPARE(signal.factor, 5.0);
450 QCOMPARE(signal.offset, -0.25);
451 QCOMPARE(signal.minimum, -1.0);
452 QCOMPARE(signal.maximum, 200.0);
453 } catch (const std::exception &error) { CHECK(false, error.what()); }
454 }
455 fixture.Write("BO_ 1 Frame: 8 ECU\n"
456 "SG_\tValue\t:\t0 | 8 @1+ ( 1 , 0 ) [ 0 | 255 ] \"rpm\" ECU1,\tECU2\n");
457 try {
458 const DbcDatabase parsed = signalParser.ParseFile(fixture.path.string());
459 const SignalInfo &signal = parsed.frames.at(0).signals.at(0);
460 CHECK(signal.name == "Value" && signal.unit == "rpm" &&
461 signal.receivers == std::vector<std::string>({"ECU1", "ECU2"}),
462 "Signal tabs, spaces, and receiver list");
463 } catch (const std::exception &error) { CHECK(false, error.what()); }
464}
465
466void DbcParserTest::dispatch() {
467 CountingHandler::instances = 0;
468 CountingHandler::calls = 0;
469 DbcDatabase dispatchDatabase;
470 DbcParseContext context{dispatchDatabase, nullptr};
471 DbcStatementDispatcher dispatcher;
472 dispatcher.RegisterType<CountingHandler>("TEST_");
473 CHECK(dispatcher.Dispatch("TEST_ first", context) &&
474 dispatcher.Dispatch("TEST_ second", context) &&
475 CountingHandler::instances == 1 && CountingHandler::calls == 2 &&
476 dispatchDatabase.frames.size() == 2U,
477 "Factory dispatch and handler reuse");
478 CHECK(!dispatcher.Dispatch("TEST_OTHER data", context) &&
479 !dispatcher.Dispatch("BO_TX_BU_ 1 : ECU;", context) &&
480 !dispatcher.Dispatch("VAL_TABLE_ States 0 \"Stopped\";", context) &&
481 CountingHandler::calls == 2,
482 "Exact and unknown keyword dispatch");
483}
484
485void DbcParserTest::independentSessions() {
486 Fixture fixture;
487 const DbcParser parser;
488 fixture.Write("BO_ 1 First: 8 ECU\n");
489 CHECK(parser.ParseFile(fixture.path.string()).frames.size() == 1U, "First parse");
490 fixture.Write("SG_ Speed : invalid\n");
491 try {
492 parser.ParseFile(fixture.path.string());
493 CHECK(false, "Independent parse context");
494 } catch (const std::runtime_error &error) {
495 CHECK(std::string(error.what()).find("Signal found before any frame") !=
496 std::string::npos,
497 "Independent parse context");
498 }
499 fixture.Write("BO_ 2 Second: 8 ECU\n");
500 const DbcDatabase recovered = parser.ParseFile(fixture.path.string());
501 CHECK(recovered.frames.size() == 1U && recovered.frames[0].canId == 2U,
502 "Fresh parse after error");
503}
504
505
506void DbcParserTest::valueDescriptions() {
507 Fixture fixture;
508 fixture.Write(
509 "VAL_\t256\tState\t0\t\"Off\"\t1 \"On\" 2 \"Error with spaces; and \\\"quotes\\\"\";\n"
510 "BO_ 256 Status: 8 ECU\n"
511 "SG_ State : 0|8@1+ (2,10) [0|520] \"\" ECU\n"
512 "VAL_ 256 State 3 \"\";\n"
513 "VAL_ 2147483904 State -9223372036854775808 \"Minimum\" -1 \"Negative\" 9223372036854775807 \"Maximum\";\n"
514 "BO_ 2147483904 Extended: 8 ECU\n"
515 "SG_ State : 0|64@1- (1,0) [-1e20|1e20] \"\" ECU\n"
516 "BO_ 2048 Unsigned: 8 ECU\n"
517 "SG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n"
518 "VAL_ 2147485696 State 9223372036854775808 \"High\" 18446744073709551615 \"Full\";\n");
519 const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string());
520 const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
521 const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed);
522 CHECK(tree->GetChild(0)->GetSignal() == nullptr, "Frame node");
523 for (std::size_t index = 0; index < parsed.frames.size(); ++index) {
524 CHECK(tree->GetChild(index)->GetChild(0)->GetSignal()->valueDescriptions ==
525 parsed.frames[index].signals[0].valueDescriptions &&
526 runtime.frames[index].signals[0].valueDescriptions ==
527 parsed.frames[index].signals[0].valueDescriptions,
528 "All mappings survive both builders");
529 }
530 CHECK(tree->GetChild(0)->GetChild(0)->GetSignal()->valueDescriptions ==
531 runtime.frames[0].signals[0].valueDescriptions,
532 "Both builders preserve descriptions");
533 RawCanFrame frame;
534 frame.canId = 256;
535 frame.data.resize(8);
536 const std::string expected[] = {"Off", "On", "Error with spaces; and \"quotes\"", ""};
537 for (unsigned value = 0; value < 5; ++value) {
538 frame.data[0] = value;
539 const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0];
540 CHECK(decoded.valid && decoded.physicalValue == value * 2 + 10, "Numeric scaling unchanged");
541 const std::string *description = decoded.GetValueDescription();
542 CHECK(value < 4 ? description && *description == expected[value] : !description,
543 "Raw lookup and unknown value");
544 }
545 frame.isExtended = true;
546 struct Boundary {
547 std::uint64_t bits;
548 const char *description;
549 };
550 for (const Boundary &test: {Boundary{0x8000000000000000ULL, "Minimum"},
551 Boundary{0xFFFFFFFFFFFFFFFFULL, "Negative"},
552 Boundary{0x7FFFFFFFFFFFFFFFULL, "Maximum"}}) {
553 for (unsigned byte = 0; byte < 8; ++byte)
554 frame.data[byte] = static_cast<std::uint8_t>(test.bits >> (byte * 8));
555 const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0];
556 CHECK(decoded.GetValueDescription() && *decoded.GetValueDescription() == test.description,
557 "Signed boundaries and distinct extended frame");
558 }
559 frame.canId = 2048;
560 for (const Boundary &test: {Boundary{0x8000000000000000ULL, "High"},
561 Boundary{0xFFFFFFFFFFFFFFFFULL, "Full"}}) {
562 for (unsigned byte = 0; byte < 8; ++byte)
563 frame.data[byte] = static_cast<std::uint8_t>(test.bits >> (byte * 8));
564 const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0];
565 CHECK(decoded.GetValueDescription() && *decoded.GetValueDescription() == test.description,
566 "Unsigned boundaries");
567 }
568 CHECK(FindValueDescription(runtime.frames[0].signals[0].valueDescriptions, std::int64_t(1)) != nullptr,
569 "Nonnegative signed lookup matches unsigned key");
570 frame.data.clear();
571 CHECK(!DbcDecoder().Decode(runtime, frame).signals[0].GetValueDescription(), "Invalid decode has no description");
572}
573
574void DbcParserTest::invalidValueDescriptions() {
575 Fixture fixture;
576 const std::string definitions = "BO_ 1 Frame: 8 ECU\nSG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n";
577 for (const std::string statement: {
578 "VAL_", "VAL_ 1 State;", "VAL_ 1 State ;", "VAL_ 1 State 0 \"Off\"",
579 "VAL_ 1 State 0 \"Off\"; junk", "VAL_ 1 State 0 Off;",
580 "VAL_ 1 State 0 \"Unclosed;", "VAL_ 1 State 0 \"Off\"1 \"On\";",
581 "VAL_ 1 State 0 \"Off\" 00 \"Duplicate\";",
582 "VAL_ 1 State 0 \"Off\" -0 \"Duplicate\";",
583 "VAL_ 1 State 0 \"Off\";\nVAL_ 1 State 0 \"Duplicate\";",
584 "VAL_ 2 State 0 \"Missing frame\";", "VAL_ 1 Missing 0 \"Missing signal\";",
585 "VAL_ -1 State 0 \"Bad ID\";", "VAL_ 536870912 State 0 \"Bad ID\";",
586 "VAL_ 4294967296 State 0 \"Bad ID\";", "VAL_ Environment 0 \"Unsupported\";",
587 "VAL_ 1 State 0 \"Bad\\q escape\";"}) {
588 fixture.Write(definitions + statement + '\n');
589 const int line = statement.find('\n') == std::string::npos ? 3 : 4;
590 try {
591 DbcParser().ParseFile(fixture.path.string());
592 CHECK(false, statement);
593 } catch (const std::runtime_error &error) {
594 CHECK(std::string(error.what()).find(fixture.path.string() + ":" + std::to_string(line) + ": ") == 0,
595 statement + " " + error.what());
596 }
597 }
598 for (const std::string suffix: {
599 "BO_ 1 Other: 8 ECU\n", // Duplicate frame definition.
600 "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n",// Duplicate signal definition.
601 ""}) {
602 fixture.Write("VAL_ 1 " + std::string(suffix.empty() ? "Missing" : "State") +
603 " 0 \"Description\";\n" + definitions + suffix);
604 try {
605 DbcParser().ParseFile(fixture.path.string());
606 CHECK(false, "Missing reference or duplicate definition");
607 } catch (const std::runtime_error &error) {
608 const std::string line = suffix.empty() ? "1" : "4";
609 CHECK(std::string(error.what()).find(fixture.path.string() + ":" + line + ": ") == 0,
610 "Error retains the reference or duplicate definition source location");
611 }
612 }
613 for (const std::string number: {"+1", "1x", "0x10", "1.5", "1e2", "--1",
614 "-9223372036854775809", "18446744073709551616"}) {
615 fixture.Write("VAL_ 1 State " + number + " \"Bad\";\n" + definitions);
616 try {
617 DbcParser().ParseFile(fixture.path.string());
618 CHECK(false, number);
619 } catch (const std::runtime_error &error) {
620 CHECK(std::string(error.what()).find(fixture.path.string() + ":1: ") == 0, number);
621 }
622 }
623}
624
625int RunParserTests(int argc, char **argv) {
626 DbcParserTest tests;
627 return QTest::qExec(&tests, argc, argv);
628}
629
630#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:72
DecodedFrameValue Decode(const DecodeDatabase &database, const RawCanFrame &frame) const
Decode one raw CAN frame.
Minimal DBC parser.
Definition dbc_parser.h:34
DbcDatabase ParseFile(const std::string &filePath) const
Parse DBC file.
bool Dispatch(const std::string &line, DbcParseContext &context) const
Builds a simple tree from parsed DBC database.
std::unique_ptr< TreeNode > Build(const DbcDatabase &database) const
Build tree representation of parsed DBC data.
Created: 2026-03-13 Author: Deeaitch (Dim. Himro)
Created: 2026-03-13 Author: Deeaitch (Dim. Himro)
Created: 2026-03-13 Author: Deeaitch (Dim. Himro)
Created: 2026-03-13 Author: Deeaitch (Dim. Himro)
Parsed DBC content stored in a simple internal form.
std::vector< FrameInfo > frames
Runtime decode database with fast lookup by CAN ID.
std::string comment
Fully decoded frame.
Definition dbc_decoder.h:61
const DecodeFrame * definition
Definition dbc_decoder.h:62
One decoded signal value.
Definition dbc_decoder.h:43
const std::string * GetValueDescription() const
Definition dbc_decoder.h:51
Describes one CAN frame from a DBC file.
Definition frame_info.h:24
bool isExtended
Definition frame_info.h:27
std::string transmitter
Definition frame_info.h:31
std::uint8_t dlc
Definition frame_info.h:30
std::string name
Definition frame_info.h:25
std::uint32_t canId
Definition frame_info.h:26
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:24
std::uint32_t length
Definition signal_info.h:28
std::string unit
Definition signal_info.h:35
std::vector< std::string > receivers
Definition signal_info.h:36
std::string name
Definition signal_info.h:26
bool isSigned
Definition signal_info.h:30
double offset
Definition signal_info.h:32
std::uint32_t startBit
Definition signal_info.h:27
double minimum
Definition signal_info.h:33
double maximum
Definition signal_info.h:34
double factor
Definition signal_info.h:31
bool isLittleEndian
Definition signal_info.h:29