diff --git a/.gitignore b/.gitignore index 642e3ec..e25b64c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *.user .project_rules +AGENTS.md diff --git a/README.md b/README.md index 0d14855..230e361 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/changelog.md b/changelog.md index 3efb19c..e88d055 100644 --- a/changelog.md +++ b/changelog.md @@ -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 diff --git a/tests/README.md b/tests/README.md index 225da77..c863fad 100644 --- a/tests/README.md +++ b/tests/README.md @@ -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. diff --git a/tests/dbc_pipeline_test.cpp b/tests/dbc_pipeline_test.cpp new file mode 100644 index 0000000..775bdb0 --- /dev/null +++ b/tests/dbc_pipeline_test.cpp @@ -0,0 +1,391 @@ +#include "dbc_decode_builder.h" +#include "dbc_decoder.h" +#include "dbc_parser.h" +#include "dbc_tree_builder.h" +#include +#include +#include +#include + +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(text.size())) != static_cast(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 + 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 + 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 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 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 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(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(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("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 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 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 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(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" diff --git a/tests/main.cpp b/tests/main.cpp index cdd9681..9511c77 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -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; } diff --git a/tests/tests.pro b/tests/tests.pro index 6fdaf4d..88ab460 100644 --- a/tests/tests.pro +++ b/tests/tests.pro @@ -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