DBC Framework
DBC parsing and CAN signal decoding framework
Loading...
Searching...
No Matches
dbc_coverage_test.cpp
1// Additional regression coverage. Kept separate for review before the first run.
3#include "dbc_decoder.h"
4#include "dbc_parser.h"
5#include "dbc_statement_handler.h"
6#include "dbc_tree_builder.h"
7#include "typefactory.h"
8#include <QTemporaryFile>
9#include <QtTest>
10#include <array>
11#include <limits>
12#include <stdexcept>
13
14namespace {
15 DbcDatabase ParseText(const std::string &text) {
16 QTemporaryFile file;
17 if (!file.open()) throw std::runtime_error("Cannot create DBC fixture");
18 if (file.write(text.data(), static_cast<qint64>(text.size())) != static_cast<qint64>(text.size()) || !file.flush())
19 throw std::runtime_error("Cannot write DBC fixture");
20 return DbcParser().ParseFile(file.fileName().toStdString());
21 }
22
23 DecodeDatabase SingleSignal(const DecodeSignal &signal) {
24 DecodeFrame frame;
25 frame.canId = 17U;
26 frame.signals.push_back(signal);
27 return DecodeDatabase({frame});
28 }
29
30 // Independent reference: collect the visited bits first, then weight them.
31 // Motorola uses a linear MSB-first coordinate instead of sawtooth traversal.
32 std::uint64_t ReferenceExtract(const std::vector<std::uint8_t> &data,
33 ByteOrder order, unsigned start, unsigned length) {
34 std::uint64_t expected = 0;
35 for (unsigned i = 0; i < length; ++i) {
36 const unsigned coordinate = order == ByteOrder::Intel
37 ? start + i
38 : (start / 8U) * 8U + (7U - start % 8U) + i;
39 const unsigned bit = order == ByteOrder::Intel ? coordinate % 8U : 7U - coordinate % 8U;
40 const unsigned weight = order == ByteOrder::Intel ? i : length - 1U - i;
41 expected |= std::uint64_t((data.at(coordinate / 8U) >> bit) & 1U) << weight;
42 }
43 return expected;
44 }
45
46 struct FactoryBase {
47 virtual ~FactoryBase() = default;
48 virtual int Value() const = 0;
49 };
50 struct FactoryFirst : FactoryBase {
51 explicit FactoryFirst(std::unique_ptr<int> value) : value(std::move(value)) {}
52 int Value() const override { return *value; }
53 std::unique_ptr<int> value;
54 };
55 struct FactorySecond : FactoryFirst {
56 using FactoryFirst::FactoryFirst;
57 int Value() const override { return *value + 1; }
58 };
59}// namespace
60
61class CoverageTest : public QObject {
62 Q_OBJECT
63private Q_SLOTS:
64 void frameLookup();
65 void signedBoundaries_data();
66 void signedBoundaries();
67 void extraction_data();
68 void extraction();
69 void invalidSignalStatus_data();
70 void invalidSignalStatus();
71 void malformedRuntimeMultiplexing_data();
72 void malformedRuntimeMultiplexing();
73 void pgn_data();
74 void pgn();
75 void comments_data();
76 void comments();
77 void ignoredCommentStatements_data();
78 void ignoredCommentStatements();
79 void ambiguousValueReferences_data();
80 void ambiguousValueReferences();
81 void valueReferenceScope();
82 void fileFormatting_data();
83 void fileFormatting();
84 void missingFile();
85 void builders();
86 void physicalConversion_data();
87 void physicalConversion();
88 void multiplexingScenarios();
89 void multiplexingErrorLocations_data();
90 void multiplexingErrorLocations();
91 void escapedValueDescriptions();
92 void factory();
93};
94
95void CoverageTest::frameLookup() {
96 DbcDecoder decoder;
97 RawCanFrame raw;
98 raw.canId = 17U;
99 DecodeDatabase empty;
100 QVERIFY(decoder.FindFrame(empty, 17U, false) == nullptr);
101 const DecodedFrameValue absent = decoder.Decode(empty, raw);
102 QVERIFY(!absent.valid);
103 QVERIFY(absent.definition == nullptr);
104 QVERIFY(absent.signals.empty());
105 DecodeSignal signal;
106 DecodeDatabase database = SingleSignal(signal);
107 QVERIFY(decoder.FindFrame(database, 18U, false) == nullptr);
108 QVERIFY(decoder.FindFrame(database, 17U, true) == nullptr);
109 raw.isExtended = true;
110 QVERIFY(!decoder.Decode(database, raw).valid);
111 raw.isExtended = false;
112 raw.canId = 18U;
113 QVERIFY(!decoder.Decode(database, raw).valid);
114 raw.canId = 17U;
115 DecodeFrame noSignals;
116 noSignals.canId = 17U;
117 database = DecodeDatabase({noSignals});
118 const DecodedFrameValue matched = decoder.Decode(database, raw);
119 QVERIFY(matched.valid);
120 QCOMPARE(matched.definition.get(), &database.GetFrames()[0]);
121 QVERIFY(matched.signals.empty());
122}
123
124void CoverageTest::signedBoundaries_data() {
125 QTest::addColumn<int>("order");
126 QTest::addColumn<unsigned>("length");
127 QTest::addColumn<qlonglong>("expected");
128 for (int order = 0; order < 2; ++order) {
129 for (unsigned length: {1U, 2U, 7U, 8U, 31U, 32U, 63U}) {
130 const std::int64_t half = std::int64_t(1) << (length - 1U);
131 const std::array<std::int64_t, 4> values{{0, -1, -half, half - 1}};
132 for (unsigned index = 0; index < values.size(); ++index) {
133 const QByteArray name = QByteArray::number(order) + "-" + QByteArray::number(length) + "-" + QByteArray::number(index);
134 QTest::newRow(name.constData()) << order << length << qlonglong(values[index]);
135 }
136 }
137 }
138}
139
140void CoverageTest::signedBoundaries() {
141 QFETCH(int, order);
142 QFETCH(unsigned, length);
143 QFETCH(qlonglong, expected);
144 DecodeSignal signal;
145 signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola;
146 signal.startBit = order == 0 ? 0U : 7U;
147 signal.length = length;
148 signal.valueType = ValueType::Signed;
149 RawCanFrame raw;
150 raw.canId = 17U;
151 raw.data.assign(8U, 0U);
152 const std::uint64_t bits = static_cast<std::uint64_t>(expected);
153 for (unsigned i = 0; i < length; ++i) {
154 const unsigned weight = order == 0 ? i : length - 1U - i;
155 const unsigned position = order == 0 ? i % 8U : 7U - i % 8U;
156 raw.data[i / 8U] |= static_cast<std::uint8_t>(((bits >> weight) & 1U) << position);
157 }
158 const DecodeDatabase database = SingleSignal(signal);
159 const DecodedFrameValue result = DbcDecoder().Decode(database, raw);
160 QVERIFY(result.signals[0].valid);
161 QVERIFY(std::holds_alternative<std::int64_t>(result.signals[0].rawValue));
162 QCOMPARE(std::get<std::int64_t>(result.signals[0].rawValue), static_cast<std::int64_t>(expected));
163}
164
165void CoverageTest::extraction_data() {
166 QTest::addColumn<int>("order");
167 QTest::addColumn<unsigned>("start");
168 QTest::addColumn<unsigned>("length");
169 for (int order = 0; order < 2; ++order)
170 for (unsigned start = 0; start < 16U; ++start)
171 for (unsigned length = 1; length <= 64U; ++length) {
172 const QByteArray name = QByteArray::number(order) + "-" + QByteArray::number(start) + "-" + QByteArray::number(length);
173 QTest::newRow(name.constData()) << order << start << length;
174 }
175}
176
177void CoverageTest::extraction() {
178 QFETCH(int, order);
179 QFETCH(unsigned, start);
180 QFETCH(unsigned, length);
181 DecodeSignal signal;
182 signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola;
183 signal.startBit = start;
184 signal.length = length;
185 const DecodeDatabase database = SingleSignal(signal);
186 RawCanFrame raw;
187 raw.canId = 17U;
188 const std::vector<std::vector<std::uint8_t>> patterns = {
189 std::vector<std::uint8_t>(10U, 0U),
190 std::vector<std::uint8_t>(10U, 255U),
191 {0xA5, 0x3C, 0x81, 0x7E, 0x12, 0xD4, 0x69, 0xB0, 0x5A, 0xC3}};
192 for (const std::vector<std::uint8_t> &data: patterns) {
193 raw.data = data;
194 const unsigned firstCoordinate = order == 0 ? start : (start / 8U) * 8U + 7U - start % 8U;
195 raw.data.resize((firstCoordinate + length + 7U) / 8U);
196 const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw);
197 QVERIFY(decoded.signals[0].valid);
198 QCOMPARE(std::get<std::uint64_t>(decoded.signals[0].rawValue), ReferenceExtract(data, signal.byteOrder, start, length));
199 }
200}
201
202void CoverageTest::invalidSignalStatus_data() {
203 QTest::addColumn<int>("order");
204 QTest::addColumn<unsigned>("start");
205 QTest::addColumn<unsigned>("length");
206 QTest::addColumn<unsigned>("bytes");
207 QTest::addColumn<int>("status");
208 for (int order = 0; order < 2; ++order) {
209 const unsigned start = order == 0 ? 0U : 7U;
210 const QByteArray prefix = QByteArray::number(order);
211 QTest::newRow((prefix + "-zero").constData()) << order << start << 0U << 1U << int(SignalDecodeStatus::InvalidDefinition);
212 QTest::newRow((prefix + "-65").constData()) << order << start << 65U << 9U << int(SignalDecodeStatus::InvalidDefinition);
213 QTest::newRow((prefix + "-empty").constData()) << order << start << 8U << 0U << int(SignalDecodeStatus::InsufficientPayload);
214 QTest::newRow((prefix + "-short").constData()) << order << start << 16U << 1U << int(SignalDecodeStatus::InsufficientPayload);
215 QTest::newRow((prefix + "-large-start").constData()) << order << std::numeric_limits<unsigned>::max() << 8U << 1U << int(SignalDecodeStatus::InsufficientPayload);
216 }
217}
218
219void CoverageTest::invalidSignalStatus() {
220 QFETCH(int, order);
221 QFETCH(unsigned, start);
222 QFETCH(unsigned, length);
223 QFETCH(unsigned, bytes);
224 QFETCH(int, status);
225 DecodeSignal signal;
226 signal.startBit = start;
227 signal.length = length;
228 signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola;
229 const DecodeDatabase database = SingleSignal(signal);
230 RawCanFrame raw;
231 raw.canId = 17U;
232 raw.data.assign(bytes, 255U);
233 const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw);
234 QVERIFY(decoded.valid);
235 QVERIFY(!decoded.signals[0].valid);
236 QCOMPARE(int(decoded.signals[0].status), status);
237 QVERIFY(decoded.signals[0].GetValueDescription() == nullptr);
238}
239
240void CoverageTest::malformedRuntimeMultiplexing_data() {
241 QTest::addColumn<int>("kind");
242 QTest::newRow("two-selectors") << 0;
243 QTest::newRow("signed-selector") << 1;
244 QTest::newRow("65-bit-selector") << 2;
245 QTest::newRow("branch-out-of-range") << 3;
246}
247
248void CoverageTest::malformedRuntimeMultiplexing() {
249 QFETCH(int, kind);
250 DecodeSignal selector;
251 selector.length = 1U;
252 selector.multiplexing.role = MultiplexRole::Selector;
253 selector.name = "Selector";
254 std::vector<DecodeFrame> definitions = SingleSignal(selector).GetFrames();
255 DecodeSignal branch;
256 branch.name = "Branch";
257 branch.startBit = 8U;
258 branch.length = 8U;
259 branch.multiplexing.role = MultiplexRole::Branch;
260 branch.multiplexing.value = 1U;
261 definitions[0].signals.push_back(branch);
262 DecodeSignal ordinary;
263 ordinary.name = "Ordinary";
264 ordinary.startBit = 16U;
265 ordinary.length = 8U;
266 definitions[0].signals.push_back(ordinary);
267 if (kind == 0) {
268 selector.name = "SecondSelector";
269 definitions[0].signals.push_back(selector);
270 }
271 if (kind == 1) definitions[0].signals[0].valueType = ValueType::Signed;
272 if (kind == 2) definitions[0].signals[0].length = 65U;
273 if (kind == 3) definitions[0].signals[1].multiplexing.value = 2U;
274 const DecodeDatabase database(std::move(definitions));
275 RawCanFrame raw;
276 raw.canId = 17U;
277 raw.data = {1U, 42U, 99U};
278 const DecodedFrameValue result = DbcDecoder().Decode(database, raw);
279 QCOMPARE(int(result.signals[1].status), int(SignalDecodeStatus::InvalidDefinition));
280 QVERIFY(!result.signals[1].valid);
281 QVERIFY(result.signals[2].valid);
282 QCOMPARE(std::get<std::uint64_t>(result.signals[2].rawValue), std::uint64_t(99));
283 if (kind != 3) {
284 QCOMPARE(int(result.signals[0].status), int(SignalDecodeStatus::InvalidDefinition));
285 if (kind == 0) QCOMPARE(int(result.signals[3].status), int(SignalDecodeStatus::InvalidDefinition));
286 }
287}
288
289void CoverageTest::pgn_data() {
290 QTest::addColumn<unsigned>("id");
291 QTest::addColumn<unsigned>("expected");
292 QTest::addColumn<bool>("hasPgn");
293 QTest::newRow("standard") << 0x7FFU << 0U << false;
294 for (unsigned dp: {0U, 1U})
295 for (unsigned pf: {239U, 240U})
296 for (unsigned ps: {0U, 0xABU}) {
297 const unsigned id = 0x80000000U | (dp << 24U) | (pf << 16U) | (ps << 8U) | 0x42U;
298 const unsigned expected = (dp << 16U) | (pf == 239U ? 0xEF00U : 0xF000U | ps);
299 const QByteArray name = QByteArray::number(dp) + "-" + QByteArray::number(pf) + "-" + QByteArray::number(ps);
300 QTest::newRow(name.constData()) << id << expected << true;
301 }
302}
303
304void CoverageTest::pgn() {
305 QFETCH(unsigned, id);
306 QFETCH(unsigned, expected);
307 QFETCH(bool, hasPgn);
308 const DbcDatabase parsed = ParseText("BO_ " + std::to_string(id) + " Frame: 8 ECU\n");
309 QCOMPARE(parsed.frames[0].hasPgn, hasPgn);
310 QCOMPARE(parsed.frames[0].pgn, expected);
311}
312
313void CoverageTest::comments_data() {
314 QTest::addColumn<QString>("before");
315 QTest::addColumn<QString>("after");
316 QTest::addColumn<QString>("frameComment");
317 QTest::addColumn<QString>("signalComment");
318 QTest::newRow("tabs") << QString::fromUtf8("") << QString::fromUtf8("CM_\tBO_ 1 \"Frame\";\nCM_\tSG_ 1 State \"Signal\";\n") << QString::fromUtf8("Frame") << QString::fromUtf8("Signal");
319 QTest::newRow("empty-overwrite") << QString::fromUtf8("") << QString::fromUtf8("CM_ BO_ 1 \"Old\";\nCM_ BO_ 1 \"\";\nCM_ SG_ 1 State \"Old\";\nCM_ SG_ 1 State \"\";\n") << QString::fromUtf8("") << QString::fromUtf8("");
320 QTest::newRow("last-wins") << QString::fromUtf8("") << QString::fromUtf8("CM_ BO_ 1 \"First\";\nCM_ BO_ 1 \"Last\";\nCM_ SG_ 1 State \"First\";\nCM_ SG_ 1 State \"Last\";\n") << QString::fromUtf8("Last") << QString::fromUtf8("Last");
321 QTest::newRow("missing-targets") << QString::fromUtf8("") << QString::fromUtf8("CM_ BO_ 2 \"Unknown\";\nCM_ SG_ 2 State \"Unknown\";\nCM_ SG_ 1 Missing \"Unknown\";\n") << QString::fromUtf8("") << QString::fromUtf8("");
322 // Current documented limitation: comments resolve only already parsed targets.
323 QTest::newRow("before-definitions") << QString::fromUtf8("CM_ BO_ 1 \"Early\";\nCM_ SG_ 1 State \"Early\";\n") << QString::fromUtf8("") << QString::fromUtf8("") << QString::fromUtf8("");
324 QTest::newRow("missing-quotes") << QString::fromUtf8("") << QString::fromUtf8("CM_ BO_ 1 Missing;\nCM_ SG_ 1 State \"Unclosed;\n") << QString::fromUtf8("") << QString::fromUtf8("");
325}
326
327void CoverageTest::comments() {
328 QFETCH(QString, before);
329 QFETCH(QString, after);
330 QFETCH(QString, frameComment);
331 QFETCH(QString, signalComment);
332 const DbcDatabase parsed = ParseText(before.toStdString() + "BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" + after.toStdString());
333 QCOMPARE(parsed.frames[0].comment, frameComment.toStdString());
334 QCOMPARE(parsed.frames[0].signals[0].comment, signalComment.toStdString());
335}
336
337void CoverageTest::ignoredCommentStatements_data() {
338 QTest::addColumn<QString>("statement");
339 QTest::newRow("empty") << QString();
340 QTest::newRow("wrong-keyword") << QStringLiteral("OTHER_ BO_ 1 \"Ignored\";");
341 QTest::newRow("keyword-prefix") << QStringLiteral("CM_OTHER BO_ 1 \"Ignored\";");
342 QTest::newRow("frame-without-cm") << QStringLiteral("BO_ 1 \"Ignored\";");
343 QTest::newRow("missing-subtype") << QStringLiteral("CM_");
344 QTest::newRow("global-comment") << QStringLiteral("CM_ \"Ignored\";");
345 QTest::newRow("node-comment") << QStringLiteral("CM_ BU_ ECU \"Ignored\";");
346 QTest::newRow("environment-comment") << QStringLiteral("CM_ EV_ State \"Ignored\";");
347 QTest::newRow("unknown-subtype") << QStringLiteral("CM_ OTHER_ 1 \"Ignored\";");
348}
349
350void CoverageTest::ignoredCommentStatements() {
351 QFETCH(QString, statement);
352 DbcDatabase database;
353 FrameInfo frame;
354 frame.canId = 1U;
355 frame.name = "Frame";
356 frame.comment = "Existing frame comment";
357 SignalInfo signal;
358 signal.name = "State";
359 signal.comment = "Existing signal comment";
360 frame.signals.push_back(signal);
361 database.frames.push_back(frame);
362 DbcParseContext context{database, &database.frames[0]};
363
364 // The handler is public: callers may bypass keyword dispatch. Ignored input
365 // must preserve existing metadata, including comments already assigned.
366 DbcCommentHandler().Handle(statement.toStdString(), context);
367 QCOMPARE(database.frames.size(), std::size_t(1));
368 QCOMPARE(database.frames[0].canId, 1U);
369 QCOMPARE(database.frames[0].name, frame.name);
370 QCOMPARE(database.frames[0].comment, frame.comment);
371 QCOMPARE(database.frames[0].signals.size(), std::size_t(1));
372 QCOMPARE(database.frames[0].signals[0].name, signal.name);
373 QCOMPARE(database.frames[0].signals[0].comment, signal.comment);
374 QCOMPARE(context.currentFrame, &database.frames[0]);
375}
376
377void CoverageTest::ambiguousValueReferences_data() {
378 QTest::addColumn<bool>("duplicateFrames");
379 QTest::addColumn<bool>("extended");
380 QTest::newRow("duplicate-standard-frames") << true << false;
381 QTest::newRow("duplicate-extended-frames") << true << true;
382 QTest::newRow("duplicate-signals-in-standard-frame") << false << false;
383 QTest::newRow("duplicate-signals-in-extended-frame") << false << true;
384}
385
386void CoverageTest::ambiguousValueReferences() {
387 QFETCH(bool, duplicateFrames);
388 QFETCH(bool, extended);
389 DbcDatabase database;
390 FrameInfo frame;
391 frame.canId = 1U;
392 frame.name = "Frame";
393 frame.isExtended = extended;
394 SignalInfo signal;
395 signal.name = "State";
396 frame.signals.push_back(signal);
397 if (!duplicateFrames) frame.signals.push_back(signal);
398 database.frames.push_back(frame);
399 if (duplicateFrames) database.frames.push_back(frame);
400
401 // ParseFile rejects duplicate definitions earlier. Construct the database
402 // through the public metadata API to exercise the resolver's own checks.
403 DbcParseContext context{database, nullptr};
404 context.lineNumber = 37U;
405 const std::string rawId = extended ? "2147483649" : "1";
406 DbcValueDescriptionHandler().Handle("VAL_ " + rawId + " State 0 \"Off\";", context);
407 QCOMPARE(context.pendingValues.size(), std::size_t(1));
408 QCOMPARE(context.pendingValues[0].lineNumber, std::size_t(37));
409 const std::string path = "manual database.dbc";
410 try {
411 context.ResolveValueDescriptions(path);
412 QFAIL("The resolver must reject an ambiguous target");
413 } catch (const std::runtime_error &error) {
414 const std::string message = duplicateFrames ? "Ambiguous VAL_ frame reference."
415 : "Ambiguous VAL_ signal reference: State";
416 QCOMPARE(std::string(error.what()), path + ":37: " + message);
417 }
418 // Ambiguity must be detected before applying any descriptions.
419 for (const FrameInfo &candidate: database.frames)
420 for (const SignalInfo &candidateSignal: candidate.signals)
421 QVERIFY(candidateSignal.valueDescriptions.empty());
422}
423
424void CoverageTest::valueReferenceScope() {
425 DbcDatabase database;
426 FrameInfo frame;
427 frame.canId = 1U;
428 SignalInfo signal;
429 signal.name = "State";
430 frame.signals.push_back(signal);
431 database.frames.push_back(frame);
432 frame.isExtended = true;
433 database.frames.push_back(frame);
434 frame.canId = 2U;
435 frame.isExtended = false;
436 database.frames.push_back(frame);
437
438 DbcParseContext context{database, nullptr};
439 const DbcValueDescriptionHandler handler;
440 context.lineNumber = 3U;
441 handler.Handle("VAL_ 1 State 0 \"Standard\";", context);
442 context.lineNumber = 7U;
443 handler.Handle("VAL_ 2147483649 State 0 \"Extended\";", context);
444 context.ResolveValueDescriptions("manual database.dbc");
445
446 const std::string *standard = FindValueDescription(database.frames[0].signals[0].valueDescriptions, std::uint64_t(0));
447 const std::string *extended = FindValueDescription(database.frames[1].signals[0].valueDescriptions, std::uint64_t(0));
448 QVERIFY(standard != nullptr);
449 QVERIFY(extended != nullptr);
450 QCOMPARE(*standard, std::string("Standard"));
451 QCOMPARE(*extended, std::string("Extended"));
452 QVERIFY(database.frames[2].signals[0].valueDescriptions.empty());
453}
454
455void CoverageTest::fileFormatting_data() {
456 QTest::addColumn<QString>("text");
457 QTest::addColumn<unsigned>("frames");
458 QTest::newRow("empty") << QString::fromUtf8("") << 0U;
459 QTest::newRow("whitespace") << QString::fromUtf8(" \t\r\n\n\t") << 0U;
460 QTest::newRow("unknown-only") << QString::fromUtf8("VERSION \"test\"\nBU_: ECU\nUNKNOWN_ data\n") << 0U;
461 QTest::newRow("no-final-newline") << QString::fromUtf8("BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU") << 1U;
462 QTest::newRow("crlf") << QString::fromUtf8("\r\nBO_ 1 Frame: 1 ECU\r\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\r\nCM_ BO_ 1 \"Comment\";\r\nVAL_ 1 State 0 \"Off\";\r\n") << 1U;
463}
464
465void CoverageTest::fileFormatting() {
466 QFETCH(QString, text);
467 QFETCH(unsigned, frames);
468 const DbcDatabase parsed = ParseText(text.toStdString());
469 QCOMPARE(parsed.frames.size(), std::size_t(frames));
470 if (frames) {
471 QCOMPARE(parsed.frames[0].signals.size(), std::size_t(1));
472 if (text.contains("\r\nBO_")) {
473 QCOMPARE(parsed.frames[0].comment, std::string("Comment"));
474 const std::string *description = FindValueDescription(parsed.frames[0].signals[0].valueDescriptions, std::uint64_t(0));
475 QVERIFY(description != nullptr);
476 QCOMPARE(*description, std::string("Off"));
477 }
478 }
479}
480
481void CoverageTest::missingFile() {
482 QTemporaryFile file;
483 QVERIFY(file.open());
484 const QString path = file.fileName();
485 QVERIFY(file.remove());
486 try {
487 DbcParser().ParseFile(path.toStdString());
488 QFAIL("Missing file must throw");
489 } catch (const std::runtime_error &error) {
490 QCOMPARE(std::string(error.what()), "Failed to open DBC file: " + path.toStdString());
491 }
492}
493
494void CoverageTest::builders() {
495 const DecodeDatabase emptyRuntime = DbcDecodeBuilder().Build(DbcDatabase());
496 QVERIFY(emptyRuntime.GetFrames().empty());
497 QVERIFY(!emptyRuntime.FindFrame(FrameKey(0U, false)));
498 const std::unique_ptr<TreeNode> emptyTree = DbcTreeBuilder().Build(DbcDatabase());
499 QCOMPARE(emptyTree->GetType(), NodeType::Root);
500 QCOMPARE(emptyTree->GetName(), std::string("dbc"));
501 QCOMPARE(emptyTree->GetChildCount(), std::size_t(0));
502 QVERIFY(emptyTree->GetFrame() == nullptr && emptyTree->GetSignal() == nullptr);
503 const DbcDatabase expected = ParseText(
504 "BO_ 256 Standard: 3 ECU1\n"
505 "SG_ Mode M : 0|8@1+ (2,3) [0|513] \"mode\" ECU1,ECU2\n"
506 "SG_ Branch m1 : 15|8@0- (-2,5) [-249|261] \"rpm\" ECU2,ECU3\n"
507 "SG_ Always : 16|8@1+ (0.25,-1) [-1|62.75] \"V\" ECU3\n"
508 "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Branch \"Branch comment\";\n"
509 "VAL_ 256 Branch -1 \"Negative\" 1 \"Positive\";\n"
510 "BO_ 2163212866 Extended: 8 ECU4\n");
511 DecodeDatabase runtime;
512 std::unique_ptr<TreeNode> tree;
513 {
514 DbcDatabase source = expected;
515 runtime = DbcDecodeBuilder().Build(source);
516 tree = DbcTreeBuilder().Build(source);
517 source.frames[0].name = "Changed";
518 source.frames[0].signals[1].receivers.clear();
519 source.frames[0].signals[1].valueDescriptions.clear();
520 source.frames.clear();
521 }
522 QCOMPARE(runtime.GetFrames().size(), expected.frames.size());
523 QCOMPARE(tree->GetChildCount(), expected.frames.size());
524 for (std::size_t i = 0; i < expected.frames.size(); ++i) {
525 const FrameInfo &frame = expected.frames[i];
526 const DecodeFrame &decodedFrame = runtime.GetFrames()[i];
527 const TreeNode *node = tree->GetChild(i);
528 QVERIFY(node && node->GetFrame());
529 QCOMPARE(node->GetType(), NodeType::Frame);
530 QCOMPARE(node->GetName(), frame.name);
531 QVERIFY(node->GetSignal() == nullptr);
532 QCOMPARE(runtime.FindFrame(FrameKey(frame.canId, frame.isExtended)).get(), &decodedFrame);
533 const FrameInfo &copied = *node->GetFrame();
534 QCOMPARE(copied.name, frame.name);
535 QCOMPARE(decodedFrame.name, frame.name);
536 QCOMPARE(copied.canId, frame.canId);
537 QCOMPARE(decodedFrame.canId, frame.canId);
538 QCOMPARE(copied.isExtended, frame.isExtended);
539 QCOMPARE(decodedFrame.isExtended, frame.isExtended);
540 QCOMPARE(copied.dlc, frame.dlc);
541 QCOMPARE(decodedFrame.dlc, frame.dlc);
542 QCOMPARE(copied.pgn, frame.pgn);
543 QCOMPARE(decodedFrame.pgn, frame.pgn);
544 QCOMPARE(copied.hasPgn, frame.hasPgn);
545 QCOMPARE(decodedFrame.hasPgn, frame.hasPgn);
546 QCOMPARE(copied.transmitter, frame.transmitter);
547 QCOMPARE(decodedFrame.transmitter, frame.transmitter);
548 QCOMPARE(copied.comment, frame.comment);
549 QCOMPARE(decodedFrame.comment, frame.comment);
550 QCOMPARE(node->GetChildCount(), frame.signals.size());
551 QCOMPARE(copied.signals.size(), frame.signals.size());
552 QCOMPARE(decodedFrame.signals.size(), frame.signals.size());
553 for (std::size_t j = 0; j < frame.signals.size(); ++j) {
554 const SignalInfo &original = frame.signals[j];
555 const DecodeSignal &target = decodedFrame.signals[j];
556 const TreeNode *leaf = node->GetChild(j);
557 QVERIFY(leaf && leaf->GetSignal());
558 QCOMPARE(leaf->GetType(), NodeType::Signal);
559 QCOMPARE(leaf->GetName(), original.name);
560 QVERIFY(leaf->GetFrame() == nullptr);
561 // Check both the frame payload copy and each child payload copy.
562 for (const SignalInfo *copy: {&copied.signals[j], leaf->GetSignal()}) {
563 QCOMPARE(copy->name, original.name);
564 QCOMPARE(copy->startBit, original.startBit);
565 QCOMPARE(copy->length, original.length);
566 QCOMPARE(copy->isLittleEndian, original.isLittleEndian);
567 QCOMPARE(copy->isSigned, original.isSigned);
568 QCOMPARE(copy->factor, original.factor);
569 QCOMPARE(copy->offset, original.offset);
570 QCOMPARE(copy->minimum, original.minimum);
571 QCOMPARE(copy->maximum, original.maximum);
572 QCOMPARE(copy->unit, original.unit);
573 QCOMPARE(copy->comment, original.comment);
574 QVERIFY(copy->receivers == original.receivers);
575 QVERIFY(copy->valueDescriptions == original.valueDescriptions);
576 QCOMPARE(copy->multiplexing.role, original.multiplexing.role);
577 QCOMPARE(copy->multiplexing.value, original.multiplexing.value);
578 }
579 QCOMPARE(target.name, original.name);
580 QCOMPARE(target.startBit, original.startBit);
581 QCOMPARE(target.length, original.length);
582 QCOMPARE(target.byteOrder, original.isLittleEndian ? ByteOrder::Intel : ByteOrder::Motorola);
583 QCOMPARE(target.valueType, original.isSigned ? ValueType::Signed : ValueType::Unsigned);
584 QCOMPARE(target.factor, original.factor);
585 QCOMPARE(target.offset, original.offset);
586 QCOMPARE(target.minimum, original.minimum);
587 QCOMPARE(target.maximum, original.maximum);
588 QCOMPARE(target.unit, original.unit);
589 QCOMPARE(target.comment, original.comment);
590 QVERIFY(target.receivers == original.receivers);
591 QVERIFY(target.valueDescriptions == original.valueDescriptions);
592 QCOMPARE(target.multiplexing.role, original.multiplexing.role);
593 QCOMPARE(target.multiplexing.value, original.multiplexing.value);
594 }
595 }
596}
597
598void CoverageTest::physicalConversion_data() {
599 QTest::addColumn<double>("factor");
600 QTest::addColumn<double>("offset");
601 QTest::addColumn<double>("expected");
602 QTest::newRow("zero-factor") << 0.0 << 7.0 << 7.0;
603 QTest::newRow("negative-factor") << -2.0 << 5.0 << -15.0;
604 QTest::newRow("above-declared-range") << 2.0 << 3.0 << 23.0;
605}
606
607void CoverageTest::physicalConversion() {
608 QFETCH(double, factor);
609 QFETCH(double, offset);
610 QFETCH(double, expected);
611 DecodeSignal signal;
612 signal.length = 8U;
613 signal.factor = factor;
614 signal.offset = offset;
615 signal.minimum = 0.0;
616 signal.maximum = 1.0;
617 const DecodeDatabase database = SingleSignal(signal);
618 RawCanFrame raw;
619 raw.canId = 17U;
620 raw.data = {10U};
621 const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw);
622 QVERIFY(decoded.signals[0].valid);
623 QCOMPARE(std::get<std::uint64_t>(decoded.signals[0].rawValue), std::uint64_t(10));
624 QCOMPARE(decoded.signals[0].physicalValue, expected);
625}
626
627void CoverageTest::multiplexingScenarios() {
628 const DecodeDatabase selectorOnly = DbcDecodeBuilder().Build(ParseText(
629 "BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"));
630 RawCanFrame raw;
631 raw.canId = 1U;
632 raw.data = {1U};
633 const DecodedFrameValue only = DbcDecoder().Decode(selectorOnly, raw);
634 QCOMPARE(only.signals.size(), std::size_t(1));
635 QVERIFY(only.signals[0].valid);
636 const DecodeDatabase database = DbcDecodeBuilder().Build(ParseText(
637 "BO_ 1 First: 3 ECU\n"
638 "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"
639 "SG_ A m0 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
640 "SG_ B m0 : 16|8@1+ (1,0) [0|255] \"\" ECU\n"
641 "BO_ 2 Second: 2 ECU\n"
642 "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"
643 "SG_ A m1 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"));
644 raw.data = {0U, 42U, 99U};
645 const DecodedFrameValue first = DbcDecoder().Decode(database, raw);
646 QVERIFY(first.signals[1].valid && first.signals[2].valid);
647 QCOMPARE(std::get<std::uint64_t>(first.signals[1].rawValue), std::uint64_t(42));
648 QCOMPARE(std::get<std::uint64_t>(first.signals[2].rawValue), std::uint64_t(99));
649 raw.canId = 2U;
650 const DecodedFrameValue secondInactive = DbcDecoder().Decode(database, raw);
651 QCOMPARE(secondInactive.signals[1].status, SignalDecodeStatus::Inactive);
652 raw.data[0] = 1U;
653 QVERIFY(DbcDecoder().Decode(database, raw).signals[1].valid);
654 raw.canId = 1U;
655 const DecodedFrameValue firstInactive = DbcDecoder().Decode(database, raw);
656 QCOMPARE(firstInactive.signals[1].status, SignalDecodeStatus::Inactive);
657 QCOMPARE(firstInactive.signals[2].status, SignalDecodeStatus::Inactive);
658}
659
660void CoverageTest::multiplexingErrorLocations_data() {
661 QTest::addColumn<QString>("text");
662 QTest::addColumn<unsigned>("line");
663 QTest::addColumn<QString>("message");
664 const QString header = "BO_ 1 Frame: 3 ECU\n\n";
665 const QString selector = "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n";
666 const QString branch = "SG_ A m1 : 8|8@1+ (1,0) [0|255] \"\" ECU\n";
667 QTest::newRow("missing-selector") << header + branch << 3U << QString::fromUtf8("Multiplexed branch has no selector.");
668 QTest::newRow("second-selector") << header + selector + "\nSG_ Other M : 16|1@1+ (1,0) [0|1] \"\" ECU\n"
669 << 5U << QString::fromUtf8("Multiplexing requires one unsigned selector per frame.");
670 QTest::newRow("signed-selector") << header + "SG_ Mode M : 0|1@1- (1,0) [-1|0] \"\" ECU\n"
671 << 3U << QString::fromUtf8("Multiplexing requires one unsigned selector per frame.");
672 QTest::newRow("branch-range") << header + "SG_ A m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n" + selector << 3U << QString::fromUtf8("Branch value exceeds selector raw range.");
673 QTest::newRow("same-branch-overlap") << header + selector + branch + "SG_ B m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n"
674 << 5U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions.");
675 QTest::newRow("selector-overlap") << header + selector + "SG_ A m1 : 0|1@1+ (1,0) [0|1] \"\" ECU\n"
676 << 4U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions.");
677 QTest::newRow("unconditional-overlap") << header + selector + branch + "SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
678 << 5U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions.");
679 QTest::newRow("ordinary-overlap") << header + "SG_ A : 8|8@1+ (1,0) [0|255] \"\" ECU\nSG_ B : 15|8@0+ (1,0) [0|255] \"\" ECU\n"
680 << 4U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions.");
681 QTest::newRow("extended-multiplexing") << header + "SG_MUL_VAL_ 1 A Mode 1-2;\n"
682 << 3U << QString::fromUtf8("Extended and nested multiplexing are unsupported.");
683 QTest::newRow("nested-marker") << header + "SG_ A m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n"
684 << 3U << QString::fromUtf8("Invalid or unsupported multiplexing marker.");
685}
686
687void CoverageTest::multiplexingErrorLocations() {
688 QFETCH(QString, text);
689 QFETCH(unsigned, line);
690 QFETCH(QString, message);
691 QTemporaryFile file;
692 QVERIFY(file.open());
693 const QByteArray content = text.toUtf8();
694 QCOMPARE(file.write(content), qint64(content.size()));
695 QVERIFY(file.flush());
696 try {
697 DbcParser().ParseFile(file.fileName().toStdString());
698 QFAIL("Expected multiplexing rejection");
699 } catch (const std::runtime_error &error) {
700 QCOMPARE(QString::fromStdString(error.what()), file.fileName() + ":" + QString::number(line) + ": " + message);
701 }
702}
703
704void CoverageTest::escapedValueDescriptions() {
705 const DbcDatabase parsed = ParseText(
706 "BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"
707 R"dbc(VAL_ 1 State -0 "Ноль" 1 "C:\\trace" 2 "\\\"quoted\"\\";)dbc"
708 "\n");
709 const ValueDescriptions &values = parsed.frames[0].signals[0].valueDescriptions;
710 QCOMPARE(values.size(), std::size_t(3));
711 QVERIFY(values.find(RawSignalValue(std::int64_t(0))) == values.end());
712 const std::string *zero = FindValueDescription(values, std::int64_t(0));
713 const std::string *path = FindValueDescription(values, std::uint64_t(1));
714 const std::string *quoted = FindValueDescription(values, std::uint64_t(2));
715 QVERIFY(zero && path && quoted);
716 QCOMPARE(*zero, std::string("Ноль"));
717 QCOMPARE(*path, std::string("C:\\trace"));
718 QCOMPARE(*quoted, std::string("\\\"quoted\"\\"));
719 QVERIFY(NormalizeValueKey(RawSignalValue(std::int64_t(0))) == RawSignalValue(std::uint64_t(0)));
720 QVERIFY(NormalizeValueKey(RawSignalValue(std::int64_t(-1))) == RawSignalValue(std::int64_t(-1)));
721}
722
723void CoverageTest::factory() {
725 QVERIFY(!factory.contains("value"));
726 QVERIFY_THROWS_EXCEPTION(std::out_of_range, static_cast<void>(factory.creator("missing")));
727 QVERIFY_THROWS_EXCEPTION(std::out_of_range, static_cast<void>(factory.create("missing", std::make_unique<int>(1))));
728 factory.registerType<FactoryFirst>("value");
729 QVERIFY(factory.contains("value"));
730 std::unique_ptr<int> argument = std::make_unique<int>(42);
731 const std::shared_ptr<FactoryBase> first = factory.create("value", std::move(argument));
732 QVERIFY(argument == nullptr);
733 QCOMPARE(first->Value(), 42);
734 const TypeFactory<std::string, FactoryBase, std::unique_ptr<int>>::CreatorFn creator = factory.creator("value");
735 QCOMPARE(creator(std::make_unique<int>(7))->Value(), 7);
736 factory.registerType<FactorySecond>("value");
737 QCOMPARE(factory.create("value", std::make_unique<int>(42))->Value(), 43);
738 QCOMPARE(first->Value(), 42);
739 QCOMPARE(creator(std::make_unique<int>(7))->Value(), 7);
740}
741
742int RunCoverageTests(int argc, char **argv) {
743 CoverageTest tests;
744 return QTest::qExec(&tests, argc, argv);
745}
746#include "dbc_coverage_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:74
DecodedFrameValue Decode(const DecodeDatabase &database, const RawCanFrame &frame) const
Decode one raw CAN frame.
std::shared_ptr< const DecodeFrame > FindFrame(const DecodeDatabase &database, std::uint32_t canId, bool isExtended) const
Find frame definition by CAN ID.
Minimal DBC parser.
Definition dbc_parser.h:33
DbcDatabase ParseFile(const std::string &filePath) const
Parse DBC file.
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...
std::shared_ptr< const DecodeFrame > FindFrame(const FrameKey &key) const
const std::vector< DecodeFrame > & GetFrames() const
Tree node for later use in model/view or other hierarchy consumers.
Definition tree_node.h:35
const SignalInfo * GetSignal() const
Get signal payload if node is signal.
Definition tree_node.cpp:64
const TreeNode * GetChild(std::size_t index) const
Get child by index.
Definition tree_node.cpp:38
const std::string & GetName() const
Get display name.
Definition tree_node.cpp:56
std::size_t GetChildCount() const
Get child count.
Definition tree_node.cpp:34
NodeType GetType() const
Get node type.
Definition tree_node.cpp:52
const FrameInfo * GetFrame() const
Get frame payload if node is frame.
Definition tree_node.cpp:60
Registry-based factory for creating objects by runtime identifier.
Definition typefactory.h:59
bool contains(const ClassId &id) const
Checks whether an id is registered.
BasePtr create(const ClassId &id, Args... args) const
Creates an instance for the given id using provided arguments.
Definition typefactory.h:97
void registerType(const ClassId &id)
Registers a Derived type under the given id.
Definition typefactory.h:76
CreatorFn creator(const ClassId &id) const
Returns creator function for the given id.
Definition typefactory.h:89
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)
ByteOrder
Signal byte order used for runtime decoding.
ValueType
Signal numeric type.
Parsed DBC content stored in a simple internal form.
std::vector< FrameInfo > frames
Runtime-ready frame definition.
std::uint32_t pgn
std::string comment
std::uint8_t dlc
std::uint32_t canId
std::string name
std::string transmitter
Runtime-ready signal definition.
std::string unit
std::string name
ValueDescriptions valueDescriptions
ValueType valueType
std::uint32_t startBit
std::vector< std::string > receivers
ByteOrder byteOrder
std::uint32_t length
std::string comment
Fully decoded frame.
Definition dbc_decoder.h:62
std::shared_ptr< const DecodeFrame > definition
Definition dbc_decoder.h:63
Describes one CAN frame from a DBC file.
Definition frame_info.h:24
bool isExtended
Definition frame_info.h:27
bool hasPgn
Definition frame_info.h:29
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::string comment
Definition frame_info.h:32
std::uint32_t pgn
Definition frame_info.h:28
std::uint32_t canId
Definition frame_info.h:26
Stable frame identifier: normalized CAN ID and frame type.
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
ValueDescriptions valueDescriptions
Definition signal_info.h:37
bool isSigned
Definition signal_info.h:30
double offset
Definition signal_info.h:32
std::string comment
Definition signal_info.h:38
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