#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 #include #include #include #include #define CHECK(condition, name) QVERIFY2((condition), std::string(name).c_str()) #include struct Fixture { std::filesystem::path path = std::filesystem::temp_directory_path() / ("dbc_parser_test_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".dbc"); ~Fixture() { std::error_code error; std::filesystem::remove(path, error); } void Write(const std::string &text) const { std::ofstream output(path); output << text; if (!output) throw std::runtime_error("Cannot write test fixture."); } }; class CommaDecimalPunctuation : public std::numpunct { protected: char do_decimal_point() const override { return ','; } char do_thousands_sep() const override { return '.'; } std::string do_grouping() const override { return "\3"; } }; class ApplicationLocaleGuard { public: ApplicationLocaleGuard() : previousCppLocale(std::locale()), previousCLocale(std::setlocale(LC_ALL, nullptr)) {} ~ApplicationLocaleGuard() { std::locale::global(previousCppLocale); std::setlocale(LC_ALL, previousCLocale.c_str()); } private: std::locale previousCppLocale; std::string previousCLocale; }; class CountingHandler : public DbcStatementHandler { public: inline static int instances = 0; inline static int calls = 0; CountingHandler() { ++instances; } void Handle(const std::string &, DbcParseContext &context) const override { ++calls; context.database.frames.emplace_back(); } }; std::string SignalLine(const std::string &start, const std::string &length, const std::string &order, const std::string &factor, const std::string &offset, const std::string &minimum, const std::string &maximum, const std::string &name = "Value") { return "SG_ " + name + " : " + start + "|" + length + "@" + order + " (" + factor + "," + offset + ") [" + minimum + "|" + maximum + "] \"\" ECU"; } class DbcParserTest : public QObject { Q_OBJECT private: void CheckPhysicalNumbers(); void RejectSignal(const Fixture &fixture, const std::string &line, unsigned payload = 8U) { fixture.Write("BO_ 1 Frame: " + std::to_string(payload) + " ECU\n\n" + line + "\n"); try { DbcParser().ParseFile(fixture.path.string()); CHECK(false, "Reject " + line); } catch (const std::runtime_error &error) { const std::string prefix = fixture.path.string() + ":3: "; CHECK(std::string(error.what()).find(prefix) == 0U && std::string(error.what()).size() > prefix.size(), "Reject " + line); } } private Q_SLOTS: void diagnostics(); void diagnosticErrors(); void diagnosticUnsupportedStatements(); void diagnosticNamespaceBoundaries(); void diagnosticFormatting(); void multiplexing(); void multiplexingBoundaries(); void validFrames(); void invalidFrames(); void duplicateFrames(); void duplicateSignals(); void distinctDefinitions(); void comments(); void invalidComments_data(); void invalidComments(); void integration(); void invalidSignals(); void validSignals(); void physicalNumbersCppLocale(); void physicalNumbersCLocale(); void dispatch(); void independentSessions(); void valueDescriptions(); void invalidValueDescriptions(); }; void DbcParserTest::diagnostics() { Fixture fixture; fixture.Write("VERSION \"test\"\n" "NS_ :\n" " CM_\n" " BA_\n" " SIG_VALTYPE_\n" " SG_MUL_VAL_\n\n" "BS_:\n" "BU_: ECU\n" "BO_ 1 Frame: 1 ECU\n" "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "SIG_VALTYPE_ 1 Value : 1;\n" "BA_ \"SignalType\" SG_ 1 Value 1;\n" "VENDOR_EXTENSION_ data;\n"); const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); CHECK(!result.HasErrors() && result.database.frames.size() == 1U, "Warnings preserve the supported database"); QCOMPARE(result.diagnostics.size(), std::size_t(3U)); for (std::size_t index = 0U; index < result.diagnostics.size(); ++index) { const DbcDiagnostic &diagnostic = result.diagnostics[index]; CHECK(diagnostic.severity == DbcDiagnosticSeverity::Warning && diagnostic.code == DbcDiagnosticCode::UnsupportedStatement, "Unsupported statements have structured warning severity and code"); QCOMPARE(diagnostic.filePath, fixture.path.string()); QCOMPARE(diagnostic.lineNumber, 12U + index); CHECK(diagnostic.message.find("ignored") != std::string::npos, "Warning explains unsupported syntax"); } CHECK(DbcParser().ParseFile(fixture.path.string()).frames.size() == 1U, "Legacy API accepts files with warnings"); fixture.Write("VERSION \"\"\nNS_:\nSIG_VALTYPE_\n\tBA_\n BS_ :\n BU_ : ECU\n"); const DbcParseResult headers = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); CHECK(headers.diagnostics.empty(), "Common header variants stay quiet"); fixture.Write("NS_ :\n BA_\n BO_\n"); CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).HasErrors(), "Namespace handling does not swallow a malformed frame"); } void DbcParserTest::diagnosticErrors() { Fixture fixture; struct ErrorCase { const char *text; DbcDiagnosticCode code; std::size_t line; }; const ErrorCase cases[] = { {"UNKNOWN_ data;\n\nBO_ invalid Frame: 1 ECU\n", DbcDiagnosticCode::InvalidStatement, 3U}, {"BO_ 1 Frame: 1 ECU\nSG_MUL_VAL_ 1 Value Mode 1-2;\n", DbcDiagnosticCode::UnsupportedStatement, 2U}, {"BO_ 1 Frame: 1 ECU\nVAL_ 1 Missing 0 \"Off\";\n", DbcDiagnosticCode::InvalidValueDescription, 2U}, {"BO_ 1 Frame: 1 ECU\nSG_ Value m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n", DbcDiagnosticCode::InvalidMultiplexing, 2U}, {"BO_ 1 Frame: 1 ECU\nSG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n", DbcDiagnosticCode::InvalidMultiplexing, 2U}, {"VAL_ broken\n", DbcDiagnosticCode::InvalidValueDescription, 1U}, {"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n", DbcDiagnosticCode::InvalidStatement, 1U}, {"BO_ 1 Frame: 1 ECU\nBO_ 1 Duplicate: 1 ECU\n", DbcDiagnosticCode::InvalidStatement, 2U}, {"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n", DbcDiagnosticCode::InvalidStatement, 3U}, {"BO_ 1 Frame: 1 ECU\nSG_ Value : 8|8@1+ (1,0) [0|255] \"\" ECU\n", DbcDiagnosticCode::InvalidStatement, 2U}, {"BO_ 1 Frame: 1 ECU\nVAL_ 2 Missing 0 \"Off\";\n", DbcDiagnosticCode::InvalidValueDescription, 2U}, {"BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "VAL_ 1 Value 0 \"Off\";\nVAL_ 1 Value 0 \"Duplicate\";\n", DbcDiagnosticCode::InvalidValueDescription, 4U}, {"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n" "SG_ Value m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n", DbcDiagnosticCode::InvalidMultiplexing, 3U}, {"BO_ 1 Frame: 2 ECU\nSG_ Mode M : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "SG_ Other M : 8|8@1+ (1,0) [0|255] \"\" ECU\n", DbcDiagnosticCode::InvalidMultiplexing, 3U}, {"BO_ 1 Frame: 1 ECU\nSG_ First : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "SG_ Second : 7|8@0+ (1,0) [0|255] \"\" ECU\n", DbcDiagnosticCode::InvalidMultiplexing, 3U} }; for (const ErrorCase &test : cases) { fixture.Write(test.text); const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); CHECK(result.HasErrors() && result.database.frames.empty(), "Failed parse never returns a partial database"); const DbcDiagnostic &diagnostic = result.diagnostics.back(); CHECK(diagnostic.severity == DbcDiagnosticSeverity::Error && diagnostic.code == test.code, "Error has expected severity and code"); QCOMPARE(diagnostic.filePath, fixture.path.string()); QCOMPARE(diagnostic.lineNumber, test.line); CHECK(!diagnostic.message.empty(), "Error includes a message"); const bool hasWarning = std::string(test.text).find("UNKNOWN_") == 0U; QCOMPARE(result.diagnostics.size(), hasWarning ? std::size_t(2U) : std::size_t(1U)); try { DbcParser().ParseFile(fixture.path.string()); CHECK(false, "Legacy parser must throw"); } catch (const std::runtime_error &error) { QCOMPARE(std::string(error.what()), fixture.path.string() + ":" + std::to_string(test.line) + ": " + diagnostic.message); } } const std::string missingPath = fixture.path.string() + ".missing"; const DbcParseResult missing = DbcParser().ParseFileWithDiagnostics(missingPath); CHECK(missing.HasErrors() && missing.database.frames.empty(), "Missing file returns error"); QCOMPARE(missing.diagnostics.size(), std::size_t(1U)); CHECK(missing.diagnostics[0].code == DbcDiagnosticCode::FileOpenError, "Missing file has a distinct code"); QCOMPARE(missing.diagnostics[0].lineNumber, std::size_t(0U)); QCOMPARE(missing.diagnostics[0].filePath, missingPath); QCOMPARE(missing.diagnostics[0].message, "Failed to open DBC file: " + missingPath); try { DbcParser().ParseFile(missingPath); CHECK(false, "Legacy API throws on missing file"); } catch (const std::runtime_error &error) { QCOMPARE(std::string(error.what()), missing.diagnostics[0].message); } const std::string directoryPath = fixture.path.parent_path().string(); const DbcParseResult directory = DbcParser().ParseFileWithDiagnostics(directoryPath); CHECK(directory.HasErrors() && directory.database.frames.empty(), "Directory cannot be parsed as a file"); QCOMPARE(directory.diagnostics.size(), std::size_t(1U)); CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError || directory.diagnostics[0].code == DbcDiagnosticCode::FileOpenError, "Directory produces a file-level error on supported platforms"); QCOMPARE(directory.diagnostics[0].lineNumber, std::size_t(0U)); QCOMPARE(directory.diagnostics[0].filePath, directoryPath); #ifdef __linux__ CHECK(directory.diagnostics[0].code == DbcDiagnosticCode::FileReadError, "Linux directory input exercises the read-error path"); QCOMPARE(directory.diagnostics[0].message, "Failed to read DBC file: " + directoryPath); #endif try { DbcParser().ParseFile(directoryPath); CHECK(false, "Legacy API throws on unreadable input"); } catch (const std::runtime_error &error) { QCOMPARE(std::string(error.what()), directory.diagnostics[0].message); } fixture.Write("BO_ 1 Frame: 1 ECU\n"); CHECK(DbcParser().ParseFileWithDiagnostics(fixture.path.string()).diagnostics.empty(), "Diagnostics belong to an independent parse session"); } void DbcParserTest::diagnosticUnsupportedStatements() { Fixture fixture; const std::string definitions = "BO_ 1 Frame: 1 ECU\nSG_ Value : 0|8@1+ (2,10) [10|520] \"V\" ECU\n" "CM_ SG_ 1 Value \"Signal comment\";\nVAL_ 1 Value 1 \"On\";\n"; const std::vector statements = { "SIG_VALTYPE_ 1 Value : 1;", "SIG_VALTYPE_ 1 Value : 2;", "BA_ \"SignalType\" SG_ 1 Value 1;", "BA_DEF_ SG_ \"SignalType\" INT 0 1;", "BA_DEF_DEF_ \"SignalType\" 0;", "BA_DEF_REL_ BU_SG_REL_ \"Relation\" INT 0 1;", "BA_REL_ \"Relation\" BU_SG_REL_ ECU SG_ 1 Value 1;", "SGTYPE_ Type : 8@1+ (1,0) [0|255] \"\" 0, Table;", "SIG_TYPE_REF_ 1 Value : Type;", "EV_ Environment : 0 [0|1] \"\" 0 1 DUMMY_NODE_VECTOR0 ECU;", "ENVVAR_DATA_ Environment : 4;", "VAL_TABLE_ Table 0 \"Off\" 1 \"On\";", "BO_TX_BU_ 1 : ECU;", "VENDOR_EXTENSION_ data;", "UNKNOWN_" }; for (const std::string &statement : statements) { fixture.Write(definitions + statement + '\n'); const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); CHECK(!result.HasErrors(), statement); QCOMPARE(result.diagnostics.size(), std::size_t(1U)); const DbcDiagnostic &warning = result.diagnostics[0]; CHECK(warning.severity == DbcDiagnosticSeverity::Warning && warning.code == DbcDiagnosticCode::UnsupportedStatement, statement); QCOMPARE(warning.filePath, fixture.path.string()); QCOMPARE(warning.lineNumber, std::size_t(5U)); const std::string keyword = statement.substr(0U, statement.find(' ')); CHECK(warning.message.find("'" + keyword + "'") != std::string::npos, statement); const DecodeDatabase runtime = DbcDecodeBuilder().Build(result.database); RawCanFrame raw; raw.canId = 1U; raw.data = {1U}; const DecodedFrameValue decoded = DbcDecoder().Decode(runtime, raw); CHECK(decoded.valid && decoded.signals.size() == 1U && decoded.signals[0].valid, "Warnings preserve supported signal decoding"); QCOMPARE(decoded.signals[0].physicalValue, 12.0); CHECK(decoded.signals[0].GetValueDescription() && *decoded.signals[0].GetValueDescription() == "On", "Warnings preserve VAL_ descriptions"); QCOMPARE(runtime.GetFrames()[0].signals[0].comment, std::string("Signal comment")); } } void DbcParserTest::diagnosticNamespaceBoundaries() { Fixture fixture; const std::vector endings = { "BS_\n", "BS_:\n", "BS_ :\n", "BU_\n", "BU_:\n", "BU_ : ECU\n", "BO_ 1 Frame: 1 ECU\n", "VENDOR_EXTENSION_ data;\n" }; for (const std::string &ending : endings) { fixture.Write("NS_ :\n\tSG_MUL_VAL_\n\nBA_\n" + ending + "SIG_VALTYPE_\n"); const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); CHECK(!result.HasErrors(), ending); const std::size_t expectedWarnings = ending.find("VENDOR_") == 0U ? 2U : 1U; QCOMPARE(result.diagnostics.size(), expectedWarnings); QCOMPARE(result.diagnostics.back().lineNumber, std::size_t(6U)); CHECK(result.diagnostics.back().message.find("SIG_VALTYPE_") != std::string::npos, "Unsupported keyword outside NS_ must produce a warning"); } for (const std::string &definition : {std::string("BO_"), std::string("SG_")}) { fixture.Write("NS_:\n\tBA_\n\t" + definition + '\n'); const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); CHECK(result.HasErrors(), "Malformed definition ends NS_ instead of being hidden"); QCOMPARE(result.diagnostics.size(), std::size_t(1U)); QCOMPARE(result.diagnostics[0].lineNumber, std::size_t(3U)); } } void DbcParserTest::diagnosticFormatting() { Fixture fixture; fixture.path += " with spaces"; const DbcParser parser; for (const std::string &text : {std::string(), std::string(" \t\r\n\n")}) { fixture.Write(text); const DbcParseResult empty = parser.ParseFileWithDiagnostics(fixture.path.string()); CHECK(!empty.HasErrors() && empty.diagnostics.empty() && empty.database.frames.empty(), "Empty and whitespace-only inputs are valid"); } fixture.Write("\r\n\tVENDOR_EXTENSION_ data;\r\n\r\nUNKNOWN_"); const std::string relativePath = std::filesystem::relative(fixture.path).string(); const DbcParseResult warnings = parser.ParseFileWithDiagnostics(relativePath); CHECK(!warnings.HasErrors() && warnings.database.frames.empty(), "Warnings-only input"); QCOMPARE(warnings.diagnostics.size(), std::size_t(2U)); QCOMPARE(warnings.diagnostics[0].lineNumber, std::size_t(2U)); QCOMPARE(warnings.diagnostics[1].lineNumber, std::size_t(4U)); for (const DbcDiagnostic &warning : warnings.diagnostics) QCOMPARE(warning.filePath, relativePath); fixture.Write("UNKNOWN_ first;\nBO_ invalid Frame: 1 ECU\n" "UNKNOWN_ after_error;\nSG_ broken\n"); const DbcParseResult stopped = parser.ParseFileWithDiagnostics(relativePath); CHECK(stopped.HasErrors() && stopped.database.frames.empty(), "First error stops parsing"); QCOMPARE(stopped.diagnostics.size(), std::size_t(2U)); QCOMPARE(stopped.diagnostics[0].lineNumber, std::size_t(1U)); QCOMPARE(stopped.diagnostics[1].lineNumber, std::size_t(2U)); QCOMPARE(stopped.diagnostics[1].filePath, relativePath); fixture.Write("VAL_ 1 Missing 0 \"Off\";\nBO_ 1 Frame: 1 ECU\nUNKNOWN_ data;\n"); const DbcParseResult deferred = parser.ParseFileWithDiagnostics(relativePath); CHECK(deferred.HasErrors() && deferred.database.frames.empty(), "Deferred error rejects database"); QCOMPARE(deferred.diagnostics.size(), std::size_t(2U)); CHECK(deferred.diagnostics[0].severity == DbcDiagnosticSeverity::Warning && deferred.diagnostics[1].severity == DbcDiagnosticSeverity::Error, "Deferred errors follow warnings in detection order"); QCOMPARE(deferred.diagnostics[0].lineNumber, std::size_t(3U)); QCOMPARE(deferred.diagnostics[1].lineNumber, std::size_t(1U)); QCOMPARE(deferred.diagnostics[1].filePath, relativePath); fixture.Write("BO_ 1 Frame: 1 ECU"); const DbcParseResult recovered = parser.ParseFileWithDiagnostics(relativePath); CHECK(!recovered.HasErrors() && recovered.diagnostics.empty() && recovered.database.frames.size() == 1U, "Same parser has clean state after errors and accepts missing final newline"); } void DbcParserTest::validFrames() { Fixture fixture; struct ValidCase { const char *line; std::uint32_t id; bool extended; unsigned length; }; const ValidCase valid[] = { {"BO_ 0 Frame: 0 ECU", 0U, false, 0U}, {"BO_ 2047 Frame: 8 ECU", 2047U, false, 8U}, {"BO_ 2048 Frame: 64 ECU", 2048U, true, 64U}, {"BO_ 536870911 Frame: 255 ECU", 0x1FFFFFFFU, true, 255U}, {"BO_ 2147483649 Frame: 8 ECU", 1U, true, 8U}, {"BO_ 2684354559 Frame: 8 ECU", 0x1FFFFFFFU, true, 8U}, {" BO_\t256\tFrame :\t8\tECU \r", 256U, false, 8U}}; for (const ValidCase &test: valid) { fixture.Write(std::string(test.line) + '\n'); try { const DbcDatabase database = DbcParser().ParseFile(fixture.path.string()); QCOMPARE(database.frames.size(), std::size_t(1U)); const FrameInfo &frame = database.frames[0]; QCOMPARE(frame.name, std::string("Frame")); QCOMPARE(frame.canId, test.id); QCOMPARE(frame.isExtended, test.extended); QCOMPARE(static_cast(frame.dlc), test.length); QCOMPARE(frame.transmitter, std::string("ECU")); } catch (const std::exception &error) { CHECK(false, error.what()); } } } void DbcParserTest::invalidFrames() { Fixture fixture; const char *invalid[] = { "BO_ invalid", "BO_", "BO_ 1", "BO_ 1 : 8 ECU", "BO_ 1 Frame 8 ECU", "BO_ 1 Frame:", "BO_ 1 Frame: 8", "BO_ 1 Frame: 8 ECU extra", "BO_ 1 Two Names: 8 ECU", "BO_ -1 Frame: 8 ECU", "BO_ +1 Frame: 8 ECU", "BO_ 1x Frame: 8 ECU", "BO_ 0x123 Frame: 8 ECU", "BO_ 1.5 Frame: 8 ECU", "BO_ 4294967296 Frame: 8 ECU", "BO_ 999999999999999999999 Frame: 8 ECU", "BO_ 536870912 Frame: 8 ECU", "BO_ 1073741824 Frame: 8 ECU", "BO_ 2684354560 Frame: 8 ECU", "BO_ 4294967295 Frame: 8 ECU", "BO_ 1 Frame: -1 ECU", "BO_ 1 Frame: +8 ECU", "BO_ 1 Frame: 8x ECU", "BO_ 1 Frame: 8.5 ECU", "BO_ 1 Frame: 256 ECU", "BO_ 1 Frame: 4294967296 ECU", "BO_\tinvalid"}; for (const char *line: invalid) { fixture.Write(std::string("VERSION \"test\"\n\n") + line + '\n'); try { DbcParser().ParseFile(fixture.path.string()); CHECK(false, line); } catch (const std::runtime_error &error) { const std::string prefix = fixture.path.string() + ":3: "; CHECK(std::string(error.what()).find(prefix) == 0U && std::string(error.what()).size() > prefix.size(), line); } } } void DbcParserTest::duplicateFrames() { Fixture fixture; struct DuplicateCase { const char *firstId; const char *duplicateId; const char *message; }; const DuplicateCase cases[] = { {"1", "1", "Duplicate standard frame CAN ID: 1"}, {"2147483649", "2147483649", "Duplicate extended frame CAN ID: 1"}, {"2048", "2147485696", "Duplicate extended frame CAN ID: 2048"}, {"2147485696", "2048", "Duplicate extended frame CAN ID: 2048"}, {"536870911", "2684354559", "Duplicate extended frame CAN ID: 536870911"} }; const DbcParser parser; for (const DuplicateCase &test : cases) { fixture.Write("BO_ " + std::string(test.firstId) + " First: 8 ECU\n" "BO_ 2 Other: 8 ECU\n\n" " BO_\t" + test.duplicateId + "\tSecond: 8 ECU\n"); try { parser.ParseFile(fixture.path.string()); CHECK(false, "Duplicate frame must be rejected"); } catch (const std::runtime_error &error) { QCOMPARE(std::string(error.what()), fixture.path.string() + ":4: " + test.message); } } fixture.Write("BO_ 1 First: 8 ECU\n"); QCOMPARE(parser.ParseFile(fixture.path.string()).frames.size(), std::size_t(1U)); } void DbcParserTest::duplicateSignals() { Fixture fixture; const std::string first = "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"; const std::string duplicates[] = { first, " SG_\tState : 16|8@1+ (2,1) [0|511] \"V\" ECU\n", "SG_ State m1 : 16|8@1+ (1,0) [0|255] \"\" ECU\n" }; for (const std::string &duplicate : duplicates) { fixture.Write("BO_ 1 Frame: 8 ECU\n" + first + "SG_ Other : 8|8@1+ (1,0) [0|255] \"\" ECU\n\n" + duplicate); try { DbcParser().ParseFile(fixture.path.string()); CHECK(false, "Duplicate signal must be rejected"); } catch (const std::runtime_error &error) { QCOMPARE(std::string(error.what()), fixture.path.string() + ":5: Duplicate signal name 'State' in frame 'Frame'."); } } } void DbcParserTest::distinctDefinitions() { Fixture fixture; fixture.Write("BO_ 1 Shared: 1 ECU\n" "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "BO_ 2147483649 Shared: 1 ECU\n" "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "BO_ 2 Shared: 2 ECU\n" "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n" "SG_ state : 8|8@1+ (1,0) [0|255] \"\" ECU\n" "CM_ BO_ 1 \"Standard\";\n" "CM_ BO_ 2147483649 \"Extended\";\n" "CM_ SG_ 1 State \"Standard state\";\n" "CM_ SG_ 2147483649 State \"Extended state\";\n"); const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string()); QCOMPARE(parsed.frames.size(), std::size_t(3U)); QCOMPARE(parsed.frames[0].canId, parsed.frames[1].canId); CHECK(!parsed.frames[0].isExtended && parsed.frames[1].isExtended, "Standard and extended frames with the same numeric ID remain distinct"); QCOMPARE(parsed.frames[0].comment, std::string("Standard")); QCOMPARE(parsed.frames[1].comment, std::string("Extended")); QCOMPARE(parsed.frames[0].signals[0].comment, std::string("Standard state")); QCOMPARE(parsed.frames[1].signals[0].comment, std::string("Extended state")); QCOMPARE(parsed.frames[2].signals.size(), std::size_t(2U)); const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed); RawCanFrame raw; raw.canId = 1U; raw.data = {7U}; const DecodedFrameValue standard = DbcDecoder().Decode(runtime, raw); QVERIFY(standard.valid); QCOMPARE(standard.definition->comment, std::string("Standard")); raw.isExtended = true; const DecodedFrameValue extended = DbcDecoder().Decode(runtime, raw); QVERIFY(extended.valid); QCOMPARE(extended.definition->comment, std::string("Extended")); } void DbcParserTest::comments() { Fixture fixture; fixture.Write( "CM_\tBO_\t1\t\"First\";\r\n" "CM_\tSG_\t1\tState\t\" Line one \r\n" "\r\n" "\tBO_ is text; \\\"quoted\\\" and \\\\slash\"\t;\r\n" "CM_ BO_ 2147483649 \"Extended\";\r\n" "CM_ SG_ 2147483649 State \"Extended signal\";\r\n" "BO_ 1 Standard: 1 ECU\r\n" "CM_ BO_ 1 \"Last\";\r\n" "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\r\n" "BO_ 2147483649 Extended: 1 ECU\r\n" "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU"); const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); QVERIFY(!result.HasErrors()); QVERIFY(result.diagnostics.empty()); QCOMPARE(result.database.frames.size(), std::size_t(2)); QCOMPARE(result.database.frames[0].comment, std::string("Last")); QCOMPARE(result.database.frames[0].signals[0].comment, std::string(" Line one \n\n\tBO_ is text; \"quoted\" and \\slash")); QCOMPARE(result.database.frames[1].comment, std::string("Extended")); QCOMPARE(result.database.frames[1].signals[0].comment, std::string("Extended signal")); const std::string ids[][2] = { {"0", "0"}, {"2047", "2047"}, {"2048", "2147485696"}, {"536870911", "2684354559"}, {"2684354559", "536870911"}}; for (const std::string(&id)[2]: ids) { fixture.Write("CM_ BO_ " + id[0] + " \"Frame\";\nCM_ SG_ " + id[0] + " State \"Signal\";\nBO_ " + id[1] + " Frame: 1 ECU\n" "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n"); const DbcDatabase database = DbcParser().ParseFile(fixture.path.string()); QCOMPARE(database.frames[0].comment, std::string("Frame")); QCOMPARE(database.frames[0].signals[0].comment, std::string("Signal")); } fixture.Write("CM_ BO_ 1 \"Line one\nLine two\";\n" "BO_ 1 Frame: 0 ECU\nUNKNOWN_ value\nBO_ bad Frame: 0 ECU\n"); const DbcParseResult following = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); QVERIFY(following.HasErrors()); QCOMPARE(following.diagnostics.size(), std::size_t(2)); QCOMPARE(following.diagnostics[0].lineNumber, std::size_t(4)); QCOMPARE(following.diagnostics[1].lineNumber, std::size_t(5)); } void DbcParserTest::invalidComments_data() { QTest::addColumn("statement"); const char *ids[] = {"-1", "+1", "1x", "0x1", "4294967296", "536870912", "1073741824", "2684354560", "4294967295"}; for (const char *id: ids) { const std::string frameRow = std::string("frame-id-") + id; const std::string signalRow = std::string("signal-id-") + id; QTest::newRow(frameRow.c_str()) << QString::fromStdString("CM_ BO_ " + std::string(id) + " \"Text\";"); QTest::newRow(signalRow.c_str()) << QString::fromStdString("CM_ SG_ " + std::string(id) + " State \"Text\";"); } QTest::newRow("missing-id") << QStringLiteral("CM_ BO_"); QTest::newRow("missing-signal") << QStringLiteral("CM_ SG_ 1 \"Text\";"); QTest::newRow("missing-quote") << QStringLiteral("CM_ BO_ 1 Text;"); QTest::newRow("unclosed") << QStringLiteral("CM_ SG_ 1 State \"Text\n More"); QTest::newRow("invalid-escape") << QStringLiteral("CM_ BO_ 1 \"bad\\q\";"); QTest::newRow("dangling-escape") << QStringLiteral("CM_ BO_ 1 \"bad\\"); QTest::newRow("missing-semicolon") << QStringLiteral("CM_ SG_ 1 State \"Text\""); QTest::newRow("trailing-text") << QStringLiteral("CM_ BO_ 1 \"Text\"; garbage"); QTest::newRow("extra-header") << QStringLiteral("CM_ BO_ 1 extra \"Text\";"); QTest::newRow("extra-string") << QStringLiteral("CM_ BO_ 1 \"Text\" \"More\";"); QTest::newRow("missing-frame") << QStringLiteral("CM_ BO_ 2 \"Text\";"); QTest::newRow("missing-signal-frame") << QStringLiteral("CM_ SG_ 2 State \"Text\";"); QTest::newRow("missing-signal-reference") << QStringLiteral("CM_ SG_ 1 Missing \"Text\";"); QTest::newRow("different-frame-type") << QStringLiteral("CM_ BO_ 2147483649 \"Text\";"); QTest::newRow("missing-multiline-reference") << QStringLiteral("CM_ SG_ 1 Missing \"Text\nMore\";"); } void DbcParserTest::invalidComments() { QFETCH(QString, statement); Fixture fixture; fixture.Write("BO_ 1 Frame: 1 ECU\nSG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n\n" + statement.toStdString()); const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); QVERIFY(result.HasErrors()); QVERIFY(result.database.frames.empty()); QCOMPARE(result.diagnostics.size(), std::size_t(1)); const DbcDiagnostic &error = result.diagnostics[0]; QCOMPARE(error.severity, DbcDiagnosticSeverity::Error); QCOMPARE(error.code, DbcDiagnosticCode::InvalidStatement); QCOMPARE(error.filePath, fixture.path.string()); QCOMPARE(error.lineNumber, std::size_t(4)); QVERIFY(!error.message.empty()); try { DbcParser().ParseFile(fixture.path.string()); QFAIL("Malformed or unresolved comment must throw."); } catch (const std::runtime_error &exception) { QVERIFY(std::string(exception.what()).find(fixture.path.string() + ":4: ") == 0U); } } void DbcParserTest::integration() { Fixture fixture; fixture.Write( "BO_TX_BU_ 256 : ECU;\nBO_ 256 Frame: 8 ECU\n" " SG_ Speed : 0|16@1+ (0.5,1) [0|100] \"rpm\" ECU\n" "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Speed \"Signal comment\";\n" "BO_ 257 Second: 8 ECU\n"); const DbcDatabase database = DbcParser().ParseFile(fixture.path.string()); CHECK(database.frames.size() == 2U && database.frames[0].signals.size() == 1U && database.frames[0].comment == "Frame comment" && database.frames[0].signals[0].comment == "Signal comment", "Integration"); fixture.Write("\nSG_ Speed : invalid\n"); try { DbcParser().ParseFile(fixture.path.string()); CHECK(false, "Signal error location"); } catch (const std::runtime_error &error) { CHECK(std::string(error.what()).find(fixture.path.string() + ":2: ") == 0U, "Signal error location"); } } void DbcParserTest::multiplexing() { Fixture fixture; const std::string selector = "SG_ Mode M : 0|8@1+ (10,3) [0|255] \"\" ECU\n"; const std::string first = "SG_ First m1 : 8|8@1- (2,0) [-128|127] \"V\" ECU\n"; const std::string second = "SG_ Second m2 : 15|8@0+ (1,0) [0|255] \"\" ECU\n"; const std::string common = "SG_ Common : 16|8@1+ (1,0) [0|255] \"\" ECU\n"; fixture.Write("BO_ 1 Frame: 3 ECU\n" + first + second + common + selector + "VAL_ 1 First -1 \"Negative\";\n"); const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string()); const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed); CHECK(parsed.frames[0].signals[3].multiplexing.role == MultiplexRole::Selector, "Selector declared after branches"); const std::unique_ptr tree = DbcTreeBuilder().Build(parsed); CHECK(tree->GetChild(0U)->GetChild(0U)->GetSignal()->multiplexing.value == 1U, "Tree preserves multiplexing"); RawCanFrame raw; raw.canId = 1U; raw.data = {1U, 255U, 7U}; DecodedFrameValue result = DbcDecoder().Decode(runtime, raw); CHECK(result.signals[0].valid && std::get(result.signals[0].rawValue) == -1 && result.signals[0].physicalValue == -2 && *result.signals[0].GetValueDescription() == "Negative", "Active signed branch and VAL_ use exact raw selector"); CHECK(!result.signals[1].valid && result.signals[1].status == SignalDecodeStatus::Inactive && result.signals[1].GetValueDescription() == nullptr && result.signals[2].valid, "Inactive branch and unconditional signal"); raw.data[0] = 2U; result = DbcDecoder().Decode(runtime, raw); CHECK(result.signals[0].status == SignalDecodeStatus::Inactive && result.signals[1].valid && std::get(result.signals[1].rawValue) == 255U, "Motorola branch becomes active"); raw.data = {3U}; result = DbcDecoder().Decode(runtime, raw); CHECK(result.signals[0].status == SignalDecodeStatus::Inactive && result.signals[1].status == SignalDecodeStatus::Inactive && result.signals[2].status == SignalDecodeStatus::InsufficientPayload, "Unmapped selector leaves branches inactive even with missing bytes"); raw.data = {1U}; result = DbcDecoder().Decode(runtime, raw); CHECK(result.signals[0].status == SignalDecodeStatus::InsufficientPayload, "Active branch requires payload"); raw.data.clear(); result = DbcDecoder().Decode(runtime, raw); CHECK(result.signals[0].status == SignalDecodeStatus::SelectorUnavailable && result.signals[3].status == SignalDecodeStatus::InsufficientPayload, "Unavailable selector does not imply inactive branch"); const std::vector invalid = { first, selector + "SG_ Other M : 16|8@1+ (1,0) [0|255] \"\" ECU\n", "SG_ Mode M : 0|8@1- (1,0) [-128|127] \"\" ECU\n", selector + "SG_ TooLarge m256 : 8|8@1+ (1,0) [0|255] \"\" ECU\n", selector + first + "SG_ Same m1 : 15|8@0+ (1,0) [0|255] \"\" ECU\n", selector + "SG_ Collision m1 : 0|8@1+ (1,0) [0|255] \"\" ECU\n", selector + first + "SG_ Always : 8|8@1+ (1,0) [0|255] \"\" ECU\n", "SG_MUL_VAL_ 1 First Mode 1-2;\n" }; for (const std::string &signals : invalid) { fixture.Write("BO_ 1 Frame: 3 ECU\n" + signals); bool rejected = false; try { DbcParser().ParseFile(fixture.path.string()); } catch (const std::runtime_error &error) { rejected = std::string(error.what()).find(fixture.path.string() + ":") == 0U; } CHECK(rejected, "Invalid multiplexing rejected with location"); } for (const std::string marker : {"m", "m-1", "m+1", "m1M", "m18446744073709551616", "M m1"}) RejectSignal(fixture, "SG_ Bad " + marker + " : 8|8@1+ (1,0) [0|255] \"\" ECU"); } void DbcParserTest::multiplexingBoundaries() { Fixture fixture; fixture.Write("BO_ 1 Frame: 9 ECU\n" "SG_ Branch m18446744073709551615 : 64|8@1+ (1,0) [0|255] \"\" ECU\n" "SG_ Mode M : 7|64@0+ (0,5) [0|1] \"\" ECU\n"); const DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string()); DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed); CHECK(runtime.GetFrames()[0].signals[0].multiplexing.value == std::numeric_limits::max(), "Exact uint64 branch metadata"); RawCanFrame raw; raw.canId = 1U; raw.data = std::vector(9U, 255U); DecodedFrameValue result = DbcDecoder().Decode(runtime, raw); CHECK(result.signals[0].valid && result.signals[1].valid && std::get(result.signals[1].rawValue) == std::numeric_limits::max(), "Motorola uint64 selector compares raw value before zero scaling"); raw.data[7] = 254U; result = DbcDecoder().Decode(runtime, raw); CHECK(result.signals[0].status == SignalDecodeStatus::Inactive, "Adjacent large integers remain distinct"); std::vector definitions = runtime.GetFrames(); definitions[0].signals[1].length = 0U; QVERIFY_THROWS_EXCEPTION(std::invalid_argument, runtime = DecodeDatabase(definitions)); definitions[0].signals.pop_back(); QVERIFY_THROWS_EXCEPTION(std::invalid_argument, runtime = DecodeDatabase(definitions)); fixture.Write("BO_ 1 Frame: 2 ECU\n\n" "SG_ Branch m2 : 8|8@1+ (1,0) [0|255] \"\" ECU\n" "SG_ Mode M : 0|1@1+ (1,0) [0|1] \"\" ECU\n"); bool rejected = false; try { DbcParser().ParseFile(fixture.path.string()); } catch (const std::runtime_error &error) { rejected = std::string(error.what()).find(fixture.path.string() + ":3: ") == 0U; } CHECK(rejected, "Deferred validation reports branch source line"); fixture.Write("BO_ 1 Frame: 2 ECU\n" "SG_ A : 8|8@1+ (1,0) [0|255] \"\" ECU\n" "SG_ B : 15|8@0+ (1,0) [0|255] \"\" ECU\n"); rejected = false; try { DbcParser().ParseFile(fixture.path.string()); } catch (const std::runtime_error &) { rejected = true; } CHECK(rejected, "Ordinary overlapping signals rejected"); } void DbcParserTest::invalidSignals() { Fixture fixture; const std::string ordinary = SignalLine("0", "8", "1+", "1", "0", "0", "255"); for (const std::string value: {"-1", "+1", "0junk", "0x10", "1.5", "4294967296", "999999999999999999999"}) { RejectSignal(fixture, SignalLine(value, "8", "1+", "1", "0", "0", "255")); RejectSignal(fixture, SignalLine("0", value, "1+", "1", "0", "0", "255")); } for (const std::string length: {"0", "65"}) RejectSignal(fixture, SignalLine("0", length, "1+", "1", "0", "0", "255")); for (const std::string order: {"2+", "9-", "1", "1x", "1++", "01+"}) RejectSignal(fixture, SignalLine("0", "8", order, "1", "0", "0", "255")); for (const std::string value: {"1junk", "nan", "NaN", "inf", "-inf", "1e999", "1e-999", "0x1p2", "", "1e", "1.2.3"}) { RejectSignal(fixture, SignalLine("0", "8", "1+", value, "0", "0", "255")); RejectSignal(fixture, SignalLine("0", "8", "1+", "1", value, "0", "255")); RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", value, "255")); RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "0", value)); } RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "2", "1")); for (const std::string name: {"", "Two Names", "Value m1M"}) { std::string line = ordinary; line.replace(4U, 5U, name); RejectSignal(fixture, line); } for (const std::string broken: { "SG_", "SG_ Value", "SG_ Value : 0|8@1+ (1,0) [0|255] ECU", "SG_ Value : 0|8@1+ (1,0) [0|255] \"\"", "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU,", "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU extra"}) RejectSignal(fixture, broken); RejectSignal(fixture, SignalLine("64", "1", "1+", "1", "0", "0", "1")); RejectSignal(fixture, SignalLine("63", "2", "1+", "1", "0", "0", "3")); RejectSignal(fixture, SignalLine("56", "2", "0+", "1", "0", "0", "3")); RejectSignal(fixture, SignalLine("4294967295", "64", "1+", "1", "0", "0", "1")); RejectSignal(fixture, ordinary, 0U); for (const char delimiter: {'|', '@', '(', ')', '[', ']', '"'}) { std::string line = ordinary; line.erase(line.find(delimiter), 1U); RejectSignal(fixture, line); } } void DbcParserTest::validSignals() { Fixture fixture; const DbcParser signalParser; fixture.Write("BO_ 1 Frame: 8 ECU\n" + SignalLine("0", "8", "1+", "0", "0", "0", "0") + "\n" + SignalLine("8", "8", "1-", "-1", "0", "-255", "0", "Negative") + "\n"); const DbcDatabase factors = signalParser.ParseFile(fixture.path.string()); CHECK(factors.frames[0].signals.size() == 2U && factors.frames[0].signals[0].factor == 0.0 && factors.frames[0].signals[1].factor == -1.0, "Zero and negative factors and equal range boundaries"); struct SignalCase { const char *start; const char *length; const char *order; unsigned payload; }; const SignalCase validSignals[] = { {"0", "1", "1+", 1U}, {"7", "1", "0-", 1U}, {"0", "64", "1+", 8U}, {"7", "64", "0-", 8U}, {"63", "1", "1+", 8U}, {"56", "1", "0+", 8U}, {"3", "12", "0+", 2U}, {"2039", "1", "1+", 255U}}; for (const SignalCase &test: validSignals) { fixture.Write("BO_ 1 Frame: " + std::to_string(test.payload) + " ECU\n" + SignalLine(test.start, test.length, test.order, ".5e+1", "-2.5E-1", "-1.", "+2e2") + "\n"); try { const DbcDatabase parsed = signalParser.ParseFile(fixture.path.string()); const SignalInfo &signal = parsed.frames.at(0).signals.at(0); QCOMPARE(signal.startBit, static_cast(std::stoul(test.start))); QCOMPARE(signal.length, static_cast(std::stoul(test.length))); QCOMPARE(signal.isLittleEndian, test.order[0] == '1'); QCOMPARE(signal.isSigned, test.order[1] == '-'); QCOMPARE(signal.factor, 5.0); QCOMPARE(signal.offset, -0.25); QCOMPARE(signal.minimum, -1.0); QCOMPARE(signal.maximum, 200.0); } catch (const std::exception &error) { CHECK(false, error.what()); } } fixture.Write("BO_ 1 Frame: 8 ECU\n" "SG_\tValue\t:\t0 | 8 @1+ ( 1 , 0 ) [ 0 | 255 ] \"rpm\" ECU1,\tECU2\n"); try { const DbcDatabase parsed = signalParser.ParseFile(fixture.path.string()); const SignalInfo &signal = parsed.frames.at(0).signals.at(0); CHECK(signal.name == "Value" && signal.unit == "rpm" && signal.receivers == std::vector({"ECU1", "ECU2"}), "Signal tabs, spaces, and receiver list"); } catch (const std::exception &error) { CHECK(false, error.what()); } } void DbcParserTest::CheckPhysicalNumbers() { Fixture fixture; struct PhysicalCase { const char *factor; const char *offset; const char *minimum; const char *maximum; double expectedFactor; double expectedOffset; double expectedMinimum; double expectedMaximum; }; const PhysicalCase valid[] = { {"+1.25", "-0.5", "-2.75", "255.5", 1.25, -0.5, -2.75, 255.5}, {"+.5e+1", "-2.5E-1", "-1.", "+2e2", 5.0, -0.25, -1.0, 200.0}, {"0e-999", "+0.", "-.0", ".0e+999", 0.0, 0.0, 0.0, 0.0}, {"1.7976931348623157e308", "2.2250738585072014e-308", "-1e308", "+1e308", std::numeric_limits::max(), std::numeric_limits::min(), -1e308, 1e308}}; for (const PhysicalCase &test: valid) { fixture.Write("BO_ 1 Frame: 1 ECU\n" + SignalLine("0", "8", "1+", test.factor, test.offset, test.minimum, test.maximum) + "\n"); const DbcParseResult result = DbcParser().ParseFileWithDiagnostics(fixture.path.string()); QVERIFY(!result.HasErrors()); QVERIFY(result.diagnostics.empty()); QCOMPARE(result.database.frames.size(), std::size_t(1)); const SignalInfo &signal = result.database.frames[0].signals[0]; QCOMPARE(signal.factor, test.expectedFactor); QCOMPARE(signal.offset, test.expectedOffset); QCOMPARE(signal.minimum, test.expectedMinimum); QCOMPARE(signal.maximum, test.expectedMaximum); } const char *invalid[] = {"1,5", "1.234,5", "1,234.5", "1junk", "nan", "inf", "-inf", "1e999", "1e-999", "1e-320", "-1e-320", "0x1p2", "1e", "1.2.3"}; for (const char *value: invalid) { RejectSignal(fixture, SignalLine("0", "8", "1+", value, "0", "0", "255")); RejectSignal(fixture, SignalLine("0", "8", "1+", "1", value, "0", "255")); RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", value, "255")); RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "0", value)); } RejectSignal(fixture, SignalLine("0", "8", "1+", "1.25", "0", "2.5", "1.5")); } void DbcParserTest::physicalNumbersCppLocale() { const ApplicationLocaleGuard guard; std::locale::global(std::locale(std::locale::classic(), new CommaDecimalPunctuation)); QCOMPARE(std::use_facet>(std::locale()).decimal_point(), ','); CheckPhysicalNumbers(); QCOMPARE(std::use_facet>(std::locale()).decimal_point(), ','); } void DbcParserTest::physicalNumbersCLocale() { const ApplicationLocaleGuard guard; bool found = false; const char *names[] = {"de_DE.UTF-8", "de_DE.utf8", "fr_FR.UTF-8", "fr_FR.utf8", "German_Germany.1252", "French_France.1252"}; for (const char *name: names) { if (std::setlocale(LC_NUMERIC, name) && std::string(std::localeconv()->decimal_point) == ",") { found = true; break; } } if (!found) QSKIP("No named comma-decimal C locale is installed; generate de_DE.UTF-8 and set LOCPATH."); QCOMPARE(std::string(std::localeconv()->decimal_point), std::string(",")); CheckPhysicalNumbers(); QCOMPARE(std::string(std::localeconv()->decimal_point), std::string(",")); } void DbcParserTest::dispatch() { CountingHandler::instances = 0; CountingHandler::calls = 0; DbcDatabase dispatchDatabase; DbcParseContext context{dispatchDatabase, nullptr}; DbcStatementDispatcher dispatcher; dispatcher.RegisterType("TEST_"); CHECK(dispatcher.Dispatch("TEST_ first", context) && dispatcher.Dispatch("TEST_ second", context) && CountingHandler::instances == 1 && CountingHandler::calls == 2 && dispatchDatabase.frames.size() == 2U, "Factory dispatch and handler reuse"); CHECK(!dispatcher.Dispatch("TEST_OTHER data", context) && !dispatcher.Dispatch("BO_TX_BU_ 1 : ECU;", context) && !dispatcher.Dispatch("VAL_TABLE_ States 0 \"Stopped\";", context) && CountingHandler::calls == 2, "Exact and unknown keyword dispatch"); } void DbcParserTest::independentSessions() { Fixture fixture; const DbcParser parser; fixture.Write("BO_ 1 First: 8 ECU\n"); CHECK(parser.ParseFile(fixture.path.string()).frames.size() == 1U, "First parse"); fixture.Write("SG_ Speed : invalid\n"); try { parser.ParseFile(fixture.path.string()); CHECK(false, "Independent parse context"); } catch (const std::runtime_error &error) { CHECK(std::string(error.what()).find("Signal found before any frame") != std::string::npos, "Independent parse context"); } fixture.Write("BO_ 2 Second: 8 ECU\n"); const DbcDatabase recovered = parser.ParseFile(fixture.path.string()); CHECK(recovered.frames.size() == 1U && recovered.frames[0].canId == 2U, "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 DbcDatabase parsed = DbcParser().ParseFile(fixture.path.string()); const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed); const std::unique_ptr 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.GetFrames()[index].signals[0].valueDescriptions == parsed.frames[index].signals[0].valueDescriptions, "All mappings survive both builders"); } CHECK(tree->GetChild(0)->GetChild(0)->GetSignal()->valueDescriptions == runtime.GetFrames()[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 DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0]; CHECK(decoded.valid && decoded.physicalValue == value * 2 + 10, "Numeric scaling unchanged"); const std::string *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 Boundary &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 DecodedSignalValue 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 Boundary &test: {Boundary{0x8000000000000000ULL, "High"}, Boundary{0xFFFFFFFFFFFFFFFFULL, "Full"}}) { for (unsigned byte = 0; byte < 8; ++byte) frame.data[byte] = static_cast(test.bits >> (byte * 8)); const DecodedSignalValue decoded = DbcDecoder().Decode(runtime, frame).signals[0]; CHECK(decoded.GetValueDescription() && *decoded.GetValueDescription() == test.description, "Unsigned boundaries"); } CHECK(FindValueDescription(runtime.GetFrames()[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 int 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", // Duplicate frame definition. "SG_ State : 0|8@1+ (1,0) [0|255] \"\" ECU\n",// Duplicate signal definition. ""}) { fixture.Write("VAL_ 1 " + std::string(suffix.empty() ? "Missing" : "State") + " 0 \"Description\";\n" + definitions + suffix); try { DbcParser().ParseFile(fixture.path.string()); CHECK(false, "Missing reference or duplicate definition"); } catch (const std::runtime_error &error) { const std::string line = suffix.empty() ? "1" : "4"; CHECK(std::string(error.what()).find(fixture.path.string() + ":" + line + ": ") == 0, "Error retains the reference or duplicate definition 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); } #include "dbc_parser_test.moc"