diff --git a/core/dbc_statement_handler.cpp b/core/dbc_statement_handler.cpp index d4a4ab5..8a44792 100644 --- a/core/dbc_statement_handler.cpp +++ b/core/dbc_statement_handler.cpp @@ -368,8 +368,7 @@ void DbcSignalHandler::Handle(const std::string &line, DbcParseContext &context) } void DbcCommentHandler::Handle(const std::string &line, DbcParseContext &context) const { - if (line.compare(0U, 4U, "CM_ ") == 0) - ParseCommentLine(line, context.database); + ParseCommentLine(line, context.database); } DbcStatementDispatcher::DbcStatementDispatcher() { diff --git a/tests/dbc_coverage_test.cpp b/tests/dbc_coverage_test.cpp new file mode 100644 index 0000000..2dcb689 --- /dev/null +++ b/tests/dbc_coverage_test.cpp @@ -0,0 +1,544 @@ +// Additional regression coverage. Kept separate for review before the first run. +#include "dbc_parser.h" +#include "dbc_decode_builder.h" +#include "dbc_decoder.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) { + DecodeDatabase database; + DecodeFrame frame; + frame.canId = 17U; + frame.signals.push_back(signal); + database.frames.push_back(frame); + database.frameIndexByKey.emplace(FrameKey(17U, false), 0U); + return database; +} + +// 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; } +}; +} + +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 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 auto absent = decoder.Decode(empty, raw); + QVERIFY(!absent.valid); + QVERIFY(absent.definition == nullptr); + QVERIFY(absent.signals.empty()); + DecodeSignal signal; + auto 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; + database.frameIndexByKey.at(FrameKey(17U, false)) = database.frames.size(); + QVERIFY(decoder.FindFrame(database, 17U, false) == nullptr); + QVERIFY(!decoder.Decode(database, raw).valid); + database.frameIndexByKey.at(FrameKey(17U, false)) = 0U; + database.frames[0].signals.clear(); + const auto matched = decoder.Decode(database, raw); + QVERIFY(matched.valid); + QCOMPARE(matched.definition, &database.frames[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 auto 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 auto database = SingleSignal(signal); + const auto 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 auto 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 auto 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 auto &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 auto 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 auto 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::InsufficientPayload); + } +} + +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; + const auto database = SingleSignal(signal); + RawCanFrame raw; raw.canId = 17U; raw.data.assign(bytes, 255U); + const auto 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; + auto database = SingleSignal(selector); + DecodeSignal branch; branch.startBit = 8U; branch.length = 8U; + branch.multiplexing.role = MultiplexRole::Branch; branch.multiplexing.value = 1U; + database.frames[0].signals.push_back(branch); + DecodeSignal ordinary; ordinary.startBit = 16U; ordinary.length = 8U; + database.frames[0].signals.push_back(ordinary); + if (kind == 0) database.frames[0].signals.push_back(selector); + if (kind == 1) database.frames[0].signals[0].valueType = ValueType::Signed; + if (kind == 2) database.frames[0].signals[0].length = 65U; + if (kind == 3) database.frames[0].signals[1].multiplexing.value = 2U; + RawCanFrame raw; raw.canId = 17U; raw.data = {1U, 42U, 99U}; + const auto result = DbcDecoder().Decode(database, raw); + QCOMPARE(int(result.signals[1].status), int(SignalDecodeStatus::InvalidDefinition)); + QVERIFY(!result.signals[1].valid); + QVERIFY(result.signals[2].valid); + QCOMPARE(std::get(result.signals[2].rawValue), std::uint64_t(99)); + if (kind != 3) { + QCOMPARE(int(result.signals[0].status), int(SignalDecodeStatus::InvalidDefinition)); + if (kind == 0) QCOMPARE(int(result.signals[3].status), int(SignalDecodeStatus::InvalidDefinition)); + } +} + +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 auto 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 auto 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 auto 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::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 auto 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 auto *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 auto 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 auto emptyRuntime = DbcDecodeBuilder().Build(DbcDatabase()); + QVERIFY(emptyRuntime.frames.empty()); + QVERIFY(emptyRuntime.frameIndexByKey.empty()); + const auto 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 auto 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.frames.size(), expected.frames.size()); + QCOMPARE(tree->GetChildCount(), expected.frames.size()); + QCOMPARE(runtime.frameIndexByKey.size(), expected.frames.size()); + for (std::size_t i = 0; i < expected.frames.size(); ++i) { + const FrameInfo &frame = expected.frames[i]; + const DecodeFrame &decodedFrame = runtime.frames[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.frameIndexByKey.at(FrameKey(frame.canId, frame.isExtended)), i); + 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 auto &original = frame.signals[j]; + const auto &target = decodedFrame.signals[j]; + const auto *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 auto database = SingleSignal(signal); + RawCanFrame raw; raw.canId = 17U; raw.data = {10U}; + const auto 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 auto 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 auto only = DbcDecoder().Decode(selectorOnly, raw); + QCOMPARE(only.signals.size(), std::size_t(1)); + QVERIFY(only.signals[0].valid); + const auto 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 auto 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 auto 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 auto 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 auto 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 auto &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 auto *zero = FindValueDescription(values, std::int64_t(0)); + const auto *path = FindValueDescription(values, std::uint64_t(1)); + const auto *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")); + auto argument = std::make_unique(42); + const auto first = factory.create("value", std::move(argument)); + QVERIFY(argument == nullptr); + QCOMPARE(first->Value(), 42); + const auto 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" diff --git a/tests/dbc_tree_model_test.cpp b/tests/dbc_tree_model_test.cpp index 068f515..37794b8 100644 --- a/tests/dbc_tree_model_test.cpp +++ b/tests/dbc_tree_model_test.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -22,6 +23,76 @@ static std::unique_ptr ParsedTree() { class ModelTest : public QObject { Q_OBJECT private Q_SLOTS: + void additionalMetadata() { + DbcDatabase database; + FrameInfo frame; + frame.name = "Extended"; + frame.canId = 0x18F00442U; + frame.isExtended = true; + frame.dlc = 8U; + frame.comment = "Frame tooltip"; + SignalInfo signal; + signal.name = "SignedMotorola"; + signal.startBit = 7U; + signal.length = 16U; + signal.isLittleEndian = false; + signal.isSigned = true; + signal.unit = "rpm"; + signal.comment = "Signal tooltip"; + frame.signals.push_back(signal); + database.frames.push_back(frame); + DbcTreeModel model; + model.SetTree(DbcTreeBuilder().Build(database)); + const QModelIndex parent = model.index(0, 0); + QCOMPARE(model.data(model.index(0, DbcTreeModel::ByteOrder, parent)).toString(), QString("Motorola")); + QCOMPARE(model.data(model.index(0, DbcTreeModel::Signed, parent)).toString(), QString("Signed")); + QCOMPARE(model.data(model.index(0, DbcTreeModel::CanId)).toString(), QString("0X18F00442")); + const QStringList headers = {"Name", "CAN ID", "Payload (bytes)", "Start bit", + "Length (bits)", "Byte order", "Signedness", "Unit", "Comment"}; + for (int column = 0; column < model.columnCount(); ++column) { + const auto frameIndex = model.index(0, column); + const auto signalIndex = model.index(0, column, parent); + QCOMPARE(model.headerData(column, Qt::Horizontal).toString(), headers.at(column)); + QVERIFY(!model.headerData(column, Qt::Horizontal, Qt::ToolTipRole).isValid()); + QCOMPARE(model.data(frameIndex, Qt::ToolTipRole).toString(), QString("Frame tooltip")); + QCOMPARE(model.data(signalIndex, Qt::ToolTipRole).toString(), QString("Signal tooltip")); + QCOMPARE(model.data(frameIndex, DbcTreeModel::CommentRole).toString(), QString("Frame tooltip")); + QCOMPARE(model.data(signalIndex, DbcTreeModel::CommentRole).toString(), QString("Signal tooltip")); + QCOMPARE(model.data(frameIndex, DbcTreeModel::CanIdRole).toUInt(), frame.canId); + QCOMPARE(model.data(frameIndex, DbcTreeModel::PayloadLengthRole).toInt(), 8); + QCOMPARE(model.data(frameIndex, DbcTreeModel::ExtendedRole).toBool(), true); + QCOMPARE(model.data(signalIndex, DbcTreeModel::StartBitRole).toUInt(), 7U); + QCOMPARE(model.data(signalIndex, DbcTreeModel::BitLengthRole).toUInt(), 16U); + QCOMPARE(model.data(signalIndex, DbcTreeModel::LittleEndianRole).toBool(), false); + QCOMPARE(model.data(signalIndex, DbcTreeModel::SignedRole).toBool(), true); + QCOMPARE(model.data(signalIndex, DbcTreeModel::UnitRole).toString(), QString("rpm")); + const auto descriptions = model.data(signalIndex, DbcTreeModel::ValueDescriptionsRole); + QVERIFY(descriptions.isValid()); + QVERIFY(descriptions.toList().isEmpty()); + QVERIFY(!model.data(frameIndex, Qt::UserRole + 1000).isValid()); + QVERIFY(!model.data(signalIndex, Qt::UserRole + 1000).isValid()); + QVERIFY(!model.setData(frameIndex, "Changed", Qt::EditRole)); + QVERIFY(!model.setData(signalIndex, "Changed", Qt::EditRole)); + } + QCOMPARE(model.data(parent).toString(), QString("Extended")); + QCOMPARE(model.data(model.index(0, 0, parent)).toString(), QString("SignedMotorola")); + } + void emptyRootAndRejectedReplacement() { + DbcTreeModel model; + QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::QtTest); + model.SetTree(ParsedTree()); + QPersistentModelIndex retained(model.index(0, 0)); + QSignalSpy resets(&model, &QAbstractItemModel::modelReset); + QVERIFY_THROWS_EXCEPTION(std::invalid_argument, model.SetTree(std::make_unique(SignalInfo()))); + QCOMPARE(resets.count(), 0); + QVERIFY(retained.isValid()); + QCOMPARE(model.rowCount(), 2); + model.SetTree(DbcTreeBuilder().Build(DbcDatabase())); + QCOMPARE(resets.count(), 1); + QVERIFY(!retained.isValid()); + QCOMPARE(model.rowCount(), 0); + QVERIFY(!model.index(0, 0).isValid()); + } void navigation() { DbcTreeModel model; QAbstractItemModelTester tester(&model, QAbstractItemModelTester::FailureReportingMode::QtTest); diff --git a/tests/main.cpp b/tests/main.cpp index b5a388a..b93038d 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -3,10 +3,12 @@ int RunParserTests(int argc, char **argv); int RunDecoderTests(int argc, char **argv); +int RunCoverageTests(int argc, char **argv); int main(int argc, char **argv) { QCoreApplication application(argc, argv); const int parserResult = RunParserTests(argc, argv); const int decoderResult = RunDecoderTests(argc, argv); - return parserResult | decoderResult; + const int coverageResult = RunCoverageTests(argc, argv); + return parserResult | decoderResult | coverageResult; } diff --git a/tests/tests.pro b/tests/tests.pro index f5d428d..13abad9 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -7,5 +7,5 @@ CONFIG -= app_bundle include(../core/core.pri) include(parser_tests.pri) include(decoder_tests.pri) -SOURCES += main.cpp +SOURCES += main.cpp dbc_coverage_test.cpp CONFIG -= debug_and_release debug_and_release_target