// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari #include "harness.hpp" #include #include #include #include #include #include #include namespace { /// Fails if any colon-separated field of a clock label reaches its radix: minutes /// and seconds must stay below 60 and hours below 24 (a value that rounds up to /// 24 h takes the day shape instead), so "1:60" or "24:00:00" cannot slip through. void check_no_field_reaches_its_radix(const std::string& label) { const std::size_t colons = static_cast(std::count(label.begin(), label.end(), ':')); if (colons == 0) return; // decimal shapes have no fields // Two colons mean h:mm:ss, one means m:ss: only the former starts with hours. const int first_radix = colons >= 2 ? 24 : 60; std::string clock = label; if (!clock.empty() && (clock[0] == '-' || clock[0] == '+')) clock.erase(0, 1); std::size_t start = 0; std::size_t field = 0; while (start <= clock.size()) { const std::size_t colon = clock.find(':', start); const std::string part = clock.substr(start, colon == std::string::npos ? std::string::npos : colon - start); if (!part.empty()) { const int value = std::atoi(part.c_str()); PLOT_CHECK(value >= 0 && value < (field == 0 ? first_radix : 60)); } if (colon == std::string::npos) break; start = colon + 1; ++field; } } } // namespace PLOT_TEST(nice_ticks_are_powers_of_ten_times_125) { std::vector t; plot::generate_ticks({0.0, 1.0}, /*px=*/500.0f, /*min_px=*/60.0f, t); PLOT_CHECK(!t.empty()); for (auto& tick : t) PLOT_CHECK(std::fabs(tick.value * 5.0 - std::round(tick.value * 5.0)) < 1e-9); // spacing of major ticks >= min_px PLOT_CHECK(static_cast(t.size()) <= 500.0 / 60.0 + 2.0); } PLOT_TEST(ticks_are_monotonic_and_cover_range) { std::vector t; plot::generate_ticks({-3.7, 91.2}, 800.0f, 60.0f, t); for (size_t i = 1; i < t.size(); ++i) PLOT_CHECK(t[i].value > t[i-1].value); PLOT_CHECK(t.front().value <= 0.0); PLOT_CHECK(t.back().value >= 90.0); } PLOT_TEST(si_formatting) { char b[32]; plot::format_si(0.0000015, b, sizeof b); PLOT_CHECK(std::string(b) == "1.5\xC2\xB5s" || std::string(b).find("1.5") == 0); plot::format_si(2.5e9, b, sizeof b); PLOT_CHECK(std::string(b).find("2.5") == 0); plot::format_si(1234.0, b, sizeof b); PLOT_CHECK(std::string(b).find("1.23") == 0); plot::format_si(0.0, b, sizeof b); PLOT_CHECK(std::string(b) == "0"); } PLOT_TEST(degenerate_ranges_do_not_hang) { std::vector t; plot::generate_ticks({5.0, 5.0}, 500.0f, 60.0f, t); // must not loop forever PLOT_CHECK(t.size() <= 64); plot::generate_ticks({0.0, 0.0}, 0.0f, 60.0f, t); plot::generate_ticks({1e300, 1e300}, 500.0f, 60.0f, t); } PLOT_TEST(hostile_tick_inputs_clear_the_output) { std::vector t{{1.0, true, "stale"}}; plot::generate_ticks({10.0, -10.0}, 500.0f, 60.0f, t); // inverted PLOT_CHECK(t.empty()); plot::generate_ticks({0.0, std::nan("")}, 500.0f, 60.0f, t); // non-finite max PLOT_CHECK(t.empty()); plot::generate_ticks({std::nan(""), 1.0}, 500.0f, 60.0f, t); PLOT_CHECK(t.empty()); plot::generate_ticks({0.0, 1.0}, 500.0f, 0.0f, t); // min_px <= 0 PLOT_CHECK(t.empty()); // Span overflows to infinity: the step would be inf, so nothing is emitted // and, crucially, the loop never runs. plot::generate_ticks({-1e308, 1e308}, 500.0f, 60.0f, t); PLOT_CHECK(t.empty()); } PLOT_TEST(minor_ticks_subdivide_and_the_total_is_capped) { std::vector t; plot::generate_ticks({0.0, 1.0}, 500.0f, 60.0f, t, /*include_minors=*/true); PLOT_CHECK(t.size() > 10); // minors really added for (size_t i = 1; i < t.size(); ++i) PLOT_CHECK(t[i].value > t[i-1].value); // The major step is 0.2 (leading digit 2), so minors are quartered at 0.05. for (const plot::Tick& tick : t) { const double scale = tick.major ? 5.0 : 20.0; PLOT_CHECK(std::fabs(tick.value * scale - std::round(tick.value * scale)) < 1e-9); PLOT_CHECK(tick.major ? !tick.label.empty() : tick.label.empty()); } // A range dense enough to want millions of minors stops at the hard cap. std::vector capped; plot::generate_ticks({0.0, 1e9}, 1e6f, 1.0f, capped, /*include_minors=*/true); PLOT_CHECK(capped.size() == plot::kMaxTicks); } PLOT_TEST(si_labels_are_utf8_and_never_overrun) { char b[32]; plot::format_si(0.0000015, b, sizeof b); PLOT_CHECK(std::string(b) == "1.5\xC2\xB5"); // U+00B5, not the Greek mu plot::format_si(1234.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1.23k"); plot::format_si(-1234.0, b, sizeof b); PLOT_CHECK(std::string(b) == "-1.23k"); plot::format_si(2.5e9, b, sizeof b); PLOT_CHECK(std::string(b) == "2.5G"); plot::format_si(1.5e-6, b, sizeof b, "s"); PLOT_CHECK(std::string(b) == "1.5\xC2\xB5s"); // a caller-supplied unit suffix plot::format_si(std::nan(""), b, sizeof b); // never silently dropped PLOT_CHECK(std::string(b) == "nan"); plot::format_si(std::numeric_limits::infinity(), b, sizeof b); PLOT_CHECK(std::string(b) == "inf"); // A short buffer truncates; the string is always terminated inside it and // nothing is ever written at or past `n`. for (plot::usize n = 1; n <= 8; ++n) { char small[8]; for (char& c : small) c = '#'; plot::format_si(-1234.567, small, n); PLOT_CHECK(std::strlen(small) < n); PLOT_CHECK(small[std::strlen(small)] == '\0'); } } PLOT_TEST(format_time_shapes) { char b[32]; plot::format_time(0.5, b, sizeof b); PLOT_CHECK(std::string(b) == "500ms"); plot::format_time(1.5, b, sizeof b); PLOT_CHECK(std::string(b) == "1.5s"); plot::format_time(90.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1:30"); plot::format_time(3661.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1:01:01"); plot::format_time(-90.0, b, sizeof b); PLOT_CHECK(std::string(b) == "-1:30"); plot::format_time(86400.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1d"); plot::format_time(std::nan(""), b, sizeof b); PLOT_CHECK(std::string(b) == "nan"); } PLOT_TEST(format_time_never_lets_a_field_reach_its_radix) { char b[32]; // Rounding the seconds leftover on its own used to print 119.5 s as "1:60"; // every field now comes from one rounded whole-second count. plot::format_time(119.5, b, sizeof b); PLOT_CHECK(std::string(b) == "2:00"); plot::format_time(3599.5, b, sizeof b); PLOT_CHECK(std::string(b) == "1:00:00"); plot::format_time(59.4999, b, sizeof b); PLOT_CHECK(std::string(b) == "59.5s"); plot::format_time(59.5, b, sizeof b); PLOT_CHECK(std::string(b) == "1:00"); plot::format_time(60.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1:00"); // At 24 h the day field appears, so the hour field never reaches 24. plot::format_time(86399.4, b, sizeof b); PLOT_CHECK(std::string(b) == "23:59:59"); plot::format_time(86399.5, b, sizeof b); PLOT_CHECK(std::string(b) == "1d"); plot::format_time(86400.0, b, sizeof b); PLOT_CHECK(std::string(b) == "1d"); // Sweep the boundary neighbourhoods and check every emitted field. const double values[] = {0.4, 0.9995, 1.0, 59.4, 59.4999, 59.5, 59.9, 60.0, 90.0, 119.5, 3599.4, 3599.5, 3599.9, 86399.4, 86399.5, 86399.9, 86400.0, 172800.0, -119.5, -3599.5, -86399.5}; for (const double v : values) { plot::format_time(v, b, sizeof b); const std::string label(b); PLOT_CHECK(!label.empty()); check_no_field_reaches_its_radix(label); } }