#include "dbc_parser.h" #include "dbc_statement_handler.h" #include #include #include #include #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 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(); } }; int main() { Fixture fixture; int failures = 0; std::size_t count = 0U; const auto check = [&](bool passed, const std::string &name) { ++count; if (!passed) { ++failures; std::cerr << "FAIL: " << name << '\n'; } }; 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()); } } 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); } } 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"); } const DbcParser signalParser; const auto 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"; }; const auto rejectSignal = [&](const std::string &line, unsigned payload = 8U) { fixture.Write("BO_ 1 Frame: " + std::to_string(payload) + " ECU\n\n" + line + "\n"); try { signalParser.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); } }; 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(signalLine(value, "8", "1+", "1", "0", "0", "255")); rejectSignal(signalLine("0", value, "1+", "1", "0", "0", "255")); } for (const std::string length: {"0", "65"}) rejectSignal(signalLine("0", length, "1+", "1", "0", "0", "255")); for (const std::string order: {"2+", "9-", "1", "1x", "1++", "01+"}) rejectSignal(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(signalLine("0", "8", "1+", value, "0", "0", "255")); rejectSignal(signalLine("0", "8", "1+", "1", value, "0", "255")); rejectSignal(signalLine("0", "8", "1+", "1", "0", value, "255")); rejectSignal(signalLine("0", "8", "1+", "1", "0", "0", value)); } rejectSignal(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(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(broken); rejectSignal(signalLine("64", "1", "1+", "1", "0", "0", "1")); rejectSignal(signalLine("63", "2", "1+", "1", "0", "0", "3")); rejectSignal(signalLine("56", "2", "0+", "1", "0", "0", "3")); rejectSignal(signalLine("4294967295", "64", "1+", "1", "0", "0", "1")); rejectSignal(ordinary, 0U); for (const char delimiter: {'|', '@', '(', ')', '[', ']', '"'}) { std::string line = ordinary; line.erase(line.find(delimiter), 1U); rejectSignal(line); } 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({"ECU1", "ECU2"}), "Signal tabs, spaces, and receiver list"); } catch (const std::exception &error) { check(false, error.what()); } 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_ 1 Speed 0 \"Stopped\";", context) && CountingHandler::calls == 2, "Exact and unknown keyword dispatch"); 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"); if (failures != 0) return 1; std::cout << "All " << count << " parser cases passed.\n"; return 0; }