Closes #19
This commit is contained in:
1 parent
d87c100398
commit
4ae4e527b8
5 files changed
+138
-10
No files matched your search
@@ -737,8 +737,11 @@ sawtooth traversal. This uses the stored payload length without introducing
|
|||||||
an additional CAN or CAN FD payload limit.
|
an additional CAN or CAN FD payload limit.
|
||||||
|
|
||||||
Factor, offset, minimum, and maximum accept signed decimal numbers, fractions,
|
Factor, offset, minimum, and maximum accept signed decimal numbers, fractions,
|
||||||
and scientific notation. The entire numeric token must be valid; hexadecimal
|
and scientific notation. Conversion is independent of the application's C and
|
||||||
numbers, NaN, infinity, conversion overflow, and conversion underflow are
|
C++ locales: the decimal separator is always a dot (`.`), and comma decimals
|
||||||
|
and digit grouping are rejected. The entire numeric token must be valid; hexadecimal
|
||||||
|
numbers, NaN, infinity, conversion overflow, and conversion underflow (including
|
||||||
|
subnormal values) are
|
||||||
rejected. Minimum must not exceed maximum. Zero and negative factors are allowed.
|
rejected. Minimum must not exceed maximum. Zero and negative factors are allowed.
|
||||||
|
|
||||||
Basic multiplexing markers `M` and `mN` are supported as described below.
|
Basic multiplexing markers `M` and `mN` are supported as described below.
|
||||||
|
|||||||
@@ -10,6 +10,16 @@ existing Git tag; subsequent changes are listed as unreleased.
|
|||||||
|
|
||||||
#### Fixed
|
#### Fixed
|
||||||
|
|
||||||
|
- Issue #19: parse signal factor, offset, minimum, and maximum independently of
|
||||||
|
C/C++ application locales with `std::from_chars`, retaining signed decimals,
|
||||||
|
scientific notation, complete-token validation, and range/finite checks.
|
||||||
|
- Added custom C++ and named C comma-decimal locale regressions for valid physical
|
||||||
|
values and invalid syntax, grouping, overflow/underflow, and reversed ranges.
|
||||||
|
Documented portable test coverage and Linux locale generation. Verified with
|
||||||
|
`qmake6`, `make -j4`, and `LOCPATH=/tmp/dbc-issue19-locales make check`: all six
|
||||||
|
suites passed without skips. The named C locale regression fails with the old
|
||||||
|
`std::stod` converter.
|
||||||
|
|
||||||
- Issue #18: frame/signal comments share frame-definition CAN ID validation and
|
- Issue #18: frame/signal comments share frame-definition CAN ID validation and
|
||||||
resolve after definitions are read. Support tabs, escaped quotes/backslashes,
|
resolve after definitions are read. Support tabs, escaped quotes/backslashes,
|
||||||
and multiline text while preserving whitespace and starting source locations.
|
and multiline text while preserving whitespace and starting source locations.
|
||||||
|
|||||||
@@ -35,14 +35,18 @@ namespace {
|
|||||||
R"(^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$)");
|
R"(^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$)");
|
||||||
if (!std::regex_match(text, number))
|
if (!std::regex_match(text, number))
|
||||||
throw std::runtime_error(std::string("Invalid ") + field + ": " + text);
|
throw std::runtime_error(std::string("Invalid ") + field + ": " + text);
|
||||||
std::size_t consumed = 0U;
|
// Floating-point from_chars is locale-independent and does not accept
|
||||||
double value;
|
// a leading '+', which the DBC numeric grammar permits.
|
||||||
try {
|
const char *begin = text.data();
|
||||||
value = std::stod(text, &consumed);
|
const char *end = begin + text.size();
|
||||||
} catch (const std::exception &) {
|
if (*begin == '+')
|
||||||
throw std::runtime_error(std::string("Invalid ") + field + ": " + text);
|
++begin;
|
||||||
}
|
double value = 0.0;
|
||||||
if (consumed != text.size() || !std::isfinite(value))
|
const std::from_chars_result parsed =
|
||||||
|
std::from_chars(begin, end, value, std::chars_format::general);
|
||||||
|
// Keep the existing rejection of conversion underflow into subnormal values.
|
||||||
|
if (parsed.ec != std::errc() || parsed.ptr != end || !std::isfinite(value) ||
|
||||||
|
std::fpclassify(value) == FP_SUBNORMAL)
|
||||||
throw std::runtime_error(std::string("Invalid ") + field + ": " + text);
|
throw std::runtime_error(std::string("Invalid ") + field + ": " + text);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,22 @@ extended frames may share a numeric ID, and separate frames may reuse signal nam
|
|||||||
Signal tests cover complete syntax and numeric validation, scientific notation,
|
Signal tests cover complete syntax and numeric validation, scientific notation,
|
||||||
finite physical parameters and ordered ranges, spaces and tabs, receiver lists,
|
finite physical parameters and ordered ranges, spaces and tabs, receiver lists,
|
||||||
unsupported multiplexing, and Intel/Motorola declared-payload boundaries.
|
unsupported multiplexing, and Intel/Motorola declared-payload boundaries.
|
||||||
|
Physical-number tests apply a custom comma-decimal C++ locale and an available
|
||||||
|
named comma-decimal C numeric locale. They check all four physical fields,
|
||||||
|
leading signs, fractions, scientific notation, finite boundaries, zero, malformed
|
||||||
|
numbers, comma/grouped numbers, overflow/underflow, and reversed ranges. Locale
|
||||||
|
changes are restored after each test. The named C locale case skips if none of
|
||||||
|
the supported locale names are installed; the custom C++ locale case always runs.
|
||||||
|
On Linux with locale sources and `localedef`, run both cases without installing a
|
||||||
|
system locale:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p /tmp/dbc-test-locales
|
||||||
|
localedef --no-archive -i de_DE -f UTF-8 /tmp/dbc-test-locales/de_DE.UTF-8
|
||||||
|
LOCPATH=/tmp/dbc-test-locales make check
|
||||||
|
```
|
||||||
|
|
||||||
|
Run `make check` from the build directory as usual.
|
||||||
Value-description tests cover full `VAL_` syntax, whitespace, escaped quotes,
|
Value-description tests cover full `VAL_` syntax, whitespace, escaped quotes,
|
||||||
empty text, exact integer boundaries, duplicate keys within and across statements,
|
empty text, exact integer boundaries, duplicate keys within and across statements,
|
||||||
deferred and normalized frame references, missing targets, error locations,
|
deferred and normalized frame references, missing targets, error locations,
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
#include "dbc_parser.h"
|
#include "dbc_parser.h"
|
||||||
#include "dbc_statement_handler.h"
|
#include "dbc_statement_handler.h"
|
||||||
#include "dbc_tree_builder.h"
|
#include "dbc_tree_builder.h"
|
||||||
|
#include <clocale>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
#include <locale>
|
||||||
|
|
||||||
#include <QtTest>
|
#include <QtTest>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -28,6 +30,27 @@ struct Fixture {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class CommaDecimalPunctuation : public std::numpunct<char> {
|
||||||
|
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 {
|
class CountingHandler : public DbcStatementHandler {
|
||||||
public:
|
public:
|
||||||
inline static int instances = 0;
|
inline static int instances = 0;
|
||||||
@@ -50,6 +73,7 @@ std::string SignalLine(const std::string &start, const std::string &length,
|
|||||||
class DbcParserTest : public QObject {
|
class DbcParserTest : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
private:
|
private:
|
||||||
|
void CheckPhysicalNumbers();
|
||||||
void RejectSignal(const Fixture &fixture, const std::string &line, unsigned payload = 8U) {
|
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");
|
fixture.Write("BO_ 1 Frame: " + std::to_string(payload) + " ECU\n\n" + line + "\n");
|
||||||
try {
|
try {
|
||||||
@@ -81,6 +105,8 @@ private Q_SLOTS:
|
|||||||
void integration();
|
void integration();
|
||||||
void invalidSignals();
|
void invalidSignals();
|
||||||
void validSignals();
|
void validSignals();
|
||||||
|
void physicalNumbersCppLocale();
|
||||||
|
void physicalNumbersCLocale();
|
||||||
void dispatch();
|
void dispatch();
|
||||||
void independentSessions();
|
void independentSessions();
|
||||||
void valueDescriptions();
|
void valueDescriptions();
|
||||||
@@ -824,6 +850,75 @@ void DbcParserTest::validSignals() {
|
|||||||
} catch (const std::exception &error) { CHECK(false, error.what()); }
|
} 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<double>::max(), std::numeric_limits<double>::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::numpunct<char>>(std::locale()).decimal_point(), ',');
|
||||||
|
CheckPhysicalNumbers();
|
||||||
|
QCOMPARE(std::use_facet<std::numpunct<char>>(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() {
|
void DbcParserTest::dispatch() {
|
||||||
CountingHandler::instances = 0;
|
CountingHandler::instances = 0;
|
||||||
CountingHandler::calls = 0;
|
CountingHandler::calls = 0;
|
||||||
|
|||||||
Reference in new issue
Block a user