This commit is contained in:
deeaitch committed 2026-10-14 09:38:14 -04:00
1 parent 6d6ace4315
commit 0b05152bcf
7 files changed
+429 -5

No files matched your search

+17 -3
View File
@@ -24,7 +24,7 @@ make -j
make check
```
Or run `./tests/dbc_tests` directly. Parser, decoder, lifetime, validation, and additional coverage
Or run `./tests/dbc_tests` directly. Parser, decoder, pipeline, lifetime, validation, and additional coverage
suites run, and the executable returns nonzero when any suite fails. Decoder cases appear as
individual QtTest data rows; parser checks are grouped into named test slots.
Checks also run in release builds.
@@ -41,8 +41,8 @@ make check
```
`parser_tests.pri` and `decoder_tests.pri` include the original test sources;
`tests.pro` also includes `dbc_coverage_test.cpp`, `dbc_lifetime_test.cpp`, and
`dbc_validation_test.cpp`.
`tests.pro` also includes `dbc_coverage_test.cpp`, `dbc_lifetime_test.cpp`,
`dbc_validation_test.cpp`, and `dbc_pipeline_test.cpp`.
`../core/core.pri` supplies the library include path and linkage.
## Decoder coverage
@@ -167,3 +167,17 @@ all lengths 1–64, for both byte orders. Zero/65-bit lengths and uint32 maximum
inputs are rejected. Builder snapshots survive input mutation and destruction;
failed replacement with duplicate frame or signal keys preserves the existing
frame and signal lookups.
## End-to-end pipeline coverage
`dbc_pipeline_test.cpp` parses temporary DBC files and checks independent expected
metadata against the parser, runtime builder, and tree builder. It exercises
standard and extended frames with the same numeric ID and signal name, Intel
and signed Motorola decoding, scaling, raw value descriptions, basic multiplexing,
PGN metadata, receivers, units, ranges, and frame/signal comments. Tree checks
include node types, payloads, parents, rows, names, and child counts.
Empty and headers-only fixtures produce an empty runtime database and a valid
empty tree root. Replacement fixtures change definitions, metadata and decoded
values, remove old lookup keys, and then clear both outputs through an empty
fixture. Saved decoded results and trees retain their original metadata.
+391
View File
@@ -0,0 +1,391 @@
#include "dbc_decode_builder.h"
#include "dbc_decoder.h"
#include "dbc_parser.h"
#include "dbc_tree_builder.h"
#include <QTemporaryFile>
#include <QtTest>
#include <stdexcept>
#include <utility>
namespace {
const std::string fixture = R"DBC(BO_ 291 Standard: 2 SenderA
SG_ Mode M : 0|8@1+ (1,0) [0|255] "" ReceiverA
SG_ State m3 : 8|8@1+ (2,1) [1|511] "km/h" ReceiverA,ReceiverB
BO_ 2147483939 Extended: 1 SenderB
SG_ State : 7|8@0- (0.5,-3) [-67|60.5] "deg C" ReceiverB
BO_ 2565866562 J1939: 0 SenderC
CM_ BO_ 291 "Standard frame comment";
CM_ SG_ 291 Mode "Selector comment";
CM_ SG_ 291 State "Standard signal comment";
CM_ BO_ 2147483939 "Extended frame comment";
CM_ SG_ 2147483939 State "Extended signal comment";
VAL_ 291 Mode 3 "Selected";
VAL_ 291 State 4 "Standard four";
VAL_ 2147483939 State -2 "Extended minus two";
)DBC";
DbcParseResult ParseFixture(const std::string &text) {
QTemporaryFile file;
if (!file.open()) throw std::runtime_error("Cannot create DBC fixture");
if (file.write(text.data(), static_cast<qint64>(text.size())) != static_cast<qint64>(text.size()) || !file.flush())
throw std::runtime_error("Cannot write DBC fixture");
return DbcParser().ParseFileWithDiagnostics(file.fileName().toStdString());
}
// Independent expectations ensure a parser regression cannot be hidden by
// comparing builders only with the same incorrect parsed input.
DbcDatabase ExpectedMetadata() {
FrameInfo standard;
standard.name = "Standard";
standard.canId = 291U;
standard.dlc = 2U;
standard.transmitter = "SenderA";
standard.comment = "Standard frame comment";
SignalInfo mode;
mode.name = "Mode";
mode.multiplexing.role = MultiplexRole::Selector;
mode.length = 8U;
mode.maximum = 255;
mode.receivers = {"ReceiverA"};
mode.comment = "Selector comment";
mode.valueDescriptions = {{std::uint64_t(3), "Selected"}};
SignalInfo state;
state.name = "State";
state.multiplexing.role = MultiplexRole::Branch;
state.multiplexing.value = 3U;
state.startBit = 8U;
state.length = 8U;
state.factor = 2;
state.offset = 1;
state.minimum = 1;
state.maximum = 511;
state.unit = "km/h";
state.receivers = {"ReceiverA", "ReceiverB"};
state.comment = "Standard signal comment";
state.valueDescriptions = {{std::uint64_t(4), "Standard four"}};
standard.signals = {mode, state};
FrameInfo extended;
extended.name = "Extended";
extended.canId = 291U;
extended.isExtended = true;
extended.hasPgn = true;
extended.dlc = 1U;
extended.transmitter = "SenderB";
extended.comment = "Extended frame comment";
state.multiplexing = Multiplexing();
state.startBit = 7U;
state.isLittleEndian = false;
state.isSigned = true;
state.factor = 0.5;
state.offset = -3;
state.minimum = -67;
state.maximum = 60.5;
state.unit = "deg C";
state.receivers = {"ReceiverB"};
state.comment = "Extended signal comment";
state.valueDescriptions = {{std::int64_t(-2), "Extended minus two"}};
extended.signals = {state};
FrameInfo j1939;
j1939.name = "J1939";
j1939.canId = 0x18F00442U;
j1939.isExtended = true;
j1939.hasPgn = true;
j1939.pgn = 0xF004U;
j1939.transmitter = "SenderC";
return DbcDatabase{{standard, extended, j1939}};
}
template<class Signal>
void CheckSignalMetadata(const Signal &actual, const SignalInfo &expected) {
QCOMPARE(actual.name, expected.name);
QCOMPARE(actual.startBit, expected.startBit);
QCOMPARE(actual.length, expected.length);
QCOMPARE(actual.factor, expected.factor);
QCOMPARE(actual.offset, expected.offset);
QCOMPARE(actual.minimum, expected.minimum);
QCOMPARE(actual.maximum, expected.maximum);
QCOMPARE(actual.unit, expected.unit);
QVERIFY(actual.receivers == expected.receivers);
QCOMPARE(actual.comment, expected.comment);
QVERIFY(actual.valueDescriptions == expected.valueDescriptions);
QCOMPARE(actual.multiplexing.role, expected.multiplexing.role);
QCOMPARE(actual.multiplexing.value, expected.multiplexing.value);
}
void CheckSignal(const SignalInfo &actual, const SignalInfo &expected) {
CheckSignalMetadata(actual, expected);
QCOMPARE(actual.isLittleEndian, expected.isLittleEndian);
QCOMPARE(actual.isSigned, expected.isSigned);
}
void CheckSignal(const DecodeSignal &actual, const SignalInfo &expected) {
CheckSignalMetadata(actual, expected);
QCOMPARE(actual.byteOrder, expected.isLittleEndian ? ByteOrder::Intel : ByteOrder::Motorola);
QCOMPARE(actual.valueType, expected.isSigned ? ValueType::Signed : ValueType::Unsigned);
}
template<class Frame>
void CheckFrame(const Frame &actual, const FrameInfo &expected) {
QCOMPARE(actual.name, expected.name);
QCOMPARE(actual.canId, expected.canId);
QCOMPARE(actual.isExtended, expected.isExtended);
QCOMPARE(actual.dlc, expected.dlc);
QCOMPARE(actual.pgn, expected.pgn);
QCOMPARE(actual.hasPgn, expected.hasPgn);
QCOMPARE(actual.transmitter, expected.transmitter);
QCOMPARE(actual.comment, expected.comment);
QCOMPARE(actual.signals.size(), expected.signals.size());
for (std::size_t i = 0; i < expected.signals.size(); ++i)
CheckSignal(actual.signals[i], expected.signals[i]);
}
void CheckTree(const TreeNode &root, const DbcDatabase &expected) {
QCOMPARE(root.GetType(), NodeType::Root);
QVERIFY(root.GetParent() == nullptr);
QVERIFY(root.GetFrame() == nullptr);
QVERIFY(root.GetSignal() == nullptr);
QCOMPARE(root.GetChildCount(), expected.frames.size());
for (std::size_t i = 0; i < expected.frames.size(); ++i) {
const TreeNode *frame = root.GetChild(i);
QVERIFY(frame != nullptr);
QCOMPARE(frame->GetType(), NodeType::Frame);
QCOMPARE(frame->GetParent(), &root);
QCOMPARE(frame->GetRow(), i);
QCOMPARE(frame->GetName(), expected.frames[i].name);
QVERIFY(frame->GetFrame() != nullptr);
QVERIFY(frame->GetSignal() == nullptr);
CheckFrame(*frame->GetFrame(), expected.frames[i]);
QCOMPARE(frame->GetChildCount(), expected.frames[i].signals.size());
for (std::size_t j = 0; j < frame->GetChildCount(); ++j) {
const TreeNode *signal = frame->GetChild(j);
QVERIFY(signal != nullptr);
QCOMPARE(signal->GetType(), NodeType::Signal);
QCOMPARE(signal->GetParent(), frame);
QCOMPARE(signal->GetRow(), j);
QCOMPARE(signal->GetName(), expected.frames[i].signals[j].name);
QVERIFY(signal->GetSignal() != nullptr);
QVERIFY(signal->GetFrame() == nullptr);
QCOMPARE(signal->GetChildCount(), std::size_t(0));
CheckSignal(*signal->GetSignal(), expected.frames[i].signals[j]);
}
}
QVERIFY(root.GetChild(expected.frames.size()) == nullptr);
}
void CheckUnknown(const DecodeDatabase &runtime, bool extended) {
const FrameKey key(291U, extended);
QVERIFY(runtime.FindFrame(key) == nullptr);
QVERIFY(runtime.FindSignal(SignalKey(key, "State")) == nullptr);
QVERIFY(DbcDecoder().FindFrame(runtime, key.canId, key.isExtended) == nullptr);
RawCanFrame raw;
raw.canId = key.canId;
raw.isExtended = extended;
raw.data = {3U, 4U};
const DecodedFrameValue decoded = DbcDecoder().Decode(runtime, raw);
QVERIFY(!decoded.valid);
QCOMPARE(decoded.status, FrameDecodeStatus::UnknownFrame);
QVERIFY(decoded.definition == nullptr);
QVERIFY(decoded.signals.empty());
QVERIFY(decoded.key == key);
}
}// namespace
class PipelineTest : public QObject {
Q_OBJECT
private Q_SLOTS:
void metadataLookupDecodeAndTree();
void emptyDatabase_data();
void emptyDatabase();
void databaseReplacement();
};
void PipelineTest::metadataLookupDecodeAndTree() {
const DbcParseResult parsed = ParseFixture(fixture);
QVERIFY(!parsed.HasErrors());
QVERIFY(parsed.diagnostics.empty());
const DbcDatabase expected = ExpectedMetadata();
QCOMPARE(parsed.database.frames.size(), expected.frames.size());
const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed.database);
QCOMPARE(runtime.GetFrames().size(), expected.frames.size());
const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed.database);
QVERIFY(tree != nullptr);
CheckTree(*tree, expected);
for (std::size_t i = 0; i < expected.frames.size(); ++i) {
CheckFrame(parsed.database.frames[i], expected.frames[i]);
CheckFrame(runtime.GetFrames()[i], expected.frames[i]);
const FrameKey key(expected.frames[i].canId, expected.frames[i].isExtended);
const std::shared_ptr<const DecodeFrame> found = runtime.FindFrame(key);
QVERIFY(found != nullptr);
QCOMPARE(found.get(), &runtime.GetFrames()[i]);
QCOMPARE(DbcDecoder().FindFrame(runtime, key.canId, key.isExtended).get(), found.get());
for (std::size_t j = 0; j < found->signals.size(); ++j) {
const std::shared_ptr<const DecodeSignal> signal = runtime.FindSignal(SignalKey(key, found->signals[j].name));
QVERIFY(signal != nullptr);
QCOMPARE(signal.get(), &found->signals[j]);
}
}
QVERIFY(runtime.FindSignal(SignalKey(FrameKey(291U, true), "Mode")) == nullptr);
QVERIFY(runtime.FindFrame(FrameKey(292U, false)) == nullptr);
RawCanFrame raw;
raw.canId = 291U;
raw.data = {3U, 4U};
const DecodedFrameValue standard = DbcDecoder().Decode(runtime, raw);
QVERIFY(standard.valid);
QCOMPARE(standard.status, FrameDecodeStatus::Valid);
QVERIFY(standard.key == FrameKey(291U, false));
QCOMPARE(standard.definition.get(), runtime.FindFrame(standard.key).get());
QCOMPARE(standard.signals.size(), std::size_t(2));
for (std::size_t i = 0; i < standard.signals.size(); ++i) {
const DecodedSignalValue &signal = standard.signals[i];
QVERIFY(signal.valid);
QCOMPARE(signal.status, SignalDecodeStatus::Valid);
QVERIFY(signal.key == SignalKey(standard.key, i == 0 ? "Mode" : "State"));
QCOMPARE(signal.definition.get(), runtime.FindSignal(signal.key).get());
QCOMPARE(std::get<std::uint64_t>(signal.rawValue), i == 0 ? 3U : 4U);
QCOMPARE(signal.physicalValue, i == 0 ? 3.0 : 9.0);
QVERIFY(signal.GetValueDescription() != nullptr);
QCOMPARE(*signal.GetValueDescription(), std::string(i == 0 ? "Selected" : "Standard four"));
}
raw.isExtended = true;
raw.data = {0xFEU};
const DecodedFrameValue extended = DbcDecoder().Decode(runtime, raw);
QVERIFY(extended.valid);
QCOMPARE(extended.status, FrameDecodeStatus::Valid);
QVERIFY(extended.key == FrameKey(291U, true));
QCOMPARE(extended.definition.get(), runtime.FindFrame(extended.key).get());
QCOMPARE(extended.signals.size(), std::size_t(1));
const DecodedSignalValue &signal = extended.signals[0];
QVERIFY(signal.valid);
QCOMPARE(signal.status, SignalDecodeStatus::Valid);
QVERIFY(signal.key == SignalKey(extended.key, "State"));
QCOMPARE(signal.definition.get(), runtime.FindSignal(signal.key).get());
QCOMPARE(std::get<std::int64_t>(signal.rawValue), std::int64_t(-2));
QCOMPARE(signal.physicalValue, -4.0);
QVERIFY(signal.GetValueDescription() != nullptr);
QCOMPARE(*signal.GetValueDescription(), std::string("Extended minus two"));
raw.canId = 0x18F00442U;
raw.data.clear();
const DecodedFrameValue j1939 = DbcDecoder().Decode(runtime, raw);
QVERIFY(j1939.valid);
QCOMPARE(j1939.status, FrameDecodeStatus::Valid);
QVERIFY(j1939.key == FrameKey(0x18F00442U, true));
QCOMPARE(j1939.definition.get(), runtime.FindFrame(j1939.key).get());
QVERIFY(j1939.signals.empty());
}
void PipelineTest::emptyDatabase_data() {
QTest::addColumn<QString>("text");
QTest::newRow("empty-file") << QString();
QTest::newRow("headers-only") << QString::fromUtf8("VERSION \"empty\"\nNS_ :\nBS_:\nBU_: Sender\n");
}
void PipelineTest::emptyDatabase() {
QFETCH(QString, text);
const DbcParseResult parsed = ParseFixture(text.toStdString());
QVERIFY(!parsed.HasErrors());
QVERIFY(parsed.diagnostics.empty());
QVERIFY(parsed.database.frames.empty());
const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed.database);
QVERIFY(runtime.GetFrames().empty());
CheckUnknown(runtime, false);
CheckUnknown(runtime, true);
const std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed.database);
QVERIFY(tree != nullptr);
CheckTree(*tree, DbcDatabase());
}
void PipelineTest::databaseReplacement() {
DbcParseResult parsed = ParseFixture(fixture);
QVERIFY(!parsed.HasErrors());
DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed.database);
std::unique_ptr<TreeNode> tree = DbcTreeBuilder().Build(parsed.database);
RawCanFrame raw;
raw.canId = 291U;
raw.data = {3U, 4U};
const DecodedFrameValue saved = DbcDecoder().Decode(runtime, raw);
QVERIFY(saved.valid);
QCOMPARE(saved.signals.size(), std::size_t(2));
QVERIFY(tree != nullptr);
std::unique_ptr<TreeNode> savedTree = std::move(tree);
parsed = ParseFixture(R"DBC(BO_ 291 Replacement: 1 NewSender
SG_ NewState : 0|8@1+ (3,-1) [-1|764] "V" NewReceiver
CM_ BO_ 291 "Replacement frame";
CM_ SG_ 291 NewState "Replacement signal";
VAL_ 291 NewState 4 "New four";
)DBC");
QVERIFY(!parsed.HasErrors());
QVERIFY(parsed.diagnostics.empty());
FrameInfo replacement;
replacement.name = "Replacement";
replacement.canId = 291U;
replacement.dlc = 1U;
replacement.transmitter = "NewSender";
replacement.comment = "Replacement frame";
SignalInfo state;
state.name = "NewState";
state.length = 8U;
state.factor = 3;
state.offset = -1;
state.minimum = -1;
state.maximum = 764;
state.unit = "V";
state.receivers = {"NewReceiver"};
state.comment = "Replacement signal";
state.valueDescriptions = {{std::uint64_t(4), "New four"}};
replacement.signals = {state};
const DbcDatabase expected{{replacement}};
QCOMPARE(parsed.database.frames.size(), std::size_t(1));
CheckFrame(parsed.database.frames[0], replacement);
runtime = DbcDecodeBuilder().Build(parsed.database);
tree = DbcTreeBuilder().Build(parsed.database);
QVERIFY(tree != nullptr);
QCOMPARE(runtime.GetFrames().size(), std::size_t(1));
CheckFrame(runtime.GetFrames()[0], replacement);
CheckTree(*tree, expected);
CheckUnknown(runtime, true);
QVERIFY(runtime.FindFrame(FrameKey(0x18F00442U, true)) == nullptr);
QVERIFY(runtime.FindSignal(SignalKey(FrameKey(291U, false), "State")) == nullptr);
QVERIFY(runtime.FindSignal(SignalKey(FrameKey(291U, false), "Mode")) == nullptr);
raw.data = {4U};
const DecodedFrameValue current = DbcDecoder().Decode(runtime, raw);
QVERIFY(current.valid);
QCOMPARE(current.status, FrameDecodeStatus::Valid);
QCOMPARE(current.signals.size(), std::size_t(1));
QVERIFY(current.signals[0].valid);
QVERIFY(current.signals[0].key == SignalKey(FrameKey(291U, false), "NewState"));
QCOMPARE(current.signals[0].definition.get(), runtime.FindSignal(current.signals[0].key).get());
QCOMPARE(std::get<std::uint64_t>(current.signals[0].rawValue), std::uint64_t(4));
QCOMPARE(current.signals[0].physicalValue, 11.0);
QVERIFY(current.signals[0].GetValueDescription() != nullptr);
QCOMPARE(*current.signals[0].GetValueDescription(), std::string("New four"));
parsed = ParseFixture("");
QVERIFY(!parsed.HasErrors());
QVERIFY(parsed.diagnostics.empty());
runtime = DbcDecodeBuilder().Build(parsed.database);
tree = DbcTreeBuilder().Build(parsed.database);
QVERIFY(tree != nullptr);
QVERIFY(runtime.GetFrames().empty());
CheckUnknown(runtime, false);
CheckUnknown(runtime, true);
CheckTree(*tree, DbcDatabase());
QVERIFY(runtime.FindSignal(current.signals[0].key) == nullptr);
CheckFrame(*current.definition, replacement);
CheckFrame(*saved.definition, ExpectedMetadata().frames[0]);
CheckTree(*savedTree, ExpectedMetadata());
QCOMPARE(saved.signals[1].physicalValue, 9.0);
QVERIFY(saved.signals[1].GetValueDescription() != nullptr);
QCOMPARE(*saved.signals[1].GetValueDescription(), std::string("Standard four"));
}
int RunPipelineTests(int argc, char **argv) {
PipelineTest test;
return QTest::qExec(&test, argc, argv);
}
#include "dbc_pipeline_test.moc"
+4 -1
View File
@@ -9,6 +9,8 @@ int RunLifetimeTests(int argc, char **argv);
int RunValidationTests(int argc, char **argv);
int RunPipelineTests(int argc, char **argv);
int main(int argc, char **argv) {
QCoreApplication application(argc, argv);
const int parserResult = RunParserTests(argc, argv);
@@ -16,5 +18,6 @@ int main(int argc, char **argv) {
const int coverageResult = RunCoverageTests(argc, argv);
const int lifetimeResult = RunLifetimeTests(argc, argv);
const int validationResult = RunValidationTests(argc, argv);
return validationResult | parserResult | decoderResult | coverageResult | lifetimeResult;
const int pipelineResult = RunPipelineTests(argc, argv);
return pipelineResult | validationResult | parserResult | decoderResult | coverageResult | lifetimeResult;
}
+1 -1
View File
@@ -8,5 +8,5 @@ CONFIG -= app_bundle
include(../core/core.pri)
include(parser_tests.pri)
include(decoder_tests.pri)
SOURCES += main.cpp dbc_coverage_test.cpp dbc_lifetime_test.cpp dbc_validation_test.cpp
SOURCES += main.cpp dbc_coverage_test.cpp dbc_lifetime_test.cpp dbc_validation_test.cpp dbc_pipeline_test.cpp
CONFIG -= debug_and_release debug_and_release_target