xo-umbrella2/xo-indentlog2/utest/group_fit.test.cpp

207 lines
7.3 KiB
C++

/** @file group_fit.test.cpp
*
* @author Roland Conybeare, Aug 2026
*
* Regression tests for PpState's group-fit computation.
*
* A group's width is established at its k_end token (PpState::end). It used
* to be measured as (scan totals now) - (PRINT totals now), but print totals
* say nothing about where the group started -- and printing is deferred until
* a group's fate is known, so they lag arbitrarily far behind. The effect was
* that a group's measured width included everything scanned before it even
* opened, so the deeper the nesting the more foreign content was counted.
*
* Symptoms, all covered below:
* - a record whose flat form fits the margin was broken anyway
* - structurally identical siblings rendered differently
* - output stopped responding to the margin once everything bottomed out
* - tags split ":name" from their value with room to spare
*
* The shape used here is the one that exposed it: an Outer with some scalar
* fields plus a vector of Inner, where Inner is itself a pretty_struct. That
* is the shape of xo-alloc's GcStatistics / ObjectStatistics, which is where
* this was first noticed.
**/
#include <xo/ppsink/pretty_struct.hpp>
#include <xo/ppsink/PrettyVector.hpp>
#include "print/PrettySink.hpp"
#include "print/toppstr.hpp"
#include <xo/arena/ArenaConfig.hpp>
#include <catch2/catch.hpp>
#include <algorithm>
#include <string>
#include <vector>
namespace ut {
namespace {
struct Inner {
int scanned_n_ = 0;
int scanned_z_ = 0;
};
struct Outer {
int total_ = 0;
int n_gc_ = 0;
std::vector<Inner> per_type_v_;
};
} /*namespace*/
} /*namespace ut*/
namespace xo::pp {
template <>
struct Prettifier<ut::Inner> {
static void print(PpSink & sink, const ut::Inner & x) {
sink.pretty_struct("Inner",
field("scanned_n", x.scanned_n_),
field("scanned_z", x.scanned_z_));
}
};
template <>
struct Prettifier<ut::Outer> {
static void print(PpSink & sink, const ut::Outer & x) {
sink.pretty_struct("Outer",
field("total", x.total_),
field("n_gc", x.n_gc_),
field("per_type_v", x.per_type_v_));
}
};
} /*namespace xo::pp*/
namespace ut {
using xo::pp::PrettySink;
using xo::pp::PpConfig;
using xo::pp::PpStyle;
using xo::pp::toppstr;
using xo::mm::ArenaConfig;
namespace {
/** render @p x at right margin @p margin.
*
* NB unique arena name per call: two PrettySinks sharing an
* ArenaConfig name interfere, and the symptom is wrong indentation in
* whichever runs second -- which looks exactly like a printer bug.
**/
template <typename T>
std::string
render(std::uint32_t margin, const T & x) {
return toppstr(PpConfig::scratch_plain(margin), x);
}
/** length of the longest line in @p s **/
std::size_t
longest_line(const std::string & s) {
std::size_t lo = 0, worst = 0;
for (std::size_t i = 0; i <= s.size(); ++i) {
if ((i == s.size()) || (s[i] == '\n')) {
worst = std::max(worst, i - lo);
lo = i + 1;
}
}
return worst;
}
Outer sample() {
Outer o;
o.total_ = 100;
o.n_gc_ = 3;
o.per_type_v_.push_back(Inner{1, 16});
o.per_type_v_.push_back(Inner{2, 32});
return o;
}
/* the flat rendering, for reference: 110 chars */
constexpr std::string_view c_flat
= "<Outer :total 100 :n_gc 3 :per_type_v"
" [<Inner :scanned_n 1 :scanned_z 16>,<Inner :scanned_n 2 :scanned_z 32>]>";
} /*namespace*/
TEST_CASE("group_fit-wide-margin-stays-flat", "[group_fit]") {
REQUIRE(render(200, sample()) == std::string(c_flat));
REQUIRE(c_flat.size() == 110);
}
TEST_CASE("group_fit-inner-group-that-fits-is-not-broken", "[group_fit]") {
/* THE regression test. At margin 80 the outer record (110 flat) must
* break, but the per_type_v vector is only ~72 chars and starts at
* column 3, so it fits and must stay on one line.
*
* Before the fix the vector's measured width included everything
* scanned before it, so it "did not fit" and every Inner broke.
*/
std::string out = render(80, sample());
REQUIRE(out.find("\n :total 100") != std::string::npos);
REQUIRE(out.find("[<Inner :scanned_n 1 :scanned_z 16>,"
"<Inner :scanned_n 2 :scanned_z 32>]") != std::string::npos);
}
TEST_CASE("group_fit-identical-siblings-render-identically", "[group_fit]") {
/* the two Inner values differ only in their numbers, so at any margin
* they must break the same way. Before the fix the second one broke
* harder than the first, because more content had been scanned by the
* time it opened.
*/
for (std::uint32_t margin : {60u, 30u}) {
std::string out = render(margin, sample());
INFO("margin " << margin << "\n" << out);
/* Each element runs from its "<Inner" to the next '>' -- Inner has
* no nested group, so the first '>' closes it. (Taking the LAST
* '>' would swallow the enclosing "]>" on the second element.)
*/
auto element_at = [&out](std::size_t from) {
std::size_t lo = out.find("<Inner", from);
REQUIRE(lo != std::string::npos);
std::size_t hi = out.find('>', lo);
REQUIRE(hi != std::string::npos);
return out.substr(lo, hi + 1 - lo);
};
/* digits differ between the two elements by construction; the
* layout must not
*/
auto strip_digits = [](std::string s) {
std::string r;
for (char c : s) { if (!std::isdigit((unsigned char)c)) r += c; }
return r;
};
std::string a = element_at(0);
std::string b = element_at(out.find("<Inner") + 1);
REQUIRE(strip_digits(a) == strip_digits(b));
}
}
TEST_CASE("group_fit-respects-the-margin", "[group_fit]") {
/* output must actually narrow as the margin narrows. Before the fix
* everything bottomed out: margins 40 and 30 produced identical text.
*/
std::size_t wide = longest_line(render(80, sample()));
std::size_t narrow = longest_line(render(30, sample()));
REQUIRE(wide > narrow);
REQUIRE(narrow <= 30);
}
TEST_CASE("group_fit-tag-keeps-name-with-value", "[group_fit]") {
/* ":scanned_n 1" is short; it must not be split across lines while
* there is room. Before the fix the value landed on its own line.
*/
std::string out = render(30, sample());
INFO(out);
REQUIRE(out.find(":scanned_n 1") != std::string::npos);
REQUIRE(out.find(":scanned_n\n") == std::string::npos);
}
} /*namespace ut*/
/* end group_fit.test.cpp */