// Additional regression coverage. Kept separate for review before the first run. #include "dbc_decode_builder.h" #include "dbc_decoder.h" #include "dbc_parser.h" #include "dbc_statement_handler.h" #include "dbc_tree_builder.h" #include "typefactory.h" #include #include #include #include #include namespace { DbcDatabase ParseText(const std::string &text) { QTemporaryFile file; if (!file.open()) throw std::runtime_error("Cannot create DBC fixture"); if (file.write(text.data(), static_cast(text.size())) != static_cast(text.size()) || !file.flush()) throw std::runtime_error("Cannot write DBC fixture"); return DbcParser().ParseFile(file.fileName().toStdString()); } DecodeDatabase SingleSignal(const DecodeSignal &signal) { DecodeFrame frame; frame.canId = 17U; frame.dlc = 16U; frame.signals.push_back(signal); return DecodeDatabase({frame}); } // Independent reference: collect the visited bits first, then weight them. // Motorola uses a linear MSB-first coordinate instead of sawtooth traversal. std::uint64_t ReferenceExtract(const std::vector &data, ByteOrder order, unsigned start, unsigned length) { std::uint64_t expected = 0; for (unsigned i = 0; i < length; ++i) { const unsigned coordinate = order == ByteOrder::Intel ? start + i : (start / 8U) * 8U + (7U - start % 8U) + i; const unsigned bit = order == ByteOrder::Intel ? coordinate % 8U : 7U - coordinate % 8U; const unsigned weight = order == ByteOrder::Intel ? i : length - 1U - i; expected |= std::uint64_t((data.at(coordinate / 8U) >> bit) & 1U) << weight; } return expected; } struct FactoryBase { virtual ~FactoryBase() = default; virtual int Value() const = 0; }; struct FactoryFirst : FactoryBase { explicit FactoryFirst(std::unique_ptr value) : value(std::move(value)) {} int Value() const override { return *value; } std::unique_ptr value; }; struct FactorySecond : FactoryFirst { using FactoryFirst::FactoryFirst; int Value() const override { return *value + 1; } }; }// namespace class CoverageTest : public QObject { Q_OBJECT private Q_SLOTS: void frameLookup(); void signedBoundaries_data(); void signedBoundaries(); void extraction_data(); void extraction(); void invalidSignalStatus_data(); void invalidSignalStatus(); void malformedRuntimeMultiplexing_data(); void malformedRuntimeMultiplexing(); void pgn_data(); void pgn(); void comments_data(); void comments(); void ignoredCommentStatements_data(); void ignoredCommentStatements(); void ambiguousValueReferences_data(); void ambiguousValueReferences(); void valueReferenceScope(); void fileFormatting_data(); void fileFormatting(); void missingFile(); void builders(); void physicalConversion_data(); void physicalConversion(); void multiplexingScenarios(); void multiplexingErrorLocations_data(); void multiplexingErrorLocations(); void escapedValueDescriptions(); void factory(); }; void CoverageTest::frameLookup() { DbcDecoder decoder; RawCanFrame raw; raw.canId = 17U; DecodeDatabase empty; QVERIFY(decoder.FindFrame(empty, 17U, false) == nullptr); const DecodedFrameValue absent = decoder.Decode(empty, raw); QVERIFY(!absent.valid); QVERIFY(absent.definition == nullptr); QVERIFY(absent.signals.empty()); DecodeSignal signal; signal.length = 8U; DecodeDatabase database = SingleSignal(signal); QVERIFY(decoder.FindFrame(database, 18U, false) == nullptr); QVERIFY(decoder.FindFrame(database, 17U, true) == nullptr); raw.isExtended = true; QVERIFY(!decoder.Decode(database, raw).valid); raw.isExtended = false; raw.canId = 18U; QVERIFY(!decoder.Decode(database, raw).valid); raw.canId = 17U; DecodeFrame noSignals; noSignals.canId = 17U; database = DecodeDatabase({noSignals}); const DecodedFrameValue matched = decoder.Decode(database, raw); QVERIFY(matched.valid); QCOMPARE(matched.definition.get(), &database.GetFrames()[0]); QVERIFY(matched.signals.empty()); } void CoverageTest::signedBoundaries_data() { QTest::addColumn("order"); QTest::addColumn("length"); QTest::addColumn("expected"); for (int order = 0; order < 2; ++order) { for (unsigned length: {1U, 2U, 7U, 8U, 31U, 32U, 63U}) { const std::int64_t half = std::int64_t(1) << (length - 1U); const std::array values{{0, -1, -half, half - 1}}; for (unsigned index = 0; index < values.size(); ++index) { const QByteArray name = QByteArray::number(order) + "-" + QByteArray::number(length) + "-" + QByteArray::number(index); QTest::newRow(name.constData()) << order << length << qlonglong(values[index]); } } } } void CoverageTest::signedBoundaries() { QFETCH(int, order); QFETCH(unsigned, length); QFETCH(qlonglong, expected); DecodeSignal signal; signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola; signal.startBit = order == 0 ? 0U : 7U; signal.length = length; signal.valueType = ValueType::Signed; RawCanFrame raw; raw.canId = 17U; raw.data.assign(8U, 0U); const std::uint64_t bits = static_cast(expected); for (unsigned i = 0; i < length; ++i) { const unsigned weight = order == 0 ? i : length - 1U - i; const unsigned position = order == 0 ? i % 8U : 7U - i % 8U; raw.data[i / 8U] |= static_cast(((bits >> weight) & 1U) << position); } const DecodeDatabase database = SingleSignal(signal); const DecodedFrameValue result = DbcDecoder().Decode(database, raw); QVERIFY(result.signals[0].valid); QVERIFY(std::holds_alternative(result.signals[0].rawValue)); QCOMPARE(std::get(result.signals[0].rawValue), static_cast(expected)); } void CoverageTest::extraction_data() { QTest::addColumn("order"); QTest::addColumn("start"); QTest::addColumn("length"); for (int order = 0; order < 2; ++order) for (unsigned start = 0; start < 16U; ++start) for (unsigned length = 1; length <= 64U; ++length) { const QByteArray name = QByteArray::number(order) + "-" + QByteArray::number(start) + "-" + QByteArray::number(length); QTest::newRow(name.constData()) << order << start << length; } } void CoverageTest::extraction() { QFETCH(int, order); QFETCH(unsigned, start); QFETCH(unsigned, length); DecodeSignal signal; signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola; signal.startBit = start; signal.length = length; const DecodeDatabase database = SingleSignal(signal); RawCanFrame raw; raw.canId = 17U; const std::vector> patterns = { std::vector(10U, 0U), std::vector(10U, 255U), {0xA5, 0x3C, 0x81, 0x7E, 0x12, 0xD4, 0x69, 0xB0, 0x5A, 0xC3}}; for (const std::vector &data: patterns) { raw.data = data; const unsigned firstCoordinate = order == 0 ? start : (start / 8U) * 8U + 7U - start % 8U; raw.data.resize((firstCoordinate + length + 7U) / 8U); const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw); QVERIFY(decoded.signals[0].valid); QCOMPARE(std::get(decoded.signals[0].rawValue), ReferenceExtract(data, signal.byteOrder, start, length)); } } void CoverageTest::invalidSignalStatus_data() { QTest::addColumn("order"); QTest::addColumn("start"); QTest::addColumn("length"); QTest::addColumn("bytes"); QTest::addColumn("status"); for (int order = 0; order < 2; ++order) { const unsigned start = order == 0 ? 0U : 7U; const QByteArray prefix = QByteArray::number(order); QTest::newRow((prefix + "-zero").constData()) << order << start << 0U << 1U << int(SignalDecodeStatus::InvalidDefinition); QTest::newRow((prefix + "-65").constData()) << order << start << 65U << 9U << int(SignalDecodeStatus::InvalidDefinition); QTest::newRow((prefix + "-empty").constData()) << order << start << 8U << 0U << int(SignalDecodeStatus::InsufficientPayload); QTest::newRow((prefix + "-short").constData()) << order << start << 16U << 1U << int(SignalDecodeStatus::InsufficientPayload); QTest::newRow((prefix + "-large-start").constData()) << order << std::numeric_limits::max() << 8U << 1U << int(SignalDecodeStatus::InvalidDefinition); } } void CoverageTest::invalidSignalStatus() { QFETCH(int, order); QFETCH(unsigned, start); QFETCH(unsigned, length); QFETCH(unsigned, bytes); QFETCH(int, status); DecodeSignal signal; signal.startBit = start; signal.length = length; signal.byteOrder = order == 0 ? ByteOrder::Intel : ByteOrder::Motorola; if (status == int(SignalDecodeStatus::InvalidDefinition)) { QVERIFY_THROWS_EXCEPTION(std::invalid_argument, (void) SingleSignal(signal)); return; } const DecodeDatabase database = SingleSignal(signal); RawCanFrame raw; raw.canId = 17U; raw.data.assign(bytes, 255U); const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw); QVERIFY(decoded.valid); QVERIFY(!decoded.signals[0].valid); QCOMPARE(int(decoded.signals[0].status), status); QVERIFY(decoded.signals[0].GetValueDescription() == nullptr); } void CoverageTest::malformedRuntimeMultiplexing_data() { QTest::addColumn("kind"); QTest::newRow("two-selectors") << 0; QTest::newRow("signed-selector") << 1; QTest::newRow("65-bit-selector") << 2; QTest::newRow("branch-out-of-range") << 3; } void CoverageTest::malformedRuntimeMultiplexing() { QFETCH(int, kind); DecodeSignal selector; selector.length = 1U; selector.multiplexing.role = MultiplexRole::Selector; selector.name = "Selector"; std::vector definitions = SingleSignal(selector).GetFrames(); DecodeSignal branch; branch.name = "Branch"; branch.startBit = 8U; branch.length = 8U; branch.multiplexing.role = MultiplexRole::Branch; branch.multiplexing.value = 1U; definitions[0].signals.push_back(branch); DecodeSignal ordinary; ordinary.name = "Ordinary"; ordinary.startBit = 16U; ordinary.length = 8U; definitions[0].signals.push_back(ordinary); if (kind == 0) { selector.name = "SecondSelector"; definitions[0].signals.push_back(selector); } if (kind == 1) definitions[0].signals[0].valueType = ValueType::Signed; if (kind == 2) definitions[0].signals[0].length = 65U; if (kind == 3) definitions[0].signals[1].multiplexing.value = 2U; QVERIFY_THROWS_EXCEPTION(std::invalid_argument, (void) DecodeDatabase(definitions)); } void CoverageTest::pgn_data() { QTest::addColumn("id"); QTest::addColumn("expected"); QTest::addColumn("hasPgn"); QTest::newRow("standard") << 0x7FFU << 0U << false; for (unsigned dp: {0U, 1U}) for (unsigned pf: {239U, 240U}) for (unsigned ps: {0U, 0xABU}) { const unsigned id = 0x80000000U | (dp << 24U) | (pf << 16U) | (ps << 8U) | 0x42U; const unsigned expected = (dp << 16U) | (pf == 239U ? 0xEF00U : 0xF000U | ps); const QByteArray name = QByteArray::number(dp) + "-" + QByteArray::number(pf) + "-" + QByteArray::number(ps); QTest::newRow(name.constData()) << id << expected << true; } } void CoverageTest::pgn() { QFETCH(unsigned, id); QFETCH(unsigned, expected); QFETCH(bool, hasPgn); const DbcDatabase parsed = ParseText("BO_ " + std::to_string(id) + " Frame: 8 ECU\n"); QCOMPARE(parsed.frames[0].hasPgn, hasPgn); QCOMPARE(parsed.frames[0].pgn, expected); } void CoverageTest::comments_data() { QTest::addColumn("before"); QTest::addColumn("after"); QTest::addColumn("frameComment"); QTest::addColumn("signalComment"); QTest::newRow("tabs") << QString::fromUtf8("") << QString::fromUtf8("CM_\tBO_ 1 \"Frame\";\nCM_\tSG_ 1 State \"Signal\";\n") << QString::fromUtf8("Frame") << QString::fromUtf8("Signal"); 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(""); 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"); 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(""); // Current documented limitation: comments resolve only already parsed targets. QTest::newRow("before-definitions") << QString::fromUtf8("CM_ BO_ 1 \"Early\";\nCM_ SG_ 1 State \"Early\";\n") << QString::fromUtf8("") << QString::fromUtf8("") << QString::fromUtf8(""); QTest::newRow("missing-quotes") << QString::fromUtf8("") << QString::fromUtf8("CM_ BO_ 1 Missing;\nCM_ SG_ 1 State \"Unclosed;\n") << QString::fromUtf8("") << QString::fromUtf8(""); } void CoverageTest::comments() { QFETCH(QString, before); QFETCH(QString, after); QFETCH(QString, frameComment); QFETCH(QString, signalComment); const DbcDatabase parsed = ParseText(before.toStdString() + "BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" + after.toStdString()); QCOMPARE(parsed.frames[0].comment, frameComment.toStdString()); QCOMPARE(parsed.frames[0].signals[0].comment, signalComment.toStdString()); } void CoverageTest::ignoredCommentStatements_data() { QTest::addColumn("statement"); QTest::newRow("empty") << QString(); QTest::newRow("wrong-keyword") << QStringLiteral("OTHER_ BO_ 1 \"Ignored\";"); QTest::newRow("keyword-prefix") << QStringLiteral("CM_OTHER BO_ 1 \"Ignored\";"); QTest::newRow("frame-without-cm") << QStringLiteral("BO_ 1 \"Ignored\";"); QTest::newRow("missing-subtype") << QStringLiteral("CM_"); QTest::newRow("global-comment") << QStringLiteral("CM_ \"Ignored\";"); QTest::newRow("node-comment") << QStringLiteral("CM_ BU_ ECU \"Ignored\";"); QTest::newRow("environment-comment") << QStringLiteral("CM_ EV_ State \"Ignored\";"); QTest::newRow("unknown-subtype") << QStringLiteral("CM_ OTHER_ 1 \"Ignored\";"); } void CoverageTest::ignoredCommentStatements() { QFETCH(QString, statement); DbcDatabase database; FrameInfo frame; frame.canId = 1U; frame.name = "Frame"; frame.comment = "Existing frame comment"; SignalInfo signal; signal.name = "State"; signal.comment = "Existing signal comment"; frame.signals.push_back(signal); database.frames.push_back(frame); DbcParseContext context{database, &database.frames[0]}; // The handler is public: callers may bypass keyword dispatch. Ignored input // must preserve existing metadata, including comments already assigned. DbcCommentHandler().Handle(statement.toStdString(), context); QCOMPARE(database.frames.size(), std::size_t(1)); QCOMPARE(database.frames[0].canId, 1U); QCOMPARE(database.frames[0].name, frame.name); QCOMPARE(database.frames[0].comment, frame.comment); QCOMPARE(database.frames[0].signals.size(), std::size_t(1)); QCOMPARE(database.frames[0].signals[0].name, signal.name); QCOMPARE(database.frames[0].signals[0].comment, signal.comment); QCOMPARE(context.currentFrame, &database.frames[0]); } void CoverageTest::ambiguousValueReferences_data() { QTest::addColumn("duplicateFrames"); QTest::addColumn("extended"); QTest::newRow("duplicate-standard-frames") << true << false; QTest::newRow("duplicate-extended-frames") << true << true; QTest::newRow("duplicate-signals-in-standard-frame") << false << false; QTest::newRow("duplicate-signals-in-extended-frame") << false << true; } void CoverageTest::ambiguousValueReferences() { QFETCH(bool, duplicateFrames); QFETCH(bool, extended); DbcDatabase database; FrameInfo frame; frame.canId = 1U; frame.name = "Frame"; frame.isExtended = extended; SignalInfo signal; signal.name = "State"; frame.signals.push_back(signal); if (!duplicateFrames) frame.signals.push_back(signal); database.frames.push_back(frame); if (duplicateFrames) database.frames.push_back(frame); // ParseFile rejects duplicate definitions earlier. Construct the database // through the public metadata API to exercise the resolver's own checks. DbcParseContext context{database, nullptr}; context.lineNumber = 37U; const std::string rawId = extended ? "2147483649" : "1"; DbcValueDescriptionHandler().Handle("VAL_ " + rawId + " State 0 \"Off\";", context); QCOMPARE(context.pendingValues.size(), std::size_t(1)); QCOMPARE(context.pendingValues[0].lineNumber, std::size_t(37)); const std::string path = "manual database.dbc"; try { context.ResolveValueDescriptions(path); QFAIL("The resolver must reject an ambiguous target"); } catch (const std::runtime_error &error) { const std::string message = duplicateFrames ? "Ambiguous VAL_ frame reference." : "Ambiguous VAL_ signal reference: State"; QCOMPARE(std::string(error.what()), path + ":37: " + message); } // Ambiguity must be detected before applying any descriptions. for (const FrameInfo &candidate: database.frames) for (const SignalInfo &candidateSignal: candidate.signals) QVERIFY(candidateSignal.valueDescriptions.empty()); } void CoverageTest::valueReferenceScope() { DbcDatabase database; FrameInfo frame; frame.canId = 1U; SignalInfo signal; signal.name = "State"; frame.signals.push_back(signal); database.frames.push_back(frame); frame.isExtended = true; database.frames.push_back(frame); frame.canId = 2U; frame.isExtended = false; database.frames.push_back(frame); DbcParseContext context{database, nullptr}; const DbcValueDescriptionHandler handler; context.lineNumber = 3U; handler.Handle("VAL_ 1 State 0 \"Standard\";", context); context.lineNumber = 7U; handler.Handle("VAL_ 2147483649 State 0 \"Extended\";", context); context.ResolveValueDescriptions("manual database.dbc"); const std::string *standard = FindValueDescription(database.frames[0].signals[0].valueDescriptions, std::uint64_t(0)); const std::string *extended = FindValueDescription(database.frames[1].signals[0].valueDescriptions, std::uint64_t(0)); QVERIFY(standard != nullptr); QVERIFY(extended != nullptr); QCOMPARE(*standard, std::string("Standard")); QCOMPARE(*extended, std::string("Extended")); QVERIFY(database.frames[2].signals[0].valueDescriptions.empty()); } void CoverageTest::fileFormatting_data() { QTest::addColumn("text"); QTest::addColumn("frames"); QTest::newRow("empty") << QString::fromUtf8("") << 0U; QTest::newRow("whitespace") << QString::fromUtf8(" \t\r\n\n\t") << 0U; QTest::newRow("unknown-only") << QString::fromUtf8("VERSION \"test\"\nBU_: ECU\nUNKNOWN_ data\n") << 0U; QTest::newRow("no-final-newline") << QString::fromUtf8("BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU") << 1U; 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; } void CoverageTest::fileFormatting() { QFETCH(QString, text); QFETCH(unsigned, frames); const DbcDatabase parsed = ParseText(text.toStdString()); QCOMPARE(parsed.frames.size(), std::size_t(frames)); if (frames) { QCOMPARE(parsed.frames[0].signals.size(), std::size_t(1)); if (text.contains("\r\nBO_")) { QCOMPARE(parsed.frames[0].comment, std::string("Comment")); const std::string *description = FindValueDescription(parsed.frames[0].signals[0].valueDescriptions, std::uint64_t(0)); QVERIFY(description != nullptr); QCOMPARE(*description, std::string("Off")); } } } void CoverageTest::missingFile() { QTemporaryFile file; QVERIFY(file.open()); const QString path = file.fileName(); QVERIFY(file.remove()); try { DbcParser().ParseFile(path.toStdString()); QFAIL("Missing file must throw"); } catch (const std::runtime_error &error) { QCOMPARE(std::string(error.what()), "Failed to open DBC file: " + path.toStdString()); } } void CoverageTest::builders() { const DecodeDatabase emptyRuntime = DbcDecodeBuilder().Build(DbcDatabase()); QVERIFY(emptyRuntime.GetFrames().empty()); QVERIFY(!emptyRuntime.FindFrame(FrameKey(0U, false))); const std::unique_ptr emptyTree = DbcTreeBuilder().Build(DbcDatabase()); QCOMPARE(emptyTree->GetType(), NodeType::Root); QCOMPARE(emptyTree->GetName(), std::string("dbc")); QCOMPARE(emptyTree->GetChildCount(), std::size_t(0)); QVERIFY(emptyTree->GetFrame() == nullptr && emptyTree->GetSignal() == nullptr); const DbcDatabase expected = ParseText( "BO_ 256 Standard: 3 ECU1\n" "SG_ Mode M : 0|8@1+ (2,3) [0|513] \"mode\" ECU1,ECU2\n" "SG_ Branch m1 : 15|8@0- (-2,5) [-249|261] \"rpm\" ECU2,ECU3\n" "SG_ Always : 16|8@1+ (0.25,-1) [-1|62.75] \"V\" ECU3\n" "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Branch \"Branch comment\";\n" "VAL_ 256 Branch -1 \"Negative\" 1 \"Positive\";\n" "BO_ 2163212866 Extended: 8 ECU4\n"); DecodeDatabase runtime; std::unique_ptr tree; { DbcDatabase source = expected; runtime = DbcDecodeBuilder().Build(source); tree = DbcTreeBuilder().Build(source); source.frames[0].name = "Changed"; source.frames[0].signals[1].receivers.clear(); source.frames[0].signals[1].valueDescriptions.clear(); source.frames.clear(); } QCOMPARE(runtime.GetFrames().size(), expected.frames.size()); QCOMPARE(tree->GetChildCount(), expected.frames.size()); for (std::size_t i = 0; i < expected.frames.size(); ++i) { const FrameInfo &frame = expected.frames[i]; const DecodeFrame &decodedFrame = runtime.GetFrames()[i]; const TreeNode *node = tree->GetChild(i); QVERIFY(node && node->GetFrame()); QCOMPARE(node->GetType(), NodeType::Frame); QCOMPARE(node->GetName(), frame.name); QVERIFY(node->GetSignal() == nullptr); QCOMPARE(runtime.FindFrame(FrameKey(frame.canId, frame.isExtended)).get(), &decodedFrame); const FrameInfo &copied = *node->GetFrame(); QCOMPARE(copied.name, frame.name); QCOMPARE(decodedFrame.name, frame.name); QCOMPARE(copied.canId, frame.canId); QCOMPARE(decodedFrame.canId, frame.canId); QCOMPARE(copied.isExtended, frame.isExtended); QCOMPARE(decodedFrame.isExtended, frame.isExtended); QCOMPARE(copied.dlc, frame.dlc); QCOMPARE(decodedFrame.dlc, frame.dlc); QCOMPARE(copied.pgn, frame.pgn); QCOMPARE(decodedFrame.pgn, frame.pgn); QCOMPARE(copied.hasPgn, frame.hasPgn); QCOMPARE(decodedFrame.hasPgn, frame.hasPgn); QCOMPARE(copied.transmitter, frame.transmitter); QCOMPARE(decodedFrame.transmitter, frame.transmitter); QCOMPARE(copied.comment, frame.comment); QCOMPARE(decodedFrame.comment, frame.comment); QCOMPARE(node->GetChildCount(), frame.signals.size()); QCOMPARE(copied.signals.size(), frame.signals.size()); QCOMPARE(decodedFrame.signals.size(), frame.signals.size()); for (std::size_t j = 0; j < frame.signals.size(); ++j) { const SignalInfo &original = frame.signals[j]; const DecodeSignal &target = decodedFrame.signals[j]; const TreeNode *leaf = node->GetChild(j); QVERIFY(leaf && leaf->GetSignal()); QCOMPARE(leaf->GetType(), NodeType::Signal); QCOMPARE(leaf->GetName(), original.name); QVERIFY(leaf->GetFrame() == nullptr); // Check both the frame payload copy and each child payload copy. for (const SignalInfo *copy: {&copied.signals[j], leaf->GetSignal()}) { QCOMPARE(copy->name, original.name); QCOMPARE(copy->startBit, original.startBit); QCOMPARE(copy->length, original.length); QCOMPARE(copy->isLittleEndian, original.isLittleEndian); QCOMPARE(copy->isSigned, original.isSigned); QCOMPARE(copy->factor, original.factor); QCOMPARE(copy->offset, original.offset); QCOMPARE(copy->minimum, original.minimum); QCOMPARE(copy->maximum, original.maximum); QCOMPARE(copy->unit, original.unit); QCOMPARE(copy->comment, original.comment); QVERIFY(copy->receivers == original.receivers); QVERIFY(copy->valueDescriptions == original.valueDescriptions); QCOMPARE(copy->multiplexing.role, original.multiplexing.role); QCOMPARE(copy->multiplexing.value, original.multiplexing.value); } QCOMPARE(target.name, original.name); QCOMPARE(target.startBit, original.startBit); QCOMPARE(target.length, original.length); QCOMPARE(target.byteOrder, original.isLittleEndian ? ByteOrder::Intel : ByteOrder::Motorola); QCOMPARE(target.valueType, original.isSigned ? ValueType::Signed : ValueType::Unsigned); QCOMPARE(target.factor, original.factor); QCOMPARE(target.offset, original.offset); QCOMPARE(target.minimum, original.minimum); QCOMPARE(target.maximum, original.maximum); QCOMPARE(target.unit, original.unit); QCOMPARE(target.comment, original.comment); QVERIFY(target.receivers == original.receivers); QVERIFY(target.valueDescriptions == original.valueDescriptions); QCOMPARE(target.multiplexing.role, original.multiplexing.role); QCOMPARE(target.multiplexing.value, original.multiplexing.value); } } } void CoverageTest::physicalConversion_data() { QTest::addColumn("factor"); QTest::addColumn("offset"); QTest::addColumn("expected"); QTest::newRow("zero-factor") << 0.0 << 7.0 << 7.0; QTest::newRow("negative-factor") << -2.0 << 5.0 << -15.0; QTest::newRow("above-declared-range") << 2.0 << 3.0 << 23.0; } void CoverageTest::physicalConversion() { QFETCH(double, factor); QFETCH(double, offset); QFETCH(double, expected); DecodeSignal signal; signal.length = 8U; signal.factor = factor; signal.offset = offset; signal.minimum = 0.0; signal.maximum = 1.0; const DecodeDatabase database = SingleSignal(signal); RawCanFrame raw; raw.canId = 17U; raw.data = {10U}; const DecodedFrameValue decoded = DbcDecoder().Decode(database, raw); QVERIFY(decoded.signals[0].valid); QCOMPARE(std::get(decoded.signals[0].rawValue), std::uint64_t(10)); QCOMPARE(decoded.signals[0].physicalValue, expected); } void CoverageTest::multiplexingScenarios() { const DecodeDatabase selectorOnly = DbcDecodeBuilder().Build(ParseText( "BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n")); RawCanFrame raw; raw.canId = 1U; raw.data = {1U}; const DecodedFrameValue only = DbcDecoder().Decode(selectorOnly, raw); QCOMPARE(only.signals.size(), std::size_t(1)); QVERIFY(only.signals[0].valid); const DecodeDatabase database = DbcDecodeBuilder().Build(ParseText( "BO_ 1 First: 3 ECU\n" "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n" "SG_ A m0 : 8|8@1+ (1,0) [0|255] \"\" ECU\n" "SG_ B m0 : 16|8@1+ (1,0) [0|255] \"\" ECU\n" "BO_ 2 Second: 2 ECU\n" "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n" "SG_ A m1 : 8|8@1+ (1,0) [0|255] \"\" ECU\n")); raw.data = {0U, 42U, 99U}; const DecodedFrameValue first = DbcDecoder().Decode(database, raw); QVERIFY(first.signals[1].valid && first.signals[2].valid); QCOMPARE(std::get(first.signals[1].rawValue), std::uint64_t(42)); QCOMPARE(std::get(first.signals[2].rawValue), std::uint64_t(99)); raw.canId = 2U; const DecodedFrameValue secondInactive = DbcDecoder().Decode(database, raw); QCOMPARE(secondInactive.signals[1].status, SignalDecodeStatus::Inactive); raw.data[0] = 1U; QVERIFY(DbcDecoder().Decode(database, raw).signals[1].valid); raw.canId = 1U; const DecodedFrameValue firstInactive = DbcDecoder().Decode(database, raw); QCOMPARE(firstInactive.signals[1].status, SignalDecodeStatus::Inactive); QCOMPARE(firstInactive.signals[2].status, SignalDecodeStatus::Inactive); } void CoverageTest::multiplexingErrorLocations_data() { QTest::addColumn("text"); QTest::addColumn("line"); QTest::addColumn("message"); const QString header = "BO_ 1 Frame: 3 ECU\n\n"; const QString selector = "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"; const QString branch = "SG_ A m1 : 8|8@1+ (1,0) [0|255] \"\" ECU\n"; QTest::newRow("missing-selector") << header + branch << 3U << QString::fromUtf8("Multiplexed branch has no selector."); QTest::newRow("second-selector") << header + selector + "\nSG_ Other M : 16|1@1+ (1,0) [0|1] \"\" ECU\n" << 5U << QString::fromUtf8("Multiplexing requires one unsigned selector per frame."); QTest::newRow("signed-selector") << header + "SG_ Mode M : 0|1@1- (1,0) [-1|0] \"\" ECU\n" << 3U << QString::fromUtf8("Multiplexing requires one unsigned selector per frame."); 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."); QTest::newRow("same-branch-overlap") << header + selector + branch + "SG_ B m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n" << 5U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); QTest::newRow("selector-overlap") << header + selector + "SG_ A m1 : 0|1@1+ (1,0) [0|1] \"\" ECU\n" << 4U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); QTest::newRow("unconditional-overlap") << header + selector + branch + "SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n" << 5U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); 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" << 4U << QString::fromUtf8("Signals overlap without mutually exclusive multiplexing conditions."); QTest::newRow("extended-multiplexing") << header + "SG_MUL_VAL_ 1 A Mode 1-2;\n" << 3U << QString::fromUtf8("Extended and nested multiplexing are unsupported."); QTest::newRow("nested-marker") << header + "SG_ A m1M : 8|8@1+ (1,0) [0|255] \"\" ECU\n" << 3U << QString::fromUtf8("Invalid or unsupported multiplexing marker."); } void CoverageTest::multiplexingErrorLocations() { QFETCH(QString, text); QFETCH(unsigned, line); QFETCH(QString, message); QTemporaryFile file; QVERIFY(file.open()); const QByteArray content = text.toUtf8(); QCOMPARE(file.write(content), qint64(content.size())); QVERIFY(file.flush()); try { DbcParser().ParseFile(file.fileName().toStdString()); QFAIL("Expected multiplexing rejection"); } catch (const std::runtime_error &error) { QCOMPARE(QString::fromStdString(error.what()), file.fileName() + ":" + QString::number(line) + ": " + message); } } void CoverageTest::escapedValueDescriptions() { const DbcDatabase parsed = ParseText( "BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" R"dbc(VAL_ 1 State -0 "Ноль" 1 "C:\\trace" 2 "\\\"quoted\"\\";)dbc" "\n"); const ValueDescriptions &values = parsed.frames[0].signals[0].valueDescriptions; QCOMPARE(values.size(), std::size_t(3)); QVERIFY(values.find(RawSignalValue(std::int64_t(0))) == values.end()); const std::string *zero = FindValueDescription(values, std::int64_t(0)); const std::string *path = FindValueDescription(values, std::uint64_t(1)); const std::string *quoted = FindValueDescription(values, std::uint64_t(2)); QVERIFY(zero && path && quoted); QCOMPARE(*zero, std::string("Ноль")); QCOMPARE(*path, std::string("C:\\trace")); QCOMPARE(*quoted, std::string("\\\"quoted\"\\")); QVERIFY(NormalizeValueKey(RawSignalValue(std::int64_t(0))) == RawSignalValue(std::uint64_t(0))); QVERIFY(NormalizeValueKey(RawSignalValue(std::int64_t(-1))) == RawSignalValue(std::int64_t(-1))); } void CoverageTest::factory() { TypeFactory> factory; QVERIFY(!factory.contains("value")); QVERIFY_THROWS_EXCEPTION(std::out_of_range, static_cast(factory.creator("missing"))); QVERIFY_THROWS_EXCEPTION(std::out_of_range, static_cast(factory.create("missing", std::make_unique(1)))); factory.registerType("value"); QVERIFY(factory.contains("value")); std::unique_ptr argument = std::make_unique(42); const std::shared_ptr first = factory.create("value", std::move(argument)); QVERIFY(argument == nullptr); QCOMPARE(first->Value(), 42); const TypeFactory>::CreatorFn creator = factory.creator("value"); QCOMPARE(creator(std::make_unique(7))->Value(), 7); factory.registerType("value"); QCOMPARE(factory.create("value", std::make_unique(42))->Value(), 43); QCOMPARE(first->Value(), 42); QCOMPARE(creator(std::make_unique(7))->Value(), 7); } int RunCoverageTests(int argc, char **argv) { CoverageTest tests; return QTest::qExec(&tests, argc, argv); } #include "dbc_coverage_test.moc"