Files
dbc/tests/dbc_parser_test.cpp
T
2026-10-13 08:17:49 -04:00

892 lines
44 KiB
C++

#include "dbc_decode_builder.h"
#include "dbc_decoder.h"
#include "dbc_parser.h"
#include "dbc_statement_handler.h"
#include "dbc_tree_builder.h"
#include <limits>
#include <QtTest>
#include <chrono>
#include <filesystem>
#include <fstream>
#define CHECK(condition, name) QVERIFY2((condition), std::string(name).c_str())
#include <stdexcept>
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 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 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 integration();
void invalidSignals();
void validSignals();
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<std::string> 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<std::string> 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<unsigned>(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::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<TreeNode> 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<std::int64_t>(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<std::uint64_t>(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<std::string> 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<std::uint64_t>::max(),
"Exact uint64 branch metadata");
RawCanFrame raw;
raw.canId = 1U;
raw.data = std::vector<std::uint8_t>(9U, 255U);
DecodedFrameValue result = DbcDecoder().Decode(runtime, raw);
CHECK(result.signals[0].valid && result.signals[1].valid &&
std::get<std::uint64_t>(result.signals[1].rawValue) == std::numeric_limits<std::uint64_t>::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<DecodeFrame> 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::uint32_t>(std::stoul(test.start)));
QCOMPARE(signal.length, static_cast<std::uint32_t>(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<std::string>({"ECU1", "ECU2"}),
"Signal tabs, spaces, and receiver list");
} catch (const std::exception &error) { CHECK(false, error.what()); }
}
void DbcParserTest::dispatch() {
CountingHandler::instances = 0;
CountingHandler::calls = 0;
DbcDatabase dispatchDatabase;
DbcParseContext context{dispatchDatabase, nullptr};
DbcStatementDispatcher dispatcher;
dispatcher.RegisterType<CountingHandler>("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<TreeNode> 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<std::uint8_t>(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<std::uint8_t>(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"