From 1b72da3c0bac74aa0bbb907764027ac77c65154c Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Thu, 8 Oct 2026 02:20:22 -0600 Subject: [PATCH] Test number, curlConfig and isValidInstallId directly number is checked for shortest output, a JSON-grammar round trip over edge values, and independence from the program's global locale. curlConfig is pinned line for line, so a change to the options curl is given shows up as a test failure. isValidInstallId is checked at its length bounds and against characters outside [A-Za-z0-9_.-]. DBL_MAX is left out of the number test: it is written as 2e+308, which overflows (#7). Co-Authored-By: Claude Opus 5.5 --- test/test_trace_client.cpp | 97 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/test/test_trace_client.cpp b/test/test_trace_client.cpp index 9fead48..181211c 100644 --- a/test/test_trace_client.cpp +++ b/test/test_trace_client.cpp @@ -544,6 +544,100 @@ static void parseStatusReadsCurlsWriteOutOrReportsNoAnswer() { #endif } +// JSON's number grammar: -?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)? +static bool isJsonNumber(const std::string &text) { + std::size_t i = 0, n = text.size(); + if (i < n && text[i] == '-') ++i; + if (i < n && text[i] == '0') ++i; + else if (i < n && text[i] >= '1' && text[i] <= '9') { while (i < n && std::isdigit(static_cast(text[i]))) ++i; } + else return false; + if (i < n && text[i] == '.') { + std::size_t start = ++i; + while (i < n && std::isdigit(static_cast(text[i]))) ++i; + if (i == start) return false; + } + if (i < n && (text[i] == 'e' || text[i] == 'E')) { + ++i; + if (i < n && (text[i] == '+' || text[i] == '-')) ++i; + std::size_t start = i; + while (i < n && std::isdigit(static_cast(text[i]))) ++i; + if (i == start) return false; + } + return i == n; +} + +// A locale whose decimal point is a comma, as in much of Europe. +struct CommaDecimal : std::numpunct { + char do_decimal_point() const { return ','; } + char do_thousands_sep() const { return '.'; } + std::string do_grouping() const { return "\3"; } +}; + +static void numberIsTheShortestJsonDecimalThatReadsBack() { + using trace_client::detail::number; + CHECK_EQ(std::string("0.1"), number(0.1)); + CHECK_EQ(std::string("-3"), number(-3.0)); + CHECK_EQ(std::string("1.5"), number(1.5)); + CHECK_EQ(std::string("0.30000000000000004"), number(0.1 + 0.2)); + // DBL_MAX itself is left out: it is written as 2e+308, which overflows + // (issue #7). + const double values[] = {0.0, -0.0, 1.0, 100.0, 123456789.0, 1.0 / 3.0, 2.0 / 3.0, 1e21, 1e-7, + 5e-324, 1.7e308, -2.5e-10, 9007199254740993.0}; + for (std::size_t i = 0; i < sizeof values / sizeof values[0]; ++i) { + std::string text = number(values[i]); + CHECK(isJsonNumber(text)); + double back = std::strtod(text.c_str(), NULL); + CHECK_EQ(values[i], back); + } + // The program's global locale never reaches the body: no decimal comma, + // no thousands separator. + std::locale previous = std::locale::global(std::locale(std::locale::classic(), new CommaDecimal)); + CHECK_EQ(std::string("0.5"), number(0.5)); + CHECK_EQ(std::string("1234567.25"), number(1234567.25)); + std::locale::global(previous); +} + +static void curlConfigFixesEveryOptionAndQuotesEveryValue() { +#if !defined(TRACE_CLIENT_USE_LIBCURL) && !defined(__EMSCRIPTEN__) + trace_client::detail::Shared shared; + shared.endpoint = "https://trace.example.org/api/metrics"; + shared.key = "se\"cret"; + shared.application = "My\nGame"; + std::string expected = + "url = \"https://trace.example.org/api/metrics\"\n" + "proto = \"=http,https\"\n" + "header = \"Content-Type: application/json; charset=utf-8\"\n" + "header = \"Expect:\"\n" + "header = \"Authorization: Bearer se\\\"cret\"\n" + "user-agent = \"trace-client-cpp/" TRACE_CLIENT_VERSION " (MyGame)\"\n" + "data-binary = \"{\\\"name\\\":\\\"n\\\"}\"\n" + "max-time = 5\n" + "connect-timeout = 5\n" +#if defined(_WIN32) + "output = \"NUL\"\n" +#else + "output = \"/dev/null\"\n" +#endif + "write-out = \"%{http_code}\"\n"; + CHECK_EQ(expected, trace_client::detail::curlConfig(shared, "{\"name\":\"n\"}")); +#endif +} + +static void isValidInstallIdAcceptsOnlyTheDocumentedCharacters() { + using trace_client::detail::isValidInstallId; + CHECK(isValidInstallId("0f8b6c1e-3a52-4c8e-9a0d-6e2f1b7c4d90")); + CHECK(isValidInstallId("A_z.9-")); + CHECK(isValidInstallId("x")); + CHECK(isValidInstallId(std::string(trace_client::MAX_LENGTH, 'x'))); + CHECK(!isValidInstallId("")); + CHECK(!isValidInstallId(std::string(trace_client::MAX_LENGTH + 1, 'x'))); + const char *invalid[] = {" x", "x ", "a b", "a/b", "a\\b", "a:b", "a+b", "a\"b", "a\nb", "caf\xC3\xA9"}; + for (std::size_t i = 0; i < sizeof invalid / sizeof invalid[0]; ++i) { + CHECK(!isValidInstallId(invalid[i])); + } + CHECK(!isValidInstallId(std::string("a\0b", 3))); +} + static void nothingTheProgramPassesCanChangeWhereOrWhatCurlSends() { StubServer server; // A newline in any value would, unescaped, start a new curl option. @@ -1019,6 +1113,9 @@ int main() { {"curlQuoteKeepsEveryValueOnOneConfigLine", curlQuoteKeepsEveryValueOnOneConfigLine}, {"headerSafeDropsControlCharactersOnly", headerSafeDropsControlCharactersOnly}, {"parseStatusReadsCurlsWriteOutOrReportsNoAnswer", parseStatusReadsCurlsWriteOutOrReportsNoAnswer}, + {"numberIsTheShortestJsonDecimalThatReadsBack", numberIsTheShortestJsonDecimalThatReadsBack}, + {"curlConfigFixesEveryOptionAndQuotesEveryValue", curlConfigFixesEveryOptionAndQuotesEveryValue}, + {"isValidInstallIdAcceptsOnlyTheDocumentedCharacters", isValidInstallIdAcceptsOnlyTheDocumentedCharacters}, {"nothingTheProgramPassesCanChangeWhereOrWhatCurlSends", nothingTheProgramPassesCanChangeWhereOrWhatCurlSends}, {"aNewlineInTheBaseUrlCannotAddACurlOption", aNewlineInTheBaseUrlCannotAddACurlOption}, {"queueIsBoundedAndDropsRatherThanGrows", queueIsBoundedAndDropsRatherThanGrows},