xo-umbrella2/xo-ppsink/utest/struct_scope.test.cpp

245 lines
8.4 KiB
C++

/** @file struct_scope.test.cpp
*
* exercise PpSink::struct_open() + struct_scope from
* xo/ppsink/pretty_struct.hpp -- the builder for a struct whose field count
* is only known at runtime.
*
* Assertions here are about the flat text and the *token stream*. Whether a
* forced break actually propagates through enclosing groups is a rendering
* question, and lives in xo-indentlog2/utest/struct_scope.test.cpp -- ppsink
* has no PrettySink.
**/
#include <xo/ppsink/pretty_struct.hpp>
#include <xo/ppsink/FlatSink.hpp>
#include <xo/ppsink/pretty_ostream.hpp>
#include "MarkSink.hpp"
#include <catch2/catch.hpp>
#include <sstream>
#include <string>
#include <vector>
namespace ut {
using xo::pp::FlatSink;
using xo::pp::field;
using std::stringstream;
/* MarkSink is declared in namespace ut by MarkSink.hpp */
namespace {
/** flat text produced by @p fn **/
template <typename Fn>
std::string flat_of(Fn && fn) {
stringstream ss;
FlatSink sink(ss.rdbuf());
fn(sink);
return ss.str();
}
/** token stream produced by @p fn **/
template <typename Fn>
std::string marks_of(Fn && fn) {
stringstream ss;
MarkSink sink(ss);
fn(sink);
return ss.str();
}
/** a struct-shaped type that renders itself with the builder,
* so nesting can be exercised
**/
struct Inner {
int x_ = 1;
};
} /*namespace*/
} /*namespace ut*/
namespace xo::pp {
template <>
struct Prettifier<ut::Inner> {
static void print(PpSink & sink, const ut::Inner & x) {
auto st = sink.struct_open("In");
st.field("x", x.x_);
}
};
} /*namespace xo::pp*/
namespace ut {
/* ------------------------------------------------------------------
* the load-bearing test: a fixed field list built through struct_open()
* must be indistinguishable from pretty_struct(), token for token.
* pretty_struct() is implemented in terms of struct_scope, so this pins
* that the wrapper did not change the shape.
*/
TEST_CASE("struct_scope-matches-pretty_struct", "[struct_scope]") {
const int a = 1;
const int b = 2;
auto via_builder = [&a, &b](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P");
st.field("a", a);
st.field("b", b);
};
auto via_variadic = [&a, &b](xo::pp::PpSink & sink) {
sink.pretty_struct("P", field("a", a), field("b", b));
};
REQUIRE(flat_of(via_builder) == flat_of(via_variadic));
REQUIRE(marks_of(via_builder) == marks_of(via_variadic));
/* and the value itself, so a shared regression cannot hide */
REQUIRE(flat_of(via_builder) == "<P :a 1 :b 2>");
}
TEST_CASE("struct_scope-no-fields", "[struct_scope]") {
REQUIRE(flat_of([](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P");
(void)st;
}) == "<P>");
}
/* a loop-contributed field must be indistinguishable from a variadic one:
* this is the whole point of the builder.
*/
TEST_CASE("struct_scope-loop-matches-variadic", "[struct_scope]") {
const std::vector<int> v = {10, 20, 30};
std::string looped = flat_of([&v](xo::pp::PpSink & sink) {
auto st = sink.struct_open("V");
for (std::size_t i = 0; i < v.size(); ++i)
st.field(std::string("[") + std::to_string(i) + "]", v[i]);
});
REQUIRE(looped == "<V :[0] 10 :[1] 20 :[2] 30>");
}
/* the closing ">" belongs INSIDE the group, before end(). Getting this
* wrong renders the ">" on its own line once the group breaks.
*/
TEST_CASE("struct_scope-token-stream", "[struct_scope]") {
const int a = 1;
REQUIRE(marks_of([&a](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P");
st.field("a", a);
}) == "<P<G><S 1,0><G 0>:a<S 1,1>1</G>></G>");
}
/* begin() must take its indent from the sink, not from the caller */
TEST_CASE("struct_scope-begin-takes-indent-from-sink", "[struct_scope]") {
std::string marks = marks_of([](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P");
(void)st;
});
REQUIRE(marks.find("<G>") != std::string::npos);
REQUIRE(marks.find("<G 2>") == std::string::npos);
}
/* ------------------------------------------------------------------
* force_break: separator becomes newline() rather than split().
*/
TEST_CASE("struct_scope-force-break-emits-newline", "[struct_scope]") {
const int a = 1;
const int b = 2;
auto build = [&a, &b](bool force) {
return [&a, &b, force](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P", force);
st.field("a", a);
st.field("b", b);
};
};
/* optional break: <S 1,0> separators, no <N> anywhere */
std::string relaxed = marks_of(build(false));
REQUIRE(relaxed.find("<S 1,0>") != std::string::npos);
REQUIRE(relaxed.find("<N ") == std::string::npos);
/* forced break: <N 0> separators, and no field-level <S 1,0> */
std::string forced = marks_of(build(true));
REQUIRE(forced.find("<N 0>") != std::string::npos);
REQUIRE(forced.find("<S 1,0>") == std::string::npos);
/* FlatSink renders a forced break as a hard newline even flat */
REQUIRE(flat_of(build(true)) == "<P\n:a 1\n:b 2>");
REQUIRE(flat_of(build(false)) == "<P :a 1 :b 2>");
}
/* absent fields drop the field AND its separator, as with pretty_struct */
TEST_CASE("struct_scope-absent-field-skipped", "[struct_scope]") {
const int a = 1;
const int b = 2;
REQUIRE(flat_of([&a, &b](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P");
st.field("a", a);
st.field("b", b, false /*present*/);
}) == "<P :a 1>");
/* no stray separator left behind by the absent field */
std::string marks = marks_of([&a, &b](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P");
st.field("a", a);
st.field("b", b, false /*present*/);
});
REQUIRE(marks == "<P<G><S 1,0><G 0>:a<S 1,1>1</G>></G>");
}
TEST_CASE("struct_scope-all-fields-absent", "[struct_scope]") {
const int a = 1;
REQUIRE(flat_of([&a](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P");
st.field("a", a, false /*present*/);
}) == "<P>");
}
/* item() accepts an already-built field-like, honouring present() */
TEST_CASE("struct_scope-item-accepts-field", "[struct_scope]") {
const int a = 1;
const int b = 2;
REQUIRE(flat_of([&a, &b](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P");
st.item(field("a", a));
st.item(field("b", b, false /*present*/));
}) == "<P :a 1>");
}
/* a generated name is a temporary: it must survive to the end of the
* field() call, which is all struct_scope needs (it renders immediately).
*/
TEST_CASE("struct_scope-generated-name-survives", "[struct_scope]") {
const int v = 7;
REQUIRE(flat_of([&v](xo::pp::PpSink & sink) {
auto st = sink.struct_open("P");
for (int i = 0; i < 2; ++i)
st.field(std::string("[") + std::to_string(i) + "]", v);
}) == "<P :[0] 7 :[1] 7>");
}
/* nesting: a field whose value is itself rendered with a builder.
* The inner begin()/end() must nest inside the outer group.
*/
TEST_CASE("struct_scope-nested", "[struct_scope]") {
const Inner in;
REQUIRE(flat_of([&in](xo::pp::PpSink & sink) {
auto outer = sink.struct_open("Out");
outer.field("a", 1);
outer.field("in", in);
}) == "<Out :a 1 :in <In :x 1>>");
/* inner group opens and closes inside the outer one */
std::string marks = marks_of([&in](xo::pp::PpSink & sink) {
auto outer = sink.struct_open("Out");
outer.field("in", in);
});
REQUIRE(marks == "<Out<G><S 1,0><G 0>:in<S 1,1><In<G><S 1,0>"
"<G 0>:x<S 1,1>1</G>></G></G>></G>");
}
} /*namespace ut*/
/* end struct_scope.test.cpp */