The repository had no licence at all, so every source was all-rights-reserved by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino Ferrari) and a two-line SPDX header to each of the 113 first-party sources -- 48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the grant travels with each file rather than relying on the root document alone. Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely vendored code is untouched: third_party/stb_truetype.h carries no header of ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py emit() now writes the two lines into its header comment and the file was regenerated -- gen_atlas.py --check passes and the regeneration adds only those comment lines to the previously committed file. README's provenance section now states MIT and links LICENSE. No code, test or public API change.
170 lines
7.6 KiB
C++
170 lines
7.6 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
#include "harness.hpp"
|
|
#include <plot/plot/ticks.hpp>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <limits>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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::size_t>(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<plot::Tick> 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<double>(t.size()) <= 500.0 / 60.0 + 2.0);
|
|
}
|
|
PLOT_TEST(ticks_are_monotonic_and_cover_range) {
|
|
std::vector<plot::Tick> 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<plot::Tick> 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<plot::Tick> 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<plot::Tick> 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<plot::Tick> 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<double>::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);
|
|
}
|
|
}
|