DBC Framework
DBC parsing and CAN signal decoding framework
Loading...
Searching...
No Matches
dbc_parser_test.cpp
1#include "dbc_parser.h"
2#include "dbc_statement_handler.h"
3
4#include <QtTest>
5#include <chrono>
6#include <filesystem>
7#include <fstream>
8
9#define CHECK(condition, name) QVERIFY2((condition), std::string(name).c_str())
10#include <stdexcept>
11
12struct Fixture {
13 std::filesystem::path path = std::filesystem::temp_directory_path() /
14 ("dbc_parser_test_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".dbc");
15 ~Fixture() {
16 std::error_code error;
17 std::filesystem::remove(path, error);
18 }
19 void Write(const std::string &text) const {
20 std::ofstream output(path);
21 output << text;
22 if (!output)
23 throw std::runtime_error("Cannot write test fixture.");
24 }
25};
26
27class CountingHandler : public DbcStatementHandler {
28public:
29 inline static int instances = 0;
30 inline static int calls = 0;
31 CountingHandler() { ++instances; }
32 void Handle(const std::string &, DbcParseContext &context) const override {
33 ++calls;
34 context.database.frames.emplace_back();
35 }
36};
37
38std::string SignalLine(const std::string &start, const std::string &length,
39 const std::string &order, const std::string &factor,
40 const std::string &offset, const std::string &minimum,
41 const std::string &maximum) {
42 return "SG_ Value : " + start + "|" + length + "@" + order +
43 " (" + factor + "," + offset + ") [" + minimum + "|" + maximum + "] \"\" ECU";
44}
45
46class DbcParserTest : public QObject {
47 Q_OBJECT
48private:
49 void RejectSignal(const Fixture &fixture, const std::string &line, unsigned payload = 8U) {
50 fixture.Write("BO_ 1 Frame: " + std::to_string(payload) + " ECU\n\n" + line + "\n");
51 try {
52 DbcParser().ParseFile(fixture.path.string());
53 CHECK(false, "Reject " + line);
54 } catch (const std::runtime_error &error) {
55 const std::string prefix = fixture.path.string() + ":3: ";
56 CHECK(std::string(error.what()).find(prefix) == 0U &&
57 std::string(error.what()).size() > prefix.size(),
58 "Reject " + line);
59 }
60 }
61private Q_SLOTS:
62 void validFrames();
63 void invalidFrames();
64 void integration();
65 void invalidSignals();
66 void validSignals();
67 void dispatch();
68 void independentSessions();
69};
70
71void DbcParserTest::validFrames() {
72 Fixture fixture;
73 struct ValidCase {
74 const char *line;
75 std::uint32_t id;
76 bool extended;
77 unsigned length;
78 };
79 const ValidCase valid[] = {
80 {"BO_ 0 Frame: 0 ECU", 0U, false, 0U},
81 {"BO_ 2047 Frame: 8 ECU", 2047U, false, 8U},
82 {"BO_ 2048 Frame: 64 ECU", 2048U, true, 64U},
83 {"BO_ 536870911 Frame: 255 ECU", 0x1FFFFFFFU, true, 255U},
84 {"BO_ 2147483649 Frame: 8 ECU", 1U, true, 8U},
85 {"BO_ 2684354559 Frame: 8 ECU", 0x1FFFFFFFU, true, 8U},
86 {" BO_\t256\tFrame :\t8\tECU \r", 256U, false, 8U}};
87 for (const auto &test: valid) {
88 fixture.Write(std::string(test.line) + '\n');
89 try {
90 const auto database = DbcParser().ParseFile(fixture.path.string());
91 CHECK(database.frames.size() == 1U && database.frames[0].name == "Frame" &&
92 database.frames[0].canId == test.id &&
93 database.frames[0].isExtended == test.extended &&
94 database.frames[0].dlc == test.length &&
95 database.frames[0].transmitter == "ECU",
96 test.line);
97 } catch (const std::exception &error) { CHECK(false, error.what()); }
98 }
99}
100
101void DbcParserTest::invalidFrames() {
102 Fixture fixture;
103 const char *invalid[] = {
104 "BO_ invalid", "BO_", "BO_ 1", "BO_ 1 : 8 ECU", "BO_ 1 Frame 8 ECU",
105 "BO_ 1 Frame:", "BO_ 1 Frame: 8", "BO_ 1 Frame: 8 ECU extra",
106 "BO_ 1 Two Names: 8 ECU", "BO_ -1 Frame: 8 ECU", "BO_ +1 Frame: 8 ECU",
107 "BO_ 1x Frame: 8 ECU", "BO_ 0x123 Frame: 8 ECU", "BO_ 1.5 Frame: 8 ECU",
108 "BO_ 4294967296 Frame: 8 ECU", "BO_ 999999999999999999999 Frame: 8 ECU",
109 "BO_ 536870912 Frame: 8 ECU", "BO_ 1073741824 Frame: 8 ECU",
110 "BO_ 2684354560 Frame: 8 ECU", "BO_ 4294967295 Frame: 8 ECU",
111 "BO_ 1 Frame: -1 ECU", "BO_ 1 Frame: +8 ECU", "BO_ 1 Frame: 8x ECU",
112 "BO_ 1 Frame: 8.5 ECU", "BO_ 1 Frame: 256 ECU",
113 "BO_ 1 Frame: 4294967296 ECU", "BO_\tinvalid"};
114 for (const auto *line: invalid) {
115 fixture.Write(std::string("VERSION \"test\"\n\n") + line + '\n');
116 try {
117 DbcParser().ParseFile(fixture.path.string());
118 CHECK(false, line);
119 } catch (const std::runtime_error &error) {
120 const std::string prefix = fixture.path.string() + ":3: ";
121 CHECK(std::string(error.what()).find(prefix) == 0U &&
122 std::string(error.what()).size() > prefix.size(),
123 line);
124 }
125 }
126}
127
128void DbcParserTest::integration() {
129 Fixture fixture;
130 fixture.Write(
131 "BO_TX_BU_ 256 : ECU;\nBO_ 256 Frame: 8 ECU\n"
132 " SG_ Speed : 0|16@1+ (0.5,1) [0|100] \"rpm\" ECU\n"
133 "CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Speed \"Signal comment\";\n"
134 "BO_ 257 Second: 8 ECU\n");
135 const auto database = DbcParser().ParseFile(fixture.path.string());
136 CHECK(database.frames.size() == 2U && database.frames[0].signals.size() == 1U &&
137 database.frames[0].comment == "Frame comment" &&
138 database.frames[0].signals[0].comment == "Signal comment",
139 "Integration");
140 fixture.Write("\nSG_ Speed : invalid\n");
141 try {
142 DbcParser().ParseFile(fixture.path.string());
143 CHECK(false, "Signal error location");
144 } catch (const std::runtime_error &error) {
145 CHECK(std::string(error.what()).find(fixture.path.string() + ":2: ") == 0U,
146 "Signal error location");
147 }
148}
149
150void DbcParserTest::invalidSignals() {
151 Fixture fixture;
152 const std::string ordinary = SignalLine("0", "8", "1+", "1", "0", "0", "255");
153 for (const std::string value: {"-1", "+1", "0junk", "0x10", "1.5",
154 "4294967296", "999999999999999999999"}) {
155 RejectSignal(fixture, SignalLine(value, "8", "1+", "1", "0", "0", "255"));
156 RejectSignal(fixture, SignalLine("0", value, "1+", "1", "0", "0", "255"));
157 }
158 for (const std::string length: {"0", "65"})
159 RejectSignal(fixture, SignalLine("0", length, "1+", "1", "0", "0", "255"));
160 for (const std::string order: {"2+", "9-", "1", "1x", "1++", "01+"})
161 RejectSignal(fixture, SignalLine("0", "8", order, "1", "0", "0", "255"));
162 for (const std::string value: {"1junk", "nan", "NaN", "inf", "-inf", "1e999", "1e-999", "0x1p2", "", "1e", "1.2.3"}) {
163 RejectSignal(fixture, SignalLine("0", "8", "1+", value, "0", "0", "255"));
164 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", value, "0", "255"));
165 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", value, "255"));
166 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "0", value));
167 }
168 RejectSignal(fixture, SignalLine("0", "8", "1+", "1", "0", "2", "1"));
169 for (const std::string name: {"", "Two Names", "Value M", "Value m1", "Value m1M"}) {
170 std::string line = ordinary;
171 line.replace(4U, 5U, name);
172 RejectSignal(fixture, line);
173 }
174 for (const std::string broken: {
175 "SG_", "SG_ Value", "SG_ Value : 0|8@1+ (1,0) [0|255] ECU",
176 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\"",
177 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU,",
178 "SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU extra"})
179 RejectSignal(fixture, broken);
180 RejectSignal(fixture, SignalLine("64", "1", "1+", "1", "0", "0", "1"));
181 RejectSignal(fixture, SignalLine("63", "2", "1+", "1", "0", "0", "3"));
182 RejectSignal(fixture, SignalLine("56", "2", "0+", "1", "0", "0", "3"));
183 RejectSignal(fixture, SignalLine("4294967295", "64", "1+", "1", "0", "0", "1"));
184 RejectSignal(fixture, ordinary, 0U);
185 for (const char delimiter: {'|', '@', '(', ')', '[', ']', '"'}) {
186 std::string line = ordinary;
187 line.erase(line.find(delimiter), 1U);
188 RejectSignal(fixture, line);
189 }
190}
191
192void DbcParserTest::validSignals() {
193 Fixture fixture;
194 const DbcParser signalParser;
195 fixture.Write("BO_ 1 Frame: 8 ECU\n" +
196 SignalLine("0", "8", "1+", "0", "0", "0", "0") + "\n" +
197 SignalLine("8", "8", "1-", "-1", "0", "-255", "0") + "\n");
198 const auto factors = signalParser.ParseFile(fixture.path.string());
199 CHECK(factors.frames[0].signals.size() == 2U &&
200 factors.frames[0].signals[0].factor == 0.0 &&
201 factors.frames[0].signals[1].factor == -1.0,
202 "Zero and negative factors and equal range boundaries");
203
204 struct SignalCase {
205 const char *start;
206 const char *length;
207 const char *order;
208 unsigned payload;
209 };
210 const SignalCase validSignals[] = {
211 {"0", "1", "1+", 1U},
212 {"7", "1", "0-", 1U},
213 {"0", "64", "1+", 8U},
214 {"7", "64", "0-", 8U},
215 {"63", "1", "1+", 8U},
216 {"56", "1", "0+", 8U},
217 {"3", "12", "0+", 2U},
218 {"2039", "1", "1+", 255U}};
219 for (const auto &test: validSignals) {
220 fixture.Write("BO_ 1 Frame: " + std::to_string(test.payload) + " ECU\n" +
221 SignalLine(test.start, test.length, test.order, ".5e+1", "-2.5E-1", "-1.", "+2e2") + "\n");
222 try {
223 const auto parsed = signalParser.ParseFile(fixture.path.string());
224 const auto &signal = parsed.frames.at(0).signals.at(0);
225 CHECK(signal.startBit == std::stoul(test.start) &&
226 signal.length == std::stoul(test.length) &&
227 signal.isLittleEndian == (test.order[0] == '1') &&
228 signal.isSigned == (test.order[1] == '-') &&
229 signal.factor == 5.0 && signal.offset == -0.25 &&
230 signal.minimum == -1.0 && signal.maximum == 200.0,
231 "Valid signal boundary and numeric metadata");
232 } catch (const std::exception &error) { CHECK(false, error.what()); }
233 }
234 fixture.Write("BO_ 1 Frame: 8 ECU\n"
235 "SG_\tValue\t:\t0 | 8 @1+ ( 1 , 0 ) [ 0 | 255 ] \"rpm\" ECU1,\tECU2\n");
236 try {
237 const auto parsed = signalParser.ParseFile(fixture.path.string());
238 const auto &signal = parsed.frames.at(0).signals.at(0);
239 CHECK(signal.name == "Value" && signal.unit == "rpm" &&
240 signal.receivers == std::vector<std::string>({"ECU1", "ECU2"}),
241 "Signal tabs, spaces, and receiver list");
242 } catch (const std::exception &error) { CHECK(false, error.what()); }
243}
244
245void DbcParserTest::dispatch() {
246 CountingHandler::instances = 0;
247 CountingHandler::calls = 0;
248 DbcDatabase dispatchDatabase;
249 DbcParseContext context{dispatchDatabase, nullptr};
250 DbcStatementDispatcher dispatcher;
251 dispatcher.RegisterType<CountingHandler>("TEST_");
252 CHECK(dispatcher.Dispatch("TEST_ first", context) &&
253 dispatcher.Dispatch("TEST_ second", context) &&
254 CountingHandler::instances == 1 && CountingHandler::calls == 2 &&
255 dispatchDatabase.frames.size() == 2U,
256 "Factory dispatch and handler reuse");
257 CHECK(!dispatcher.Dispatch("TEST_OTHER data", context) &&
258 !dispatcher.Dispatch("BO_TX_BU_ 1 : ECU;", context) &&
259 !dispatcher.Dispatch("VAL_ 1 Speed 0 \"Stopped\";", context) &&
260 CountingHandler::calls == 2,
261 "Exact and unknown keyword dispatch");
262}
263
264void DbcParserTest::independentSessions() {
265 Fixture fixture;
266 const DbcParser parser;
267 fixture.Write("BO_ 1 First: 8 ECU\n");
268 CHECK(parser.ParseFile(fixture.path.string()).frames.size() == 1U, "First parse");
269 fixture.Write("SG_ Speed : invalid\n");
270 try {
271 parser.ParseFile(fixture.path.string());
272 CHECK(false, "Independent parse context");
273 } catch (const std::runtime_error &error) {
274 CHECK(std::string(error.what()).find("Signal found before any frame") !=
275 std::string::npos,
276 "Independent parse context");
277 }
278 fixture.Write("BO_ 2 Second: 8 ECU\n");
279 const auto recovered = parser.ParseFile(fixture.path.string());
280 CHECK(recovered.frames.size() == 1U && recovered.frames[0].canId == 2U,
281 "Fresh parse after error");
282}
283
284int RunParserTests(int argc, char **argv) {
285 DbcParserTest tests;
286 return QTest::qExec(&tests, argc, argv);
287}
288
289#include "dbc_parser_test.moc"
Minimal DBC parser.
Definition dbc_parser.h:33
DbcDatabase ParseFile(const std::string &filePath) const
Parse DBC file.
bool Dispatch(const std::string &line, DbcParseContext &context) const
Created: 2026-03-13 Author: Deeaitch (Dim. Himro)
Parsed DBC content stored in a simple internal form.
std::vector< FrameInfo > frames