415 lines
19 KiB
C++
415 lines
19 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) {
|
|
return "SG_ Value : " + 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 validFrames();
|
|
void invalidFrames();
|
|
void integration();
|
|
void invalidSignals();
|
|
void validSignals();
|
|
void dispatch();
|
|
void independentSessions();
|
|
void valueDescriptions();
|
|
void invalidValueDescriptions();
|
|
};
|
|
|
|
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 auto &test: valid) {
|
|
fixture.Write(std::string(test.line) + '\n');
|
|
try {
|
|
const auto database = DbcParser().ParseFile(fixture.path.string());
|
|
CHECK(database.frames.size() == 1U && database.frames[0].name == "Frame" &&
|
|
database.frames[0].canId == test.id &&
|
|
database.frames[0].isExtended == test.extended &&
|
|
database.frames[0].dlc == test.length &&
|
|
database.frames[0].transmitter == "ECU",
|
|
test.line);
|
|
} 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 auto *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::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 auto 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::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 M", "Value m1", "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") + "\n");
|
|
const auto 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 auto &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 auto parsed = signalParser.ParseFile(fixture.path.string());
|
|
const auto &signal = parsed.frames.at(0).signals.at(0);
|
|
CHECK(signal.startBit == std::stoul(test.start) &&
|
|
signal.length == std::stoul(test.length) &&
|
|
signal.isLittleEndian == (test.order[0] == '1') &&
|
|
signal.isSigned == (test.order[1] == '-') &&
|
|
signal.factor == 5.0 && signal.offset == -0.25 &&
|
|
signal.minimum == -1.0 && signal.maximum == 200.0,
|
|
"Valid signal boundary and numeric metadata");
|
|
} 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 auto parsed = signalParser.ParseFile(fixture.path.string());
|
|
const auto &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 auto 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 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<std::uint8_t>(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<std::uint8_t>(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);
|
|
}
|
|
|
|
#include "dbc_parser_test.moc"
|