diff --git a/README.md b/README.md index b00b758..25acad5 100644 --- a/README.md +++ b/README.md @@ -463,6 +463,7 @@ The current parser supports the following DBC elements: - `SG_` - `CM_ BO_` - `CM_ SG_` +- `VAL_` signal value descriptions --- @@ -799,6 +800,7 @@ lines: - `BO_` → `DbcFrameHandler` - `SG_` → `DbcSignalHandler` - `CM_` → `DbcCommentHandler` +- `VAL_` → `DbcValueDescriptionHandler` Grammar implementations live in `core/dbc_statement_handler.cpp`; their interface, context, and dispatcher live in `core/dbc_statement_handler.h`. `core/typefactory.h` @@ -814,7 +816,7 @@ void Handle(const std::string &line, DbcParseContext &context) const override; Register it in the dispatcher constructor: ```cpp -RegisterType("VAL_"); // after implementing DbcValueHandler +RegisterType("CUSTOM_"); // after implementing MyCustomHandler ``` Registration creates one handler; replacing a registration replaces that instance. @@ -829,6 +831,63 @@ and database builders remain independent of statement dispatch. --- +## Signal value descriptions (`VAL_`) + +Signal descriptions attach text to exact raw integer values: + +```dbc +BO_ 256 Status: 8 ECU + SG_ State : 0|8@1+ (1,0) [0|255] "" ECU +VAL_ 256 State 0 "Off" 1 "On" 2 "Error"; +``` + +Each statement must occupy one physical line, contain at least one integer/text +pair, and end with a semicolon. Spaces and tabs are accepted between fields; +descriptions may contain whitespace, semicolons, and empty text. Inside quoted +text, `\"` and `\\` represent a quote and a backslash; other escapes are rejected. +Keys are decimal integers in `INT64_MIN..UINT64_MAX`, with an optional minus +sign; plus signs, fractions, hexadecimal numbers, exponents, and overflow are +rejected. Keys never pass through `double`. + +`VAL_` may appear before or after its frame and signal declarations. References +are resolved after reading the file using the same CAN ID normalization as +`BO_`, including bit 31 for extended identifiers. Standard and extended frames +with the same normalized ID remain distinct. Missing or ambiguous references, +malformed statements, and duplicate integer keys produce `path:line: description` +errors at the originating `VAL_` line. Distinct keys from multiple statements +are merged; `0`, `00`, and `-0` are the same key. Keys need not fit the referenced +signal's bit width or signedness; unreachable keys simply cannot match a decode. + +Mappings are available as `SignalInfo::valueDescriptions` and +`DecodeSignal::valueDescriptions`, and survive both database builders, including +`TreeNode::GetSignal()`. Consumers can look up a decoded result directly: + +```cpp +const DecodedSignalValue &value = decodedFrame.signals.at(0); +if (const std::string *description = value.GetValueDescription()) { + std::cout << *description; +} +``` + +`GetValueDescription()` uses the exact raw integer before factor/offset scaling. +It returns `nullptr` for an invalid decode or an unknown value; numeric decoding +and `physicalValue` are unchanged. A mapped empty string returns a non-null +pointer. The pointer belongs to the signal definition and stays valid while that +definition and mapping remain alive and unchanged. + +For metadata or standalone raw values, use +`FindValueDescription(signal.valueDescriptions, rawValue)`, where `rawValue` is +`RawSignalValue` (`std::variant`). The helper matches +nonnegative signed and unsigned values identically and keeps negative values +distinct from large unsigned values. Stored keys use negative `int64_t` and +nonnegative `uint64_t`; when constructing mappings manually, normalize keys +with `NormalizeValueKey()` before insertion. + +Named `VAL_TABLE_` tables, environment-variable mappings, multiline statements, +and Qt presentation are outside this implementation. + +--- + ## Current Limitations This is still a minimal implementation. @@ -836,7 +895,7 @@ This is still a minimal implementation. Not supported yet: - multiplexed signals -- `VAL_` tables +- named `VAL_TABLE_` tables and environment-variable value descriptions - `BA_` attributes - `BA_DEF_` definitions - advanced comment handling @@ -847,8 +906,9 @@ Not supported yet: Additional behavior to account for: - unregistered DBC keywords are ignored -- an `SG_` statement before the first frame is ignored -- duplicate frame and signal definitions are not rejected +- an `SG_` statement before the first frame is rejected +- duplicate frame and signal definitions are not rejected globally; ambiguous + `VAL_` references to them are rejected - payload lengths are checked against storage bounds (`0..255`), rather than CAN or CAN FD protocol limits - comment handling is limited to the supported forms and already parsed targets @@ -868,6 +928,7 @@ A practical development order would be: - `BO_` - `SG_` - `CM_` +- `VAL_` signal value descriptions - normalized CAN ID - `isExtended` - transmitter @@ -883,7 +944,6 @@ Recommended additions: - parent pointer in `TreeNode` - Qt model adapter -- `VAL_` support for enum-style signals - better display strings for UI - selective decoding of only chosen signals @@ -981,6 +1041,7 @@ Open `docs/html/index.html` to view the generated reference. See [tests/README.md](tests/README.md) for QtTest build and execution commands. Tests cover frame validation and error locations, Intel and Motorola extraction, -signal syntax and payload bounds, statement dispatch, signed and unsigned +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. diff --git a/changelog.md b/changelog.md index 1e60a7a..3752a00 100644 --- a/changelog.md +++ b/changelog.md @@ -1,11 +1,28 @@ # Changelog -This changelog summarizes the Git history through `f17ddb6`. Entries are listed -newest first. Dates follow the commit author dates. `v1.0` is the existing Git -tag; subsequent changes have no release tag and are listed as unreleased. +This changelog summarizes release and development history. Entries are listed +newest first. Historical dates follow the commit author dates. `v1.0` is the +existing Git tag; subsequent changes are listed as unreleased. ## Unreleased +### 2026-10-06 + +#### Added + +- Added `VAL_` signal value descriptions through the statement dispatcher and + factory, with complete syntax validation, exact signed/unsigned 64-bit keys, + deferred reference resolution, duplicate detection, and source error locations. + Distinct keys from multiple statements merge; standard and extended frames + remain distinct under existing CAN ID normalization. +- Preserved descriptions in parsed metadata, runtime decode definitions, and UI + tree metadata. Added `DecodedSignalValue::GetValueDescription()` and + `FindValueDescription()` for raw integer lookup before physical scaling. + Unknown values retain normal numeric decoding. +- Added regression coverage for syntax, quoting, whitespace, integer boundaries, + declaration order, invalid references, duplicates, scaling, and both builders; + documented syntax, lookup behavior, and limitations in README. + ### 2026-10-05 #### Changed diff --git a/core/core_files.pri b/core/core_files.pri index 8bda16c..f660d76 100644 --- a/core/core_files.pri +++ b/core/core_files.pri @@ -16,5 +16,6 @@ HEADERS += \ $$PWD/decode_database.h \ $$PWD/frame_info.h \ $$PWD/signal_info.h \ + $$PWD/value_descriptions.h \ $$PWD/tree_node.h \ $$PWD/typefactory.h diff --git a/core/dbc_decode_builder.cpp b/core/dbc_decode_builder.cpp index 7acbd4d..d5027e2 100644 --- a/core/dbc_decode_builder.cpp +++ b/core/dbc_decode_builder.cpp @@ -43,6 +43,7 @@ DecodeDatabase DbcDecodeBuilder::Build(const DbcDatabase &source) const { targetSignal.unit = sourceSignal.unit; targetSignal.receivers = sourceSignal.receivers; targetSignal.comment = sourceSignal.comment; + targetSignal.valueDescriptions = sourceSignal.valueDescriptions; targetFrame.signals.push_back(targetSignal); } diff --git a/core/dbc_decoder.h b/core/dbc_decoder.h index 56d1bc0..d2c699d 100644 --- a/core/dbc_decoder.h +++ b/core/dbc_decoder.h @@ -35,10 +35,15 @@ struct RawCanFrame { * @brief One decoded signal value. */ struct DecodedSignalValue { - const DecodeSignal *definition; /**< Signal definition. */ - std::variant rawValue; /**< Exact signed or unsigned raw value. */ - double physicalValue; /**< Converted physical value. */ - bool valid; /**< true if decoding succeeded. */ + const DecodeSignal *definition; /**< Signal definition. */ + RawSignalValue rawValue; /**< Exact signed or unsigned raw value. */ + double physicalValue; /**< Converted physical value. */ + bool valid; /**< true if decoding succeeded. */ + + /** Lookup using the raw integer; nullptr for invalid or unmapped values. */ + const std::string *GetValueDescription() const { + return valid && definition ? FindValueDescription(definition->valueDescriptions, rawValue) : nullptr; + } DecodedSignalValue() : definition(nullptr), rawValue(std::uint64_t(0U)), physicalValue(0.0), valid(false) { diff --git a/core/dbc_parser.cpp b/core/dbc_parser.cpp index 5a697fc..aacdb9c 100644 --- a/core/dbc_parser.cpp +++ b/core/dbc_parser.cpp @@ -55,6 +55,7 @@ DbcDatabase DbcParser::ParseFile(const std::string &filePath) const { continue; try { + context.lineNumber = lineNumber; dispatcher.Dispatch(line, context); } catch (const std::exception &error) { throw std::runtime_error(filePath + ":" + std::to_string(lineNumber) + @@ -62,6 +63,7 @@ DbcDatabase DbcParser::ParseFile(const std::string &filePath) const { } } + context.ResolveValueDescriptions(filePath); return database; } diff --git a/core/dbc_parser.h b/core/dbc_parser.h index 8cd39fa..606eb4b 100644 --- a/core/dbc_parser.h +++ b/core/dbc_parser.h @@ -24,11 +24,12 @@ * - SG_ * - CM_ BO_ * - CM_ SG_ + * - VAL_ signal value descriptions * * Ignores: * - attributes * - multiplexing - * - value tables + * - named VAL_TABLE_ tables */ class DbcParser { public: diff --git a/core/dbc_statement_handler.cpp b/core/dbc_statement_handler.cpp index beb76a7..1ff22d0 100644 --- a/core/dbc_statement_handler.cpp +++ b/core/dbc_statement_handler.cpp @@ -309,6 +309,7 @@ DbcStatementDispatcher::DbcStatementDispatcher() { RegisterType("BO_"); RegisterType("SG_"); RegisterType("CM_"); + RegisterType("VAL_"); } bool DbcStatementDispatcher::Dispatch(const std::string &line, @@ -322,3 +323,88 @@ bool DbcStatementDispatcher::Dispatch(const std::string &line, handler->second->Handle(line, context); return true; } + +void DbcValueDescriptionHandler::Handle(const std::string &line, DbcParseContext &context) const { + static const std::regex header(R"(^VAL_[ \t]+([^ \t]+)[ \t]+([^ \t;]+)[ \t]+(.*)$)"); + static const std::regex pair(R"dbc(^([^ \t]+)[ \t]+"((?:[^"\\]|\\["\\])*)")dbc"); + std::smatch fields; + if (!std::regex_match(line, fields, header)) + throw std::runtime_error("Malformed or unsupported VAL_ statement."); + const auto rawId = ParseDecimal(fields[1].str(), std::numeric_limits::max(), "VAL_ CAN ID"); + if ((rawId & 0x60000000U) != 0U) + throw std::runtime_error("VAL_ CAN ID exceeds the 29-bit identifier range."); + PendingValueDescriptions pending; + NormalizeCanId(rawId, pending.canId, pending.isExtended); + pending.signalName = fields[2].str(); + pending.lineNumber = context.lineNumber; + std::string rest = fields[3].str(); + while (rest != ";") { + if (!std::regex_search(rest, fields, pair)) + throw std::runtime_error("Malformed VAL_ value/description pair or missing semicolon."); + const std::string number = fields[1].str(); + RawSignalValue key; + if (!number.empty() && number[0] == '-') { + std::int64_t value = 0; + const auto parsed = std::from_chars(number.data(), number.data() + number.size(), value); + if (parsed.ec != std::errc() || parsed.ptr != number.data() + number.size()) + throw std::runtime_error("Invalid VAL_ integer: " + number); + key = NormalizeValueKey(RawSignalValue(value)); + } else { + std::uint64_t value = 0; + const auto parsed = std::from_chars(number.data(), number.data() + number.size(), value); + if (number.empty() || parsed.ec != std::errc() || parsed.ptr != number.data() + number.size()) + throw std::runtime_error("Invalid VAL_ integer: " + number); + key = value; + } + std::string description; + const std::string quoted = fields[2].str(); + for (std::size_t i = 0; i < quoted.size(); ++i) { + if (quoted[i] == '\\') + ++i; + description += quoted[i]; + } + if (!pending.descriptions.emplace(key, description).second) + throw std::runtime_error("Duplicate VAL_ value key: " + number); + const auto consumed = static_cast(fields.length()); + rest.erase(0, consumed); + if (!rest.empty() && rest[0] != ';' && rest[0] != ' ' && rest[0] != '\t') + throw std::runtime_error("Missing whitespace between VAL_ pairs."); + rest = TrimText(rest); + } + if (pending.descriptions.empty()) + throw std::runtime_error("VAL_ requires at least one value/description pair."); + context.pendingValues.push_back(std::move(pending)); +} + +void DbcParseContext::ResolveValueDescriptions(const std::string &filePath) { + for (const auto &pending: pendingValues) { + try { + FrameInfo *frame = nullptr; + for (auto &candidate: database.frames) { + if (candidate.canId == pending.canId && candidate.isExtended == pending.isExtended) { + if (frame) + throw std::runtime_error("Ambiguous VAL_ frame reference."); + frame = &candidate; + } + } + if (!frame) + throw std::runtime_error("Unresolved VAL_ frame reference."); + SignalInfo *signal = nullptr; + for (auto &candidate: frame->signals) { + if (candidate.name == pending.signalName) { + if (signal) + throw std::runtime_error("Ambiguous VAL_ signal reference: " + pending.signalName); + signal = &candidate; + } + } + if (!signal) + throw std::runtime_error("Unresolved VAL_ signal reference: " + pending.signalName); + for (const auto &entry: pending.descriptions) { + if (!signal->valueDescriptions.insert(entry).second) + throw std::runtime_error("Duplicate VAL_ value key for signal: " + pending.signalName); + } + } catch (const std::exception &error) { + throw std::runtime_error(filePath + ":" + std::to_string(pending.lineNumber) + ": " + error.what()); + } + } +} diff --git a/core/dbc_statement_handler.h b/core/dbc_statement_handler.h index ffd5a8d..0a63f1b 100644 --- a/core/dbc_statement_handler.h +++ b/core/dbc_statement_handler.h @@ -7,10 +7,22 @@ #include #include +struct PendingValueDescriptions { + std::uint32_t canId; + bool isExtended; + std::string signalName; + ValueDescriptions descriptions; + std::size_t lineNumber; +}; + /** Mutable state belonging to one file parse. Handlers do not retain it. */ struct DbcParseContext { DbcDatabase &database; FrameInfo *currentFrame = nullptr; + std::size_t lineNumber = 0U; + std::vector pendingValues = {}; + + void ResolveValueDescriptions(const std::string &filePath); }; class DbcStatementHandler { @@ -43,6 +55,11 @@ private: static void ParseCommentLine(const std::string &line, DbcDatabase &database); }; +class DbcValueDescriptionHandler : public DbcStatementHandler { +public: + void Handle(const std::string &line, DbcParseContext &context) const override; +}; + /** Exact-key dispatch to handlers created once per registration via TypeFactory. */ class DbcStatementDispatcher { public: diff --git a/core/decode_database.h b/core/decode_database.h index a065940..5ea4d1c 100644 --- a/core/decode_database.h +++ b/core/decode_database.h @@ -12,6 +12,7 @@ #ifndef DECODE_DATABASE_H #define DECODE_DATABASE_H +#include "value_descriptions.h" #include #include #include @@ -37,18 +38,19 @@ enum class ValueType { * @brief Runtime-ready signal definition. */ struct DecodeSignal { - std::string name; /**< Signal name. */ - std::uint32_t startBit; /**< DBC start bit. */ - std::uint32_t length; /**< Signal length in bits. */ - ByteOrder byteOrder; /**< Intel or Motorola. */ - ValueType valueType; /**< Signed or unsigned. */ - double factor; /**< Scaling factor. */ - double offset; /**< Physical offset. */ - double minimum; /**< Minimum physical value. */ - double maximum; /**< Maximum physical value. */ - std::string unit; /**< Physical unit. */ - std::vector receivers; /**< Receivers. */ - std::string comment; /**< Comment. */ + std::string name; /**< Signal name. */ + std::uint32_t startBit; /**< DBC start bit. */ + std::uint32_t length; /**< Signal length in bits. */ + ByteOrder byteOrder; /**< Intel or Motorola. */ + ValueType valueType; /**< Signed or unsigned. */ + double factor; /**< Scaling factor. */ + double offset; /**< Physical offset. */ + double minimum; /**< Minimum physical value. */ + double maximum; /**< Maximum physical value. */ + std::string unit; /**< Physical unit. */ + std::vector receivers; /**< Receivers. */ + ValueDescriptions valueDescriptions; /**< Raw integer state descriptions. */ + std::string comment; /**< Comment. */ DecodeSignal() : name(), startBit(0U), length(0U), byteOrder(ByteOrder::Intel), valueType(ValueType::Unsigned), factor(1.0), offset(0.0), minimum(0.0), maximum(0.0), unit(), receivers(), comment() { diff --git a/core/signal_info.h b/core/signal_info.h index 9fa2f34..1e19849 100644 --- a/core/signal_info.h +++ b/core/signal_info.h @@ -12,6 +12,7 @@ #ifndef SIGNAL_INFO_H #define SIGNAL_INFO_H +#include "value_descriptions.h" #include #include #include @@ -20,18 +21,19 @@ * @brief Describes one signal inside a DBC frame. */ struct SignalInfo { - std::string name; /**< Signal name. */ - std::uint32_t startBit; /**< Start bit in DBC notation. */ - std::uint32_t length; /**< Signal length in bits. */ - bool isLittleEndian; /**< true for Intel, false for Motorola. */ - bool isSigned; /**< true if signal is signed. */ - double factor; /**< Scaling factor. */ - double offset; /**< Physical offset. */ - double minimum; /**< Minimum physical value. */ - double maximum; /**< Maximum physical value. */ - std::string unit; /**< Physical unit. */ - std::vector receivers; /**< Signal receivers. */ - std::string comment; /**< Optional signal comment. */ + std::string name; /**< Signal name. */ + std::uint32_t startBit; /**< Start bit in DBC notation. */ + std::uint32_t length; /**< Signal length in bits. */ + bool isLittleEndian; /**< true for Intel, false for Motorola. */ + bool isSigned; /**< true if signal is signed. */ + double factor; /**< Scaling factor. */ + double offset; /**< Physical offset. */ + double minimum; /**< Minimum physical value. */ + double maximum; /**< Maximum physical value. */ + std::string unit; /**< Physical unit. */ + std::vector receivers; /**< Signal receivers. */ + ValueDescriptions valueDescriptions; /**< Raw integer state descriptions. */ + std::string comment; /**< Optional signal comment. */ SignalInfo() : name(), startBit(0U), length(0U), isLittleEndian(true), isSigned(false), factor(1.0), offset(0.0), minimum(0.0), maximum(0.0), unit(), receivers(), comment() { diff --git a/core/value_descriptions.h b/core/value_descriptions.h new file mode 100644 index 0000000..03b8696 --- /dev/null +++ b/core/value_descriptions.h @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT +#ifndef VALUE_DESCRIPTIONS_H +#define VALUE_DESCRIPTIONS_H + +#include +#include +#include +#include + +using RawSignalValue = std::variant; + +/** Canonical keys: negative int64_t, nonnegative uint64_t. */ +inline RawSignalValue NormalizeValueKey(const RawSignalValue &value) { + if (const auto *number = std::get_if(&value)) { + if (*number >= 0) + return static_cast(*number); + } + return value; +} + +using ValueDescriptions = std::map; + +/** Returns a borrowed string, or nullptr; never converts integers to double. */ +inline const std::string *FindValueDescription(const ValueDescriptions &descriptions, + const RawSignalValue &rawValue) { + const auto found = descriptions.find(NormalizeValueKey(rawValue)); + return found == descriptions.end() ? nullptr : &found->second; +} + +#endif diff --git a/tests/README.md b/tests/README.md index 41a6d34..218c24b 100644 --- a/tests/README.md +++ b/tests/README.md @@ -50,6 +50,10 @@ error paths and line numbers, and integration with signals and comments. Signal tests cover complete syntax and numeric validation, scientific notation, finite physical parameters and ordered ranges, spaces and tabs, receiver lists, unsupported multiplexing, and Intel/Motorola declared-payload boundaries. +Value-description tests cover full `VAL_` syntax, whitespace, escaped quotes, +empty text, exact integer boundaries, duplicate keys within and across statements, +deferred and normalized frame references, missing targets, error locations, +raw lookup with scaling, unknown/invalid values, and both database builders. Temporary DBC fixtures are created in the system temporary directory and removed automatically. diff --git a/tests/dbc_parser_test.cpp b/tests/dbc_parser_test.cpp index d276ba1..eac2d1d 100644 --- a/tests/dbc_parser_test.cpp +++ b/tests/dbc_parser_test.cpp @@ -1,5 +1,9 @@ +#include "dbc_decode_builder.h" +#include "dbc_decoder.h" #include "dbc_parser.h" #include "dbc_statement_handler.h" +#include "dbc_tree_builder.h" +#include #include #include @@ -66,6 +70,8 @@ private Q_SLOTS: void validSignals(); void dispatch(); void independentSessions(); + void valueDescriptions(); + void invalidValueDescriptions(); }; void DbcParserTest::validFrames() { @@ -256,7 +262,7 @@ void DbcParserTest::dispatch() { "Factory dispatch and handler reuse"); CHECK(!dispatcher.Dispatch("TEST_OTHER data", context) && !dispatcher.Dispatch("BO_TX_BU_ 1 : ECU;", context) && - !dispatcher.Dispatch("VAL_ 1 Speed 0 \"Stopped\";", context) && + !dispatcher.Dispatch("VAL_TABLE_ States 0 \"Stopped\";", context) && CountingHandler::calls == 2, "Exact and unknown keyword dispatch"); } @@ -281,6 +287,125 @@ void DbcParserTest::independentSessions() { "Fresh parse after error"); } + +void DbcParserTest::valueDescriptions() { + Fixture fixture; + fixture.Write( + "VAL_\t256\tState\t0\t\"Off\"\t1 \"On\" 2 \"Error with spaces; and \\\"quotes\\\"\";\n" + "BO_ 256 Status: 8 ECU\n" + "SG_ State : 0|8@1+ (2,10) [0|520] \"\" ECU\n" + "VAL_ 256 State 3 \"\";\n" + "VAL_ 2147483904 State -9223372036854775808 \"Minimum\" -1 \"Negative\" 9223372036854775807 \"Maximum\";\n" + "BO_ 2147483904 Extended: 8 ECU\n" + "SG_ State : 0|64@1- (1,0) [-1e20|1e20] \"\" ECU\n" + "BO_ 2048 Unsigned: 8 ECU\n" + "SG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n" + "VAL_ 2147485696 State 9223372036854775808 \"High\" 18446744073709551615 \"Full\";\n"); + const auto parsed = DbcParser().ParseFile(fixture.path.string()); + const auto runtime = DbcDecodeBuilder().Build(parsed); + const auto tree = DbcTreeBuilder().Build(parsed); + CHECK(tree->GetChild(0)->GetSignal() == nullptr, "Frame node"); + for (std::size_t index = 0; index < parsed.frames.size(); ++index) { + CHECK(tree->GetChild(index)->GetChild(0)->GetSignal()->valueDescriptions == + parsed.frames[index].signals[0].valueDescriptions && + runtime.frames[index].signals[0].valueDescriptions == + parsed.frames[index].signals[0].valueDescriptions, + "All mappings survive both builders"); + } + CHECK(tree->GetChild(0)->GetChild(0)->GetSignal()->valueDescriptions == + runtime.frames[0].signals[0].valueDescriptions, + "Both builders preserve descriptions"); + RawCanFrame frame; + frame.canId = 256; + frame.data.resize(8); + const std::string expected[] = {"Off", "On", "Error with spaces; and \"quotes\"", ""}; + for (unsigned value = 0; value < 5; ++value) { + frame.data[0] = value; + const auto decoded = DbcDecoder().Decode(runtime, frame).signals[0]; + CHECK(decoded.valid && decoded.physicalValue == value * 2 + 10, "Numeric scaling unchanged"); + const auto description = decoded.GetValueDescription(); + CHECK(value < 4 ? description && *description == expected[value] : !description, + "Raw lookup and unknown value"); + } + frame.isExtended = true; + struct Boundary { + std::uint64_t bits; + const char *description; + }; + for (const auto &test: {Boundary{0x8000000000000000ULL, "Minimum"}, + Boundary{0xFFFFFFFFFFFFFFFFULL, "Negative"}, + Boundary{0x7FFFFFFFFFFFFFFFULL, "Maximum"}}) { + for (unsigned byte = 0; byte < 8; ++byte) + frame.data[byte] = static_cast(test.bits >> (byte * 8)); + const auto decoded = DbcDecoder().Decode(runtime, frame).signals[0]; + CHECK(decoded.GetValueDescription() && *decoded.GetValueDescription() == test.description, + "Signed boundaries and distinct extended frame"); + } + frame.canId = 2048; + for (const auto &test: {Boundary{0x8000000000000000ULL, "High"}, + Boundary{0xFFFFFFFFFFFFFFFFULL, "Full"}}) { + for (unsigned byte = 0; byte < 8; ++byte) + frame.data[byte] = static_cast(test.bits >> (byte * 8)); + const auto decoded = DbcDecoder().Decode(runtime, frame).signals[0]; + CHECK(decoded.GetValueDescription() && *decoded.GetValueDescription() == test.description, + "Unsigned boundaries"); + } + CHECK(FindValueDescription(runtime.frames[0].signals[0].valueDescriptions, std::int64_t(1)) != nullptr, + "Nonnegative signed lookup matches unsigned key"); + frame.data.clear(); + CHECK(!DbcDecoder().Decode(runtime, frame).signals[0].GetValueDescription(), "Invalid decode has no description"); +} + +void DbcParserTest::invalidValueDescriptions() { + Fixture fixture; + const std::string definitions = "BO_ 1 Frame: 8 ECU\nSG_ State : 0|64@1+ (1,0) [0|2e20] \"\" ECU\n"; + for (const std::string statement: { + "VAL_", "VAL_ 1 State;", "VAL_ 1 State ;", "VAL_ 1 State 0 \"Off\"", + "VAL_ 1 State 0 \"Off\"; junk", "VAL_ 1 State 0 Off;", + "VAL_ 1 State 0 \"Unclosed;", "VAL_ 1 State 0 \"Off\"1 \"On\";", + "VAL_ 1 State 0 \"Off\" 00 \"Duplicate\";", + "VAL_ 1 State 0 \"Off\" -0 \"Duplicate\";", + "VAL_ 1 State 0 \"Off\";\nVAL_ 1 State 0 \"Duplicate\";", + "VAL_ 2 State 0 \"Missing frame\";", "VAL_ 1 Missing 0 \"Missing signal\";", + "VAL_ -1 State 0 \"Bad ID\";", "VAL_ 536870912 State 0 \"Bad ID\";", + "VAL_ 4294967296 State 0 \"Bad ID\";", "VAL_ Environment 0 \"Unsupported\";", + "VAL_ 1 State 0 \"Bad\\q escape\";"}) { + fixture.Write(definitions + statement + '\n'); + const auto line = statement.find('\n') == std::string::npos ? 3 : 4; + try { + DbcParser().ParseFile(fixture.path.string()); + CHECK(false, statement); + } catch (const std::runtime_error &error) { + CHECK(std::string(error.what()).find(fixture.path.string() + ":" + std::to_string(line) + ": ") == 0, + statement + " " + error.what()); + } + } + for (const std::string suffix: { + "BO_ 1 Other: 8 ECU\n", // Ambiguous normalized frame reference. + "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n",// Ambiguous signal reference. + ""}) { + fixture.Write("VAL_ 1 " + std::string(suffix.empty() ? "Missing" : "State") + + " 0 \"Description\";\n" + definitions + suffix); + try { + DbcParser().ParseFile(fixture.path.string()); + CHECK(false, "Deferred missing or ambiguous reference"); + } catch (const std::runtime_error &error) { + CHECK(std::string(error.what()).find(fixture.path.string() + ":1: ") == 0, + "Deferred reference retains source location"); + } + } + for (const std::string number: {"+1", "1x", "0x10", "1.5", "1e2", "--1", + "-9223372036854775809", "18446744073709551616"}) { + fixture.Write("VAL_ 1 State " + number + " \"Bad\";\n" + definitions); + try { + DbcParser().ParseFile(fixture.path.string()); + CHECK(false, number); + } catch (const std::runtime_error &error) { + CHECK(std::string(error.what()).find(fixture.path.string() + ":1: ") == 0, number); + } + } +} + int RunParserTests(int argc, char **argv) { DbcParserTest tests; return QTest::qExec(&tests, argc, argv);