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

+1
View File
@@ -1,2 +1,3 @@
*.user
.project_rules
AGENTS.md
+8
View File
@@ -30,6 +30,9 @@ See [changelog.md](changelog.md) for the release and development history,
[tests/README.md](tests/README.md) for test instructions, and
[generated API documentation](docs/html/index.html) for the API reference.
[AGENTS.md](AGENTS.md) contains project instructions for coding agents,
including code style, build and test commands, and documentation requirements.
---
## High-Level Architecture
@@ -1380,6 +1383,11 @@ signal syntax and payload bounds, `VAL_` syntax and reference validation,
raw description lookup and preservation through both builders, statement dispatch, signed and unsigned
64-bit boundaries, physical conversion, numeric output, and invalid signal
payloads.
`dbc_pipeline_test.cpp` follows temporary DBC fixtures through parsed metadata,
runtime frame/signal lookups, decoded raw/physical values and descriptions, and
tree nodes. It verifies all supported metadata in both builders, standard and
extended frames sharing a numeric ID, empty databases, comments, and replacement
with another fixture or an empty database.
## Measuring code coverage
+7
View File
@@ -15,6 +15,13 @@ existing Git tag; subsequent changes are listed as unreleased.
#### Added
- Root `AGENTS.md` with project coding rules, build/test commands, and
documentation requirements, based on the existing `.project_rules`.
- End-to-end DBC fixture tests for parsed metadata, runtime frame/signal lookup,
raw and physical decoding, value descriptions, and tree nodes. Both builders
preserve supported metadata across same-numeric standard/extended IDs, comments,
empty databases, and database replacement.
- Exhaustive layout checks for both byte orders at representative payload sizes
(0, 1, 8, 64, and 255 bytes), including invalid lengths and uint32 overflow inputs.
- Regression coverage for builder input mutation/destruction and failed replacement
+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