xo-umbrella2/xo-ppsink/utest/Prettifier.test.cpp
Roland Conybeare 0781131497
All checks were successful
cmake-docker / cmake-build (clang, clang++) (push) Successful in 47m28s
cmake-docker / cmake-build (gcc, g++) (push) Successful in 41m49s
CI / smoke-test (push) Successful in 58m44s
xo-ppsink: print boolean as true/false [FEATURE]
2026-09-08 20:18:54 -04:00

167 lines
6 KiB
C++

/* @file Prettifier.test.cpp */
#include <xo/ppsink/FlatSink.hpp>
#include <xo/ppsink/pretty_ostream.hpp> /* Plain_PpsinkTest exercises the operator<< fallback */
#include <xo/ppsink/tostr0.hpp>
#include <catch2/catch.hpp>
#include <cstdint>
#include <limits>
#include <sstream>
using xo::pp::FlatSink;
using xo::pp::PpSink;
using xo::pp::Prettifier;
using xo::pp::pretty;
using xo::pp::has_prettifier;
using xo::pp::tostr0;
/* a type that opts in to Prettifier */
struct Point_PpsinkTest { int x; int y; };
/* a type that does NOT opt in -> pretty falls back to operator<< */
struct Plain_PpsinkTest { int v; };
inline std::ostream &
operator<<(std::ostream & os, const Plain_PpsinkTest & p) {
return os << "Plain{" << p.v << "}";
}
namespace xo::pp {
template <>
struct Prettifier<Point_PpsinkTest> {
static void print(PpSink & sink, const Point_PpsinkTest & p) {
sink.begin().put("(");
pretty(sink, p.x);
sink.put(",");
pretty(sink, p.y);
sink.put(")").end();
}
};
}
TEST_CASE("pretty-uses-specialization", "[pretty]") {
static_assert(has_prettifier<Point_PpsinkTest>);
std::stringstream ss;
FlatSink sink(ss.rdbuf());
pretty(sink, Point_PpsinkTest{1, 2}); /* begin/end are no-ops in FlatSink */
REQUIRE(ss.str() == "(1,2)");
}
TEST_CASE("pretty-falls-back-to-operator<<", "[pretty]") {
static_assert(!has_prettifier<Plain_PpsinkTest>);
std::stringstream ss;
FlatSink sink(ss.rdbuf());
pretty(sink, Plain_PpsinkTest{7}); /* stream_open -> operator<< */
REQUIRE(ss.str() == "Plain{7}");
}
/* Scalar leaves.
*
* Prettifier<int> used to be the only integer specialization, so every other
* width -- notably std::uint32_t, which is what a container's size() usually
* is -- fell through to the operator<< fallback: right answer, wrong path, and
* silently. These pin that widening as OUTPUT-NEUTRAL: each expectation below
* was observed BEFORE the specializations existed and is unchanged after.
*
* See .xo-backlog/xo-ppsink/issues/09-scalar-prettifiers.md
*/
TEST_CASE("prettifier-integer-widths", "[pretty][scalar]") {
static_assert(has_prettifier<short>);
static_assert(has_prettifier<unsigned short>);
static_assert(has_prettifier<int>);
static_assert(has_prettifier<unsigned int>);
static_assert(has_prettifier<long>);
static_assert(has_prettifier<unsigned long>);
static_assert(has_prettifier<long long>);
static_assert(has_prettifier<unsigned long long>);
/* the typedefs that motivated this: sizes and fixed-width ints */
static_assert(has_prettifier<std::size_t>);
static_assert(has_prettifier<std::uint32_t>);
static_assert(has_prettifier<std::int64_t>);
CHECK(tostr0(static_cast<short>(-5)) == "-5");
CHECK(tostr0(static_cast<unsigned short>(5)) == "5");
CHECK(tostr0(42) == "42");
CHECK(tostr0(4000000000u) == "4000000000");
CHECK(tostr0(-1234567890123L) == "-1234567890123");
CHECK(tostr0(1234567890123UL) == "1234567890123");
CHECK(tostr0(static_cast<std::size_t>(42)) == "42");
/* the extremes: buf[24] must hold the longest rendering of any width */
CHECK(tostr0(std::numeric_limits<std::int64_t>::min()) == "-9223372036854775808");
CHECK(tostr0(std::numeric_limits<std::uint64_t>::max()) == "18446744073709551615");
}
TEST_CASE("prettifier-bool-is-spelled-out", "[pretty][scalar]") {
static_assert(has_prettifier<bool>);
/* NOT 1/0. This departs from operator<< without std::boolalpha, which is
* where the digits came from: a bare 1 among neighbouring integer fields
* does not say which of them is a flag. Renderings pinned across the tree
* moved with it -- e.g. TypeDescr's ":complete true" -- on 2026-09-08.
*/
CHECK(tostr0(true) == "true");
CHECK(tostr0(false) == "false");
}
TEST_CASE("prettifier-leaves-char-types-alone", "[pretty][scalar]") {
/* char IS integral, so a bare std::integral constraint would have caught
* these and turned 'A' into "65" everywhere in xo. pp_number_integral
* excludes them on purpose; they keep rendering as CHARACTERS via
* operator<<.
*/
static_assert(!has_prettifier<char>);
static_assert(!has_prettifier<signed char>);
static_assert(!has_prettifier<unsigned char>);
/* std::int8_t/uint8_t are typedefs for these, hence also characters */
static_assert(!has_prettifier<std::int8_t>);
static_assert(!has_prettifier<std::uint8_t>);
CHECK(tostr0('A') == "A");
CHECK(tostr0(static_cast<signed char>(65)) == "A");
CHECK(tostr0(static_cast<unsigned char>(65)) == "A");
}
/* Pointers: byte-identical to operator<<(std::ostream&, const void*),
* except null, which xo renders "null".
*
* void* and const void* are checked separately because Prettifier<T> matches
* exactly: covering one does not cover the other. They were 307 and 52 of the
* 776 fallback instantiations measured 2026-08-16 (see
* .xo-backlog/xo-ppsink/issues/12-operator-fallback-inventory.md).
*/
TEST_CASE("prettifier-pointer", "[prettifier][pointer]") {
static_assert(xo::pp::has_prettifier<void *>);
static_assert(xo::pp::has_prettifier<const void *>);
int v = 7;
/* null is pinned below instead: xo renders it "null", so it is not
* expected to match operator<< on any host
*/
void * cases[] = { reinterpret_cast<void *>(1),
reinterpret_cast<void *>(0xff),
static_cast<void *>(&v) };
for (void * p : cases) {
const void * cp = p;
std::stringstream expect_p, expect_cp;
expect_p << p;
expect_cp << cp;
INFO("expect " + expect_p.str());
CHECK(tostr0(p) == expect_p.str());
CHECK(tostr0(cp) == expect_cp.str());
}
/* the shape, spelled out, so a change on either side is visible here */
CHECK(tostr0(static_cast<void *>(nullptr)) == "null");
CHECK(tostr0(static_cast<const void *>(nullptr)) == "null");
CHECK(tostr0(reinterpret_cast<void *>(0xff)) == "0xff");
}