370 lines
14 KiB
C++
370 lines
14 KiB
C++
/* file FopJson.test.cpp
|
|
*
|
|
* author: Roland Conybeare, Sep 2026
|
|
*/
|
|
|
|
#include "xo/printjson/PrintJson.hpp"
|
|
#include "xo/printjson/init_printjson.hpp"
|
|
#include <xo/reflectable2/FopTdx.hpp>
|
|
#include <xo/reflectable2/Reflectable.hpp>
|
|
#include <xo/facet/FacetRegistry.hpp>
|
|
/* NOT <xo/printable2/Printable.hpp>: that umbrella header pulls
|
|
* <xo/alloc2/Allocator.hpp>, which printable2 does not declare a dependency
|
|
* on. See .xo-backlog/xo-printable2/issues/03.
|
|
*/
|
|
#include <xo/printable2/detail/APrintable.hpp>
|
|
#include <xo/printable2/detail/IPrintable_Any.hpp>
|
|
#include <xo/printable2/detail/IPrintable_Xfer.hpp>
|
|
#include <xo/printable2/detail/RPrintable.hpp>
|
|
#include <xo/reflect/Reflect.hpp>
|
|
#include <xo/reflect/StructReflector.hpp>
|
|
#include <catch2/catch.hpp>
|
|
#include <sstream>
|
|
|
|
namespace xo {
|
|
using xo::json::PrintJson;
|
|
using xo::reflect::Reflect;
|
|
using xo::reflect::StructReflector;
|
|
using xo::reflect::TaggedPtr;
|
|
|
|
namespace ut {
|
|
InitEvidence s_fop_init_evidence = InitSubsys<S_printjson_tag>::require();
|
|
|
|
namespace {
|
|
/* throwaway representation; see xo-reflectable2/utest for the
|
|
* reflection-side assertions on the same shape
|
|
*/
|
|
struct DFopPoint {
|
|
double x_;
|
|
double y_;
|
|
};
|
|
|
|
class IReflectable_DFopPoint {
|
|
public:
|
|
static TaggedPtr self_tp(DFopPoint & self) {
|
|
return Reflect::make_tp(&self);
|
|
}
|
|
};
|
|
|
|
/* reflectable, but deliberately NEVER register_impl'd -- see
|
|
* print-obj-fast-path-skips-the-registry
|
|
*/
|
|
struct DFopUnregistered {
|
|
double v_;
|
|
};
|
|
|
|
class IReflectable_DFopUnregistered {
|
|
public:
|
|
static TaggedPtr self_tp(DFopUnregistered & self) {
|
|
return Reflect::make_tp(&self);
|
|
}
|
|
};
|
|
|
|
/* printable but deliberately NOT reflectable */
|
|
struct DFopOpaque {
|
|
int n_;
|
|
};
|
|
|
|
class IPrintable_DFopOpaque {
|
|
public:
|
|
using PpSink = xo::print::APrintable::PpSink;
|
|
|
|
static void pretty(const DFopOpaque & self, PpSink & sink) {
|
|
sink.put(self.n_ ? "DFopOpaque" : "DFopOpaque{}");
|
|
}
|
|
};
|
|
|
|
/* reflectable, holding an ERASED fop member -- the routine shape,
|
|
* see DDictionary/DList/DArray in xo-object2. Its member facet is
|
|
* APrintable so the member can be a representation that has not
|
|
* opted in to reflection.
|
|
*/
|
|
struct DFopBox {
|
|
xo::facet::obj<xo::print::APrintable> inner_;
|
|
};
|
|
|
|
class IReflectable_DFopBox {
|
|
public:
|
|
static TaggedPtr self_tp(DFopBox & self) {
|
|
return Reflect::make_tp(&self);
|
|
}
|
|
};
|
|
}
|
|
} /*namespace ut*/
|
|
|
|
namespace facet {
|
|
template <>
|
|
struct FacetImplementation<xo::reflect::AReflectable, xo::ut::DFopPoint> {
|
|
using ImplType
|
|
= xo::reflect::IReflectable_Xfer<xo::ut::DFopPoint,
|
|
xo::ut::IReflectable_DFopPoint>;
|
|
};
|
|
|
|
template <>
|
|
struct FacetImplementation<xo::reflect::AReflectable, xo::ut::DFopBox> {
|
|
using ImplType
|
|
= xo::reflect::IReflectable_Xfer<xo::ut::DFopBox,
|
|
xo::ut::IReflectable_DFopBox>;
|
|
};
|
|
|
|
template <>
|
|
struct FacetImplementation<xo::reflect::AReflectable,
|
|
xo::ut::DFopUnregistered> {
|
|
using ImplType
|
|
= xo::reflect::IReflectable_Xfer<xo::ut::DFopUnregistered,
|
|
xo::ut::IReflectable_DFopUnregistered>;
|
|
};
|
|
|
|
template <>
|
|
struct FacetImplementation<xo::print::APrintable, xo::ut::DFopOpaque> {
|
|
using ImplType
|
|
= xo::print::IPrintable_Xfer<xo::ut::DFopOpaque,
|
|
xo::ut::IPrintable_DFopOpaque>;
|
|
};
|
|
}
|
|
|
|
namespace ut {
|
|
using xo::reflect::AReflectable;
|
|
using xo::print::APrintable;
|
|
using xo::facet::obj;
|
|
|
|
namespace {
|
|
void require_foppoint_reflected() {
|
|
static bool s_once = []() {
|
|
/* the rotation is a runtime lookup: a FacetImplementation
|
|
* specialization alone does not register the pair
|
|
*/
|
|
using xo::facet::FacetRegistry;
|
|
|
|
FacetRegistry::register_impl<AReflectable, DFopPoint>();
|
|
FacetRegistry::register_impl<AReflectable, DFopBox>();
|
|
FacetRegistry::register_impl<APrintable, DFopOpaque>();
|
|
|
|
StructReflector<DFopPoint> sr;
|
|
REFLECT_MEMBER(sr, x);
|
|
REFLECT_MEMBER(sr, y);
|
|
sr.require_complete();
|
|
|
|
StructReflector<DFopBox> br;
|
|
REFLECT_MEMBER(br, inner);
|
|
br.require_complete();
|
|
|
|
/* DELIBERATELY no register_impl for DFopUnregistered */
|
|
StructReflector<DFopUnregistered> ur;
|
|
REFLECT_MEMBER(ur, v);
|
|
ur.require_complete();
|
|
|
|
return true;
|
|
}();
|
|
|
|
(void)s_once;
|
|
}
|
|
}
|
|
|
|
TEST_CASE("print-json-fop-object", "[printjson]") {
|
|
require_foppoint_reflected();
|
|
|
|
DFopPoint pt{1.5, -2.5};
|
|
obj<AReflectable, DFopPoint> o{&pt};
|
|
|
|
PrintJson print_json;
|
|
|
|
/* a fop prints as its representation does, the same way
|
|
* xo::ref::rp<T> prints as T does. Compare against printing the
|
|
* representation directly, rather than against a literal, so the
|
|
* claim survives a change to struct formatting.
|
|
*/
|
|
std::stringstream via_fop;
|
|
print_json.print(Reflect::make_tp(&o), &via_fop);
|
|
|
|
std::stringstream via_repr;
|
|
print_json.print(Reflect::make_tp(&pt), &via_repr);
|
|
|
|
REQUIRE(via_fop.str() == via_repr.str());
|
|
REQUIRE(via_fop.str().find("\"x\"") != std::string::npos);
|
|
REQUIRE(via_fop.str().find("1.5") != std::string::npos);
|
|
} /*TEST_CASE(print-json-fop-object)*/
|
|
|
|
TEST_CASE("print-json-erased-fop-object", "[printjson]") {
|
|
/* the routine case: D-types hold erased fop members as a matter of
|
|
* course, so this is the shape that matters, not the typed one
|
|
*/
|
|
require_foppoint_reflected();
|
|
|
|
DFopPoint pt{1.5, -2.5};
|
|
obj<AReflectable, DFopPoint> typed{&pt};
|
|
obj<AReflectable> erased{typed};
|
|
|
|
PrintJson print_json;
|
|
|
|
std::stringstream via_erased;
|
|
print_json.print(Reflect::make_tp(&erased), &via_erased);
|
|
|
|
std::stringstream via_repr;
|
|
print_json.print(Reflect::make_tp(&pt), &via_repr);
|
|
|
|
REQUIRE(via_erased.str() == via_repr.str());
|
|
} /*TEST_CASE(print-json-erased-fop-object)*/
|
|
|
|
TEST_CASE("print-json-empty-fop-object", "[printjson]") {
|
|
require_foppoint_reflected();
|
|
|
|
obj<AReflectable, DFopPoint> o;
|
|
|
|
PrintJson print_json;
|
|
|
|
std::stringstream ss;
|
|
print_json.print(Reflect::make_tp(&o), &ss);
|
|
|
|
/* print_generic_pointer emits json null when there are no
|
|
* children. It emitted "{}" until 2026-09-21 -- distinguishable
|
|
* from a real struct only by the ABSENT _name_ member, which asks
|
|
* a consumer to notice an absence.
|
|
*
|
|
* An empty fop is now spelled the same way an empty ObjectSlot
|
|
* already was, which matters because an ObjectSlot IS an erased
|
|
* fop: two spellings for one condition was the actual defect.
|
|
*/
|
|
REQUIRE(ss.str() == std::string("null"));
|
|
} /*TEST_CASE(print-json-empty-fop-object)*/
|
|
|
|
TEST_CASE("print-obj-matches-a-hand-built-tagged-ptr", "[printjson]") {
|
|
require_foppoint_reflected();
|
|
|
|
DFopPoint pt{1.5, -2.5};
|
|
obj<AReflectable, DFopPoint> o{&pt};
|
|
|
|
PrintJson print_json;
|
|
|
|
std::stringstream via_entry_point;
|
|
print_json.print_obj(o, &via_entry_point);
|
|
|
|
std::stringstream via_hand_built;
|
|
print_json.print_tp(Reflect::make_tp(&o), &via_hand_built);
|
|
|
|
REQUIRE(via_entry_point.str() == via_hand_built.str());
|
|
} /*TEST_CASE(print-obj-matches-a-hand-built-tagged-ptr)*/
|
|
|
|
TEST_CASE("print-obj-fast-path-agrees-with-the-generic-one", "[printjson]") {
|
|
/* obj<AReflectable> takes the if-constexpr fast path -- straight to
|
|
* self_tp(), no FacetRegistry probe and no pointer hop. The two
|
|
* routes reach the representation's TaggedPtr differently
|
|
* (self_tp() vs establish_most_derived_tp), so only a test says
|
|
* they agree.
|
|
*/
|
|
require_foppoint_reflected();
|
|
|
|
DFopPoint pt{1.5, -2.5};
|
|
obj<AReflectable, DFopPoint> typed{&pt};
|
|
obj<AReflectable> fast{typed};
|
|
|
|
PrintJson print_json;
|
|
|
|
std::stringstream via_fast;
|
|
print_json.print_obj(fast, &via_fast);
|
|
|
|
std::stringstream via_generic;
|
|
print_json.print_tp(Reflect::make_tp(&pt), &via_generic);
|
|
|
|
REQUIRE(via_fast.str() == via_generic.str());
|
|
} /*TEST_CASE(print-obj-fast-path-agrees-with-the-generic-one)*/
|
|
|
|
TEST_CASE("print-obj-renders-an-empty-fop-as-null", "[printjson]") {
|
|
/* the fast path's guard. An EMPTY obj<AReflectable> carries
|
|
* IReflectable_Any for its iface, whose self_tp() terminates, so
|
|
* the fast path must fall through to the generic one.
|
|
*/
|
|
require_foppoint_reflected();
|
|
|
|
obj<AReflectable> empty;
|
|
|
|
PrintJson print_json;
|
|
|
|
std::stringstream ss;
|
|
print_json.print_obj(empty, &ss);
|
|
|
|
/* null since 2026-09-21; see print-json-empty-fop-object */
|
|
REQUIRE(ss.str() == std::string("null"));
|
|
} /*TEST_CASE(print-obj-renders-an-empty-fop-as-null)*/
|
|
|
|
TEST_CASE("print-obj-fast-path-skips-the-registry", "[printjson]") {
|
|
/* The check that the fast path is actually TAKEN. Comparing its
|
|
* output against the generic path cannot show this -- they agree,
|
|
* which is the point -- so discriminate on the one thing only the
|
|
* fast path can do: an erased obj<AReflectable> already carries an
|
|
* AReflectable implementation in its iface, so self_tp() needs no
|
|
* FacetRegistry entry, while the generic path's rotation does.
|
|
*
|
|
* DFopUnregistered has the FacetImplementation mapping and no
|
|
* register_impl<>() call. If the fast path were removed, the
|
|
* first REQUIRE below would throw.
|
|
*/
|
|
require_foppoint_reflected();
|
|
|
|
DFopUnregistered u{3.25};
|
|
obj<AReflectable, DFopUnregistered> typed{&u};
|
|
obj<AReflectable> erased{typed};
|
|
|
|
PrintJson print_json;
|
|
|
|
std::stringstream via_fast;
|
|
REQUIRE_NOTHROW(print_json.print_obj(erased, &via_fast));
|
|
REQUIRE(via_fast.str().find("3.25") != std::string::npos);
|
|
|
|
/* same object, generic route: the rotation needs the registry */
|
|
std::stringstream via_generic;
|
|
REQUIRE_THROWS(print_json.print_tp(Reflect::make_tp(&erased),
|
|
&via_generic));
|
|
} /*TEST_CASE(print-obj-fast-path-skips-the-registry)*/
|
|
|
|
TEST_CASE("validate-then-print-matches-print-alone", "[printjson]") {
|
|
require_foppoint_reflected();
|
|
|
|
DFopPoint pt{1.5, -2.5};
|
|
obj<AReflectable, DFopPoint> o{&pt};
|
|
|
|
PrintJson print_json;
|
|
|
|
std::stringstream validated;
|
|
print_json.validate_obj(o);
|
|
print_json.print_obj(o, &validated);
|
|
|
|
std::stringstream print_alone;
|
|
print_json.print_obj(o, &print_alone);
|
|
|
|
REQUIRE(validated.str() == print_alone.str());
|
|
} /*TEST_CASE(validate-then-print-matches-print-alone)*/
|
|
|
|
TEST_CASE("validation-throws-without-writing", "[printjson]") {
|
|
/* why validate exists: print throws MID-traversal, leaving the
|
|
* consumer holding a truncated document. Validating first makes
|
|
* it all-or-nothing.
|
|
*/
|
|
require_foppoint_reflected();
|
|
|
|
DFopOpaque op{7};
|
|
obj<APrintable, DFopOpaque> opaque{&op};
|
|
|
|
DFopBox box{obj<APrintable>{opaque}};
|
|
|
|
PrintJson print_json;
|
|
|
|
std::stringstream ss;
|
|
|
|
REQUIRE_THROWS_WITH(print_json.validate_obj(obj<AReflectable,
|
|
DFopBox>{&box}),
|
|
Catch::Contains("DFopOpaque"));
|
|
REQUIRE(ss.str().empty());
|
|
|
|
/* and the thing being avoided: printing the same graph DOES write
|
|
* before it throws
|
|
*/
|
|
std::stringstream partial;
|
|
REQUIRE_THROWS(print_json.print_obj(obj<AReflectable, DFopBox>{&box},
|
|
&partial));
|
|
REQUIRE_FALSE(partial.str().empty());
|
|
} /*TEST_CASE(validation-throws-without-writing)*/
|
|
} /*namespace ut*/
|
|
} /*namespace xo*/
|
|
|
|
/* end FopJson.test.cpp */
|