The last opaque block of the save format. Reconciles lane W's provable negative
against lane O's issued-order saves: the two disagreed because three different
things were being compared as one.
Read out of Game::TurnCommands::Write (0x00842540, 764 bytes, no loops of its
own). Two halves:
* a PROLOGUE of six flag-gated groups. Each is a WriteBool on a member,
followed only when set by that command's payload. Write order is NOT offset
order -- each gate sits after its payload in the struct -- so the class is
one of the 89 whose offset-sorted layout view cannot be aligned to the wire.
That, not a branch, is why the sorted view showed an i32 where the save has
a bool at item 4.
* TWENTY-SEVEN std::list<T> members (0x70..0x1a8, stride 0xc, allocator-last),
each written by its own helper as WriteInt(size) then size element records.
All 27 are always written, so an empty list still costs one zero int.
So the recovered 44 items are 17 member writes plus ONE ITEM PER CONTAINER CALL
SITE: the linear recovery keeps the call site, guesses its kind from an element
field it could resolve, and drops the count word. 44 - 17 = 27 = the number of
lists. The "27 trailing ints with only 22 i32 slots" objection is that same 27
seen from both sides -- a kind mismatch in the table, not a structural
impossibility. And 8 prologue items + 27 zero counts = the 35-item block every
no-orders save carries bit-identically.
Item arithmetic closes to the unit on all five distinct workloads (35/38/41/61/
123). One correction to the provisional layout: the fleet-move element ends in a
COUNTED route vector {fleetId, nHops, nHops x systemId}, not a fixed quadruple --
with a four-item element the writer would need 26 lists on one save and 28 on
another, and it has 27.
Conformance: the generic check cannot be used here. Every tag is "." so the LCS
degenerates to a strict positional compare in which any primitive disagreement is
fatal, and the table's tail describes elements where the wire has counts. A
dedicated check states what is checkable instead: the 17-item prologue item for
item (17/17, and SchemaProbe takes every branch, so this is real evidence that
the conditional structure read from the instruction stream is the one the
recovery flattened) and the tail count (27 lists vs 27 table items). The tail is
reported as wire-only, never claimed as matched.
86 shapes / 838 items -> 87 / 856, still 0 MISMATCH.
Twenty-two of the 27 lists are HYPOTHESES: the scalar sequence comes straight off
the helper, but no save exercises them. Nested element bodies not otherwise
modelled here (ShipDesignDef, FleetLayout, WeaponGroups, DefenceLayout,
RaidTargets) are carried as opaque Nodes rather than guessed at, so a wrong body
cannot desynchronise a reader. select() matches the exact ".TurnCommands_v5"
suffix, so a future _v6 falls back to the carried Node.
SECOND DEFECT, present at main and unrelated to this block: zuul-turn23-fleet23
is the first save with a non-empty NVs list, and its element's leading id was
typed positionally as "." where the real tag is PID. The typed round trip on that
save differed at 0x89c14. Rule 6 exactly -- a path no save exercised was a
hypothesis flying as a fact. Fixed, and independently corroborated: the recovered
table for Game::ServerSystem names that item PID, and the Sys row moves from
102 matched / 1 wire-only / 3 shape-only to 103 / 0 / 2.
Coverage (CoverageArchive typed-vs-carried, not round-trip success): all eleven
saves 99.7-99.9% -> 100.0%; opaque items 37/43/63/132 -> 2 everywhere, and those
two are the deliberately-carried MT19937 block. Nothing else in any save we hold
is untyped. Ratchet 99.8 -> 99.99. Round trip byte-identical on all eleven
(newly so on zuul-turn23-fleet23).
test_save now asserts, on every real save, that each TurnCommands_v5 block is
consumed by the prologue plus the 27 lists with nothing left over -- the
item-granular statement a wrong list count or element width breaks first.
clean-room OK; host ctest 36/36; test_save 11 saves 0 failures. src/shim/ not
touched, so no cross-build was needed.
839 lines
35 KiB
C++
839 lines
35 KiB
C++
// mars::stream unit tests on hand-built byte fixtures (no game data).
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "mars/stream/dump.h"
|
|
#include "mars/stream/gzip.h"
|
|
#include "mars/stream/reader.h"
|
|
#include "mars/stream/save.h"
|
|
#include "mars/stream/writer.h"
|
|
|
|
using namespace mars::stream;
|
|
|
|
static int fails = 0;
|
|
#define CHECK(cond) \
|
|
do { \
|
|
if (!(cond)) { \
|
|
std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
|
++fails; \
|
|
} \
|
|
} while (0)
|
|
#define CHECK_EQ(a, b) \
|
|
do { \
|
|
auto _a = (a); \
|
|
auto _b = (b); \
|
|
if (!(_a == _b)) { \
|
|
std::printf("FAIL %s:%d: %s == %s\n", __FILE__, __LINE__, #a, #b); \
|
|
++fails; \
|
|
} \
|
|
} while (0)
|
|
|
|
static Bytes B(std::initializer_list<int> v) {
|
|
Bytes b;
|
|
for (int x : v) b.push_back(uint8_t(x));
|
|
return b;
|
|
}
|
|
static Bytes cat(std::initializer_list<Bytes> parts) {
|
|
Bytes b;
|
|
for (const Bytes& p : parts) b.insert(b.end(), p.begin(), p.end());
|
|
return b;
|
|
}
|
|
static size_t count(const std::vector<Issue>& v, Issue::Level l) {
|
|
size_t n = 0;
|
|
for (const Issue& i : v) n += i.level == l;
|
|
return n;
|
|
}
|
|
|
|
// --- 1. primitive encodings and joint padding (bytes written by hand) -----------
|
|
static void test_primitives_bytes() {
|
|
// [len][name][value][pad to 4], padding computed over the whole item
|
|
const Bytes turn = B({4, 0, 0, 0, 'T', 'u', 'r', 'n', 1, 0, 0, 0}); // 12: no pad
|
|
const Bytes haltv = B({5, 0, 0, 0, 'h', 'a', 'l', 't', 'v', 1, 0, 0}); // 10 -> 12
|
|
const Bytes vnh = B({3, 0, 0, 0, 'v', 'n', 'h', 0}); // 8: no pad
|
|
const Bytes name = B({4, 0, 0, 0, 'N', 'a', 'm', 'e', 3, 0, 0, 0, 'S', 'o', 'l', 0}); // 15 -> 16
|
|
const Bytes key = B({3, 0, 0, 0, 'K', 'e', 'y', 0, 0, 0, 0, 0}); // empty string = 4 zero bytes
|
|
const Bytes incmod = B({6, 0, 0, 0, 'I', 'n', 'c', 'M', 'o', 'd', 0, 0, 0x80, 0x3f, 0, 0}); // float 1.0, 14 -> 16
|
|
const Bytes bats2 = B({5, 0, 0, 0, 'B', 'a', 't', 's', '2', 0x2a, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); // int64 42: 17 -> 20
|
|
const Bytes dot = B({1, 0, 0, 0, '.', 7, 0, 0, 0, 0, 0, 0}); // "." int 7: 9 -> 12
|
|
|
|
Writer w;
|
|
w.int32("Turn", 1);
|
|
w.boolean("haltv", true);
|
|
w.boolean("vnh", false);
|
|
w.string("Name", "Sol");
|
|
w.string("Key", "");
|
|
w.float32("IncMod", 1.0f);
|
|
w.int64("Bats2", 42);
|
|
w.int32(".", 7);
|
|
CHECK(w.bytes() == cat({turn, haltv, vnh, name, key, incmod, bats2, dot}));
|
|
|
|
std::vector<Issue> issues;
|
|
Stats st;
|
|
Node root = read_tree(w.bytes(), &issues, &st); // with the save registry: all tags hinted
|
|
CHECK_EQ(root.children.size(), size_t(8));
|
|
const auto& c = root.children;
|
|
CHECK(c[0].name == "Turn" && c[0].kind == Kind::Int && c[0].as_int() == 1 && c[0].hinted);
|
|
CHECK(c[0].offset == 0 && c[0].size == 12);
|
|
CHECK(c[1].name == "haltv" && c[1].kind == Kind::Bool && c[1].as_bool() && c[1].size == 12);
|
|
CHECK(c[2].name == "vnh" && c[2].kind == Kind::Bool && !c[2].as_bool() && c[2].size == 8);
|
|
CHECK(c[3].name == "Name" && c[3].kind == Kind::String && c[3].as_string() == "Sol" && c[3].size == 16);
|
|
CHECK(c[4].name == "Key" && c[4].kind == Kind::String && c[4].as_string().empty() && c[4].size == 12);
|
|
CHECK(c[5].name == "IncMod" && c[5].kind == Kind::Float && c[5].as_float() == 1.0f && c[5].size == 16);
|
|
CHECK(c[6].name == "Bats2" && c[6].kind == Kind::Int64 && c[6].as_int64() == 42 && c[6].size == 20);
|
|
CHECK(c[7].name == "." && c[7].kind == Kind::Int && c[7].as_int() == 7 && !c[7].hinted);
|
|
CHECK_EQ(st.resyncs, 0u);
|
|
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
|
|
|
// without any registry everything is guessed by layout + bit pattern
|
|
Node g = read_tree(w.bytes(), nullptr, nullptr, nullptr);
|
|
CHECK(g.children[0].kind == Kind::Int && !g.children[0].hinted);
|
|
CHECK(g.children[1].kind == Kind::Bool);
|
|
CHECK(g.children[3].kind == Kind::String);
|
|
CHECK(g.children[4].kind == Kind::Int && g.children[4].as_int() == 0); // empty string looks like int 0
|
|
CHECK(g.children[5].kind == Kind::Float); // 0x3f800000 is not a small int
|
|
CHECK(g.children[6].kind == Kind::Int64);
|
|
|
|
// round trip: the tree re-emits byte-identically
|
|
CHECK(write_tree(root) == w.bytes());
|
|
CHECK(write_tree(g) == w.bytes());
|
|
}
|
|
|
|
// --- 2. frames: nesting, tagless frame, empty frame, "." arrays ------------------
|
|
static void test_frames() {
|
|
Writer w;
|
|
w.begin("Summary");
|
|
w.string("GameName", "x");
|
|
w.begin("Players"); // VectorHelper: "." count + "." elements
|
|
w.int32(".", 2);
|
|
w.begin(".");
|
|
w.int32("Rank", 1);
|
|
w.end();
|
|
w.begin(".");
|
|
w.int32("Rank", 2);
|
|
w.end();
|
|
w.end();
|
|
w.begin("Empty");
|
|
w.end();
|
|
w.begin_tagless();
|
|
w.float32(".", 2.5f);
|
|
w.end();
|
|
w.end();
|
|
// expected framing bytes for the head: [7]"Summary"[pad] BEEFBEEF
|
|
const Bytes head = B({7, 0, 0, 0, 'S', 'u', 'm', 'm', 'a', 'r', 'y', 0, 0xef, 0xbe, 0xef, 0xbe});
|
|
CHECK(std::equal(head.begin(), head.end(), w.bytes().begin()));
|
|
const Bytes tail = B({0x10, 0x41, 0x10, 0x41});
|
|
CHECK(std::equal(tail.begin(), tail.end(), w.bytes().end() - 4));
|
|
|
|
std::vector<Issue> issues;
|
|
Stats st;
|
|
Node root = read_tree(w.bytes(), &issues, &st);
|
|
CHECK_EQ(root.children.size(), size_t(1));
|
|
const Node& s = root.children[0];
|
|
CHECK(s.is_complex() && s.name == "Summary" && s.offset == 0 && s.size == w.size());
|
|
CHECK_EQ(s.children.size(), size_t(4));
|
|
const Node& players = s.children[1];
|
|
CHECK(players.is_complex() && players.children.size() == 3);
|
|
CHECK(players.children[0].name == "." && players.children[0].as_int() == 2);
|
|
CHECK(players.children[1].is_complex() && players.children[1].name == ".");
|
|
CHECK(players.children[2].children[0].name == "Rank" && players.children[2].children[0].as_int() == 2);
|
|
CHECK(s.children[2].is_complex() && s.children[2].children.empty());
|
|
CHECK(s.children[3].is_complex() && !s.children[3].tagged && s.children[3].children.size() == 1);
|
|
CHECK(s.children[3].children[0].as_float() == 2.5f);
|
|
CHECK_EQ(st.frames, 6u);
|
|
CHECK_EQ(st.resyncs, 0u);
|
|
CHECK(write_tree(root) == w.bytes());
|
|
|
|
// the dump prints frames with item count and size, tagless as <tagless>
|
|
auto lines = dump_tree(root);
|
|
CHECK(lines.size() == 17);
|
|
CHECK(lines[0] == "@00000000 Summary { # 4 items, " + std::to_string(w.size()) + " bytes");
|
|
CHECK(lines[1] == "@00000010 GameName string \"x\"");
|
|
CHECK(lines[13].find("<tagless> {") != std::string::npos);
|
|
}
|
|
|
|
// --- 3. resync: garbage inside a frame becomes raw; the walk continues ----------
|
|
// (both fixtures were checked against the reference reader: same tree, same
|
|
// stats, one warning each)
|
|
static void test_resync() {
|
|
Bytes tail;
|
|
{
|
|
Writer t;
|
|
t.begin("B");
|
|
t.int32("z", 2);
|
|
t.end();
|
|
tail = t.take();
|
|
}
|
|
// case A: no tag at all after a nested frame -> unnamed raw, resync to the next plausible tag
|
|
{
|
|
Writer w;
|
|
w.begin("A");
|
|
w.begin("C");
|
|
w.end();
|
|
Bytes s = w.take();
|
|
s.insert(s.end(), 12, 0xff);
|
|
Writer y;
|
|
y.string("y", std::string(80, 'y'));
|
|
y.end();
|
|
Bytes yb = y.take();
|
|
s = cat({s, yb, tail});
|
|
std::vector<Issue> issues;
|
|
Stats st;
|
|
Node root = read_tree(s, &issues, &st);
|
|
CHECK_EQ(root.children.size(), size_t(2));
|
|
const Node& a = root.children[0];
|
|
CHECK(a.name == "A" && a.children.size() == 3 && a.size == 136);
|
|
CHECK(a.children[0].name == "C" && a.children[0].children.empty() && a.children[0].size == 16);
|
|
const Node& r = a.children[1];
|
|
CHECK(r.kind == Kind::Raw && !r.tagged && r.raw.size() == 12 && r.offset == 0x1c);
|
|
CHECK(a.children[2].name == "y" && a.children[2].kind == Kind::String && a.children[2].as_string().size() == 80);
|
|
CHECK(root.children[1].name == "B" && root.children[1].children[0].as_int() == 2);
|
|
CHECK_EQ(st.resyncs, 1u);
|
|
CHECK_EQ(st.raw_bytes, 12u);
|
|
CHECK_EQ(count(issues, Issue::Warn), size_t(1));
|
|
CHECK(write_tree(root) == s); // raw nodes re-emit exactly
|
|
auto lines = dump_tree(root);
|
|
CHECK(lines[3] == "@0000001c <tagless> raw[12] ffffffffffffffffffffffff");
|
|
}
|
|
// case B: a fine-looking tag whose value fits no layout -> tagged raw up to the next tag
|
|
{
|
|
Bytes s = B({3, 0, 0, 0, 'a', 'b', 'c', 5});
|
|
s.insert(s.end(), 12, 0xff);
|
|
Writer w;
|
|
w.int32("y", 2);
|
|
w.end();
|
|
Bytes head;
|
|
{
|
|
Writer h;
|
|
h.begin("A");
|
|
head = h.take();
|
|
}
|
|
s = cat({head, s, w.take(), tail});
|
|
std::vector<Issue> issues;
|
|
Stats st;
|
|
Node root = read_tree(s, &issues, &st);
|
|
const Node& a = root.children[0];
|
|
CHECK(a.children.size() == 2 && a.size == 48);
|
|
CHECK(a.children[0].name == "abc" && a.children[0].kind == Kind::Raw && a.children[0].tagged);
|
|
CHECK(a.children[0].raw.size() == 13 && a.children[0].raw[0] == 5);
|
|
CHECK(a.children[1].name == "y" && a.children[1].as_int() == 2 && a.children[1].offset == 0x20);
|
|
CHECK_EQ(st.resyncs, 1u);
|
|
CHECK_EQ(st.raw_bytes, 13u);
|
|
CHECK_EQ(count(issues, Issue::Warn), size_t(1));
|
|
CHECK(issues[0].msg.find("no value layout fits; skipped 13 bytes to tag") != std::string::npos);
|
|
CHECK(write_tree(root) == s);
|
|
}
|
|
|
|
// unnamed 12-byte payload right before END is only an info (Vector3 fallback)
|
|
Bytes v3;
|
|
{
|
|
Writer t;
|
|
t.begin("Pos");
|
|
t.end();
|
|
v3 = t.take();
|
|
Bytes body = B({0, 0, 0x80, 0x3f, 0, 0, 0, 0x40, 0, 0, 0x40, 0x40});
|
|
v3.insert(v3.end() - 4, body.begin(), body.end());
|
|
}
|
|
std::vector<Issue> issues;
|
|
Stats st;
|
|
Node r3 = read_tree(v3, &issues, &st);
|
|
CHECK(r3.children[0].children.size() == 1 && r3.children[0].children[0].kind == Kind::Raw);
|
|
CHECK_EQ(count(issues, Issue::Info), size_t(1));
|
|
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
|
// and the shape layer reads it as a vec3
|
|
std::vector<Issue> si;
|
|
ReadArchive ar(r3.children, si, "");
|
|
Vec3 v;
|
|
ar.vec3(A("Pos"), v);
|
|
CHECK(v.x == 1.f && v.y == 2.f && v.z == 3.f);
|
|
|
|
// a stray END at top level and trailing bytes are reported, not fatal
|
|
Bytes stray = B({0x10, 0x41, 0x10, 0x41, 1, 2});
|
|
issues.clear();
|
|
Node r4 = read_tree(stray, &issues, &st);
|
|
CHECK(r4.children.size() == 2);
|
|
CHECK(count(issues, Issue::Warn) >= 2);
|
|
CHECK(write_tree(r4) == stray);
|
|
}
|
|
|
|
// --- 4. cp1252 string values never veto layout when the tag is known ------------
|
|
static void test_cp1252() {
|
|
Writer w;
|
|
w.boolean("haltv", true);
|
|
w.string("Name", std::string("Kor\x92Voth")); // 0x92 = right single quote
|
|
w.int32("VFlags", 3);
|
|
std::vector<Issue> issues;
|
|
Stats st;
|
|
Node root = read_tree(w.bytes(), &issues, &st);
|
|
CHECK_EQ(root.children.size(), size_t(3));
|
|
CHECK(root.children[0].kind == Kind::Bool && root.children[0].hinted);
|
|
CHECK(root.children[1].kind == Kind::String && root.children[1].as_string() == "Kor\x92Voth");
|
|
CHECK(root.children[2].kind == Kind::Int && root.children[2].as_int() == 3);
|
|
CHECK_EQ(st.hint_failures, 0u);
|
|
auto lines = dump_tree(root);
|
|
CHECK(lines[1] == "@0000000c Name string \"Kor\\u2019Voth\"");
|
|
// guessing an unknown tag still accepts every cp1252-defined byte
|
|
Writer g;
|
|
g.string("zz", std::string("caf\xe9"));
|
|
g.int32("q", 1);
|
|
Node r2 = read_tree(g.bytes(), nullptr, nullptr, nullptr);
|
|
CHECK(r2.children[0].kind == Kind::String);
|
|
}
|
|
|
|
// --- 5. hints: registry types Summary children positionally and by name ---------
|
|
static void test_hints() {
|
|
Writer w;
|
|
w.begin("Summary");
|
|
w.string("GameName", "g");
|
|
w.int32("Turn", 0); // 0 either way
|
|
w.int32("NumSys", 0);
|
|
w.int32("Checksum", 0);
|
|
w.begin("Players");
|
|
w.int32(".", 0);
|
|
w.end();
|
|
w.begin("Session");
|
|
w.begin("TMRS");
|
|
w.float32("TSTL", 0.0f); // 0.0f: guessed would be int 0
|
|
w.end();
|
|
w.end();
|
|
w.int32("MapShape", 0);
|
|
w.float32("IncMod", 0.0f);
|
|
w.end();
|
|
Node root = read_tree(w.bytes());
|
|
const Node& s = root.children[0];
|
|
CHECK(s.hinted);
|
|
CHECK(s.children[0].kind == Kind::String && s.children[0].hinted);
|
|
CHECK(s.children[4].hinted); // Players CArr
|
|
CHECK(s.children[4].children[0].kind == Kind::Int && s.children[4].children[0].hinted);
|
|
const Node& tstl = s.children[5].children[0].children[0];
|
|
CHECK(tstl.kind == Kind::Float && tstl.hinted && tstl.as_float() == 0.f);
|
|
CHECK(s.children[7].kind == Kind::Float && s.children[7].hinted); // IncMod by name
|
|
// the registry knows the top-level shapes and the RNG raw frame
|
|
const Registry& reg = save_registry();
|
|
CHECK(reg.shape("Summary") && reg.shape("Sim") && reg.shape("Sys") && reg.shape("Player"));
|
|
CHECK(reg.shape("RNG") && reg.shape("RNG")->type == Desc::Raw);
|
|
CHECK(reg.kind("GameName") == Prim::String && reg.kind("Bats2") == Prim::Int64 && reg.kind("haltv") == Prim::Bool);
|
|
CHECK(reg.kind(".") == Prim::None);
|
|
CHECK(reg.kind("Team") == Prim::Int); // int in Slot and Player; the frame is a separate shape entry
|
|
CHECK(reg.shape("Team") != nullptr);
|
|
CHECK(reg.kind("pop") == Prim::Int64 && reg.shape("pop") != nullptr); // int64 in stats, Population frame in Ship
|
|
}
|
|
|
|
// --- 6. RNG raw frame: body read straight to the END marker ---------------------
|
|
static void test_raw_frame() {
|
|
Bytes blob(2500, 0xAB);
|
|
Writer w;
|
|
w.begin("RNG");
|
|
w.raw(".", blob);
|
|
w.end();
|
|
w.int32("Map", 1);
|
|
std::vector<Issue> issues;
|
|
Stats st;
|
|
Node root = read_tree(w.bytes(), &issues, &st);
|
|
CHECK(root.children[0].children.size() == 1);
|
|
const Node& r = root.children[0].children[0];
|
|
CHECK(r.kind == Kind::Raw && r.name == "." && r.raw.size() == 2503); // 2500 + 3 joint-padding bytes
|
|
CHECK_EQ(st.raw_bytes, 2503u);
|
|
CHECK(root.children[1].as_int() == 1);
|
|
CHECK(write_tree(root) == w.bytes());
|
|
}
|
|
|
|
// --- 7. typed shapes: Summary write -> bytes -> read; and hand-checked bytes ------
|
|
static void test_typed_summary() {
|
|
shapes::Summary s;
|
|
s.gameName = "Test";
|
|
s.turn = 7;
|
|
s.numSys = 3;
|
|
s.checksum = 99;
|
|
shapes::PlayerInfo p;
|
|
p.slot.isPlay = true;
|
|
p.slot.fxNm = "re";
|
|
p.slot.fxCrID.idx = -1;
|
|
p.slot.fxCrID.r = 10;
|
|
p.slot.fxCrID.g = 20;
|
|
p.slot.fxCrID.b = 30;
|
|
p.slot.tag = 1466349286;
|
|
p.slot.team = -1;
|
|
p.slot.settings.treasury = 50000;
|
|
p.rank = 2;
|
|
s.players.push_back(p);
|
|
s.session.tmrs.tctl = 240.f;
|
|
s.incMod = 1.f;
|
|
s.resMod = 1.f;
|
|
s.alliances = true;
|
|
s.encounters = true;
|
|
|
|
Writer w;
|
|
WriteArchive wa(w);
|
|
wa.obj(A("Summary"), s);
|
|
Bytes bytes = w.take();
|
|
|
|
// spot-check the head bytes by hand: frame tag, GameName, Turn
|
|
Bytes head = B({7, 0, 0, 0, 'S', 'u', 'm', 'm', 'a', 'r', 'y', 0, 0xef, 0xbe, 0xef, 0xbe,
|
|
8, 0, 0, 0, 'G', 'a', 'm', 'e', 'N', 'a', 'm', 'e', 4, 0, 0, 0, 'T', 'e', 's', 't',
|
|
4, 0, 0, 0, 'T', 'u', 'r', 'n', 7, 0, 0, 0});
|
|
CHECK(bytes.size() > head.size() && std::equal(head.begin(), head.end(), bytes.begin()));
|
|
|
|
std::vector<Issue> issues;
|
|
Stats st;
|
|
Node root = read_tree(bytes, &issues, &st);
|
|
CHECK_EQ(st.resyncs, 0u);
|
|
CHECK_EQ(st.hint_failures, 0u);
|
|
shapes::Summary back;
|
|
ReadArchive ra(root.children, issues, "");
|
|
ra.obj(A("Summary"), back);
|
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
|
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
|
CHECK(back.gameName == "Test" && back.turn == 7 && back.numSys == 3 && back.checksum == 99);
|
|
CHECK(back.players.size() == 1 && back.players[0].rank == 2);
|
|
CHECK(back.players[0].slot.fxNm == "re" && back.players[0].slot.tag == 1466349286);
|
|
CHECK(back.players[0].slot.fxCrID.idx == -1 && back.players[0].slot.fxCrID.b == 30);
|
|
CHECK(back.players[0].slot.settings.treasury == 50000);
|
|
CHECK(back.session.tmrs.tctl == 240.f && back.incMod == 1.f && back.alliances && !back.teams);
|
|
CHECK(back.scenario.empty());
|
|
|
|
// and back out: byte-identical
|
|
Writer w2;
|
|
WriteArchive wa2(w2);
|
|
wa2.obj(A("Summary"), back);
|
|
CHECK(w2.bytes() == bytes);
|
|
|
|
// the "." positional items are reported as info, never warn
|
|
CHECK(count(issues, Issue::Info) >= 4); // FxCrID idx/r/g/b, Settings x4, Players count/elements
|
|
|
|
// a missing confirmed field is an error; an unexpected item before it a warning
|
|
Writer w3;
|
|
w3.begin("Summary");
|
|
w3.string("GameName", "g");
|
|
w3.int32("Bogus", 1);
|
|
w3.int32("Turn", 2);
|
|
w3.end();
|
|
Node r3 = read_tree(w3.bytes());
|
|
std::vector<Issue> i3;
|
|
shapes::Summary s3;
|
|
ReadArchive ra3(r3.children, i3, "");
|
|
ra3.obj(A("Summary"), s3);
|
|
CHECK(s3.turn == 2);
|
|
CHECK(count(i3, Issue::Warn) >= 1);
|
|
CHECK(count(i3, Issue::Error) >= 1); // NumSys and the rest are missing
|
|
}
|
|
|
|
// --- 8. conditionals, optionals and inline arrays through the archives ------------
|
|
static void test_typed_conditionals() {
|
|
shapes::Ship ship;
|
|
ship.desID = 5;
|
|
ship.hbq = true;
|
|
shapes::BuildOrder o;
|
|
o.desID = 9;
|
|
ship.bq2.orders.push_back(o);
|
|
ship.hsp = false;
|
|
ship.prisH.prMax = 2;
|
|
ship.prisH.prisoners.push_back({3, 4});
|
|
ship.thrusters.push_back({1.5f, 2.5f});
|
|
Writer w;
|
|
WriteArchive wa(w);
|
|
wa.obj(A("Ship"), ship);
|
|
Node root = read_tree(w.bytes());
|
|
std::vector<Issue> issues;
|
|
shapes::Ship back;
|
|
ReadArchive ra(root.children, issues, "");
|
|
ra.obj(A("Ship"), back);
|
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
|
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
|
CHECK(back.hbq && back.bq2.orders.size() == 1 && back.bq2.orders[0].desID == 9);
|
|
CHECK(!back.hsp && back.pop.groups.empty());
|
|
CHECK(back.prisH.prMax == 2 && back.prisH.prisoners.size() == 1 && back.prisH.prisoners[0].prNum == 4);
|
|
CHECK(back.thrusters.size() == 1 && back.thrusters[0].thm == 2.5f);
|
|
Writer w2;
|
|
WriteArchive wa2(w2);
|
|
wa2.obj(A("Ship"), back);
|
|
CHECK(w2.bytes() == w.bytes());
|
|
|
|
// Fleet: optional legacy tags absent, HFPlan false -> no FPlan; Sys: vnh gate
|
|
shapes::Fleet f;
|
|
f.ftName = "Fleet 1";
|
|
f.hfPlan = true;
|
|
shapes::Waypoint wp;
|
|
wp.wpt = 272;
|
|
f.fplan.wpts.push_back(wp);
|
|
Writer w3;
|
|
WriteArchive wa3(w3);
|
|
wa3.obj(A("Flt"), f);
|
|
Node r3 = read_tree(w3.bytes());
|
|
shapes::Fleet fb;
|
|
ReadArchive ra3(r3.children, issues, "");
|
|
ra3.obj(A("Flt"), fb);
|
|
CHECK(fb.hfPlan && fb.fplan.wpts.size() == 1 && fb.fplan.wpts[0].wpt == 272 && fb.fplan.wpts[0].nrtFramed);
|
|
CHECK(!fb.sysID && !fb.caps);
|
|
CHECK(fb.ftName == "Fleet 1");
|
|
Writer w4;
|
|
WriteArchive wa4(w4);
|
|
wa4.obj(A("Flt"), fb);
|
|
CHECK(w4.bytes() == w3.bytes());
|
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
|
}
|
|
|
|
// --- 8b. carr<std::string> and the SvSctOb variant dispatch -----------------------------
|
|
static void test_typed_string_array() {
|
|
// DOpts / trev are VectorHelper<Mars::String>: a framed count then NULL-named
|
|
// strings. The element branch has to survive the empty string, which is four
|
|
// zero bytes and so indistinguishable from the int 0 in isolation.
|
|
shapes::DesignSection sec;
|
|
sec.opts = {"OPT_Armour", "", "OPT_Shield"};
|
|
Writer w;
|
|
WriteArchive wa(w);
|
|
wa.obj(A("DSec"), sec);
|
|
Node root = read_tree(w.bytes());
|
|
std::vector<Issue> issues;
|
|
shapes::DesignSection back;
|
|
ReadArchive ra(root.children, issues, "");
|
|
ra.obj(A("DSec"), back);
|
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
|
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
|
CHECK(back.opts.size() == 3);
|
|
CHECK(back.opts[0] == "OPT_Armour" && back.opts[1].empty() && back.opts[2] == "OPT_Shield");
|
|
Writer w2;
|
|
WriteArchive wa2(w2);
|
|
wa2.obj(A("DSec"), back);
|
|
CHECK(w2.bytes() == w.bytes());
|
|
}
|
|
|
|
static void test_svsctob_variants() {
|
|
// `xsc` is keyed by the preceding `xscn` and `EncObj` by the preceding
|
|
// `EncID`. Nothing on the wire says which body follows, so the round trip is
|
|
// only correct if the reader applies the same key the writer did.
|
|
shapes::ScriptObjects so;
|
|
so.scnID = 0;
|
|
|
|
shapes::ScriptObjects::Extra traps;
|
|
traps.name = "traps";
|
|
traps.obj.select(traps.name);
|
|
traps.obj.traps.traps.push_back({272, 0, -1, -1});
|
|
so.extras.push_back(traps);
|
|
|
|
shapes::ScriptObjects::Extra gm;
|
|
gm.name = "gmtrigger";
|
|
gm.obj.select(gm.name);
|
|
gm.obj.grandMenace.gmch = 21;
|
|
so.extras.push_back(gm);
|
|
|
|
shapes::ScriptObjects::Extra ind; // "indsys" serializes nothing at all
|
|
ind.name = "indsys";
|
|
ind.obj.select(ind.name);
|
|
so.extras.push_back(ind);
|
|
|
|
shapes::ScriptObjects::Enc swarm; // EncID 3
|
|
swarm.id = 3;
|
|
swarm.obj.select(3);
|
|
swarm.obj.swarm.asg.push_back({1120, 336});
|
|
swarm.obj.swarm.infest.push_back({336, 0, 1, 2147483647, 0, false});
|
|
swarm.obj.swarm.deshive = 1072;
|
|
swarm.obj.swarm.deslarva = 1088;
|
|
so.encounters.push_back(swarm);
|
|
|
|
shapes::ScriptObjects::Enc mon; // EncID 5: Monitor writes Derelict's body first
|
|
mon.id = 5;
|
|
mon.obj.select(5);
|
|
mon.obj.monitor.base.designs.push_back({1312, 30});
|
|
mon.obj.monitor.base.asg.push_back({1360, 64});
|
|
mon.obj.monitor.spawns.push_back({"_AsteroidMonitor", 30, 1.0f, 1312});
|
|
so.encounters.push_back(mon);
|
|
|
|
shapes::ScriptObjects::Enc vn; // EncID 1
|
|
vn.id = 1;
|
|
vn.obj.select(1);
|
|
vn.obj.vonNeumann.sken = true;
|
|
vn.obj.vonNeumann.ntm = 25;
|
|
vn.obj.vonNeumann.vnhp = 0;
|
|
so.encounters.push_back(vn);
|
|
|
|
shapes::ScriptObjects::Enc unmodelled; // EncID 8 (PuppetMaster): carried, not typed
|
|
unmodelled.id = 8;
|
|
unmodelled.obj.select(8);
|
|
unmodelled.obj.unknown.push_back(Node::int32("Flt", 42));
|
|
so.encounters.push_back(unmodelled);
|
|
|
|
Writer w;
|
|
WriteArchive wa(w);
|
|
wa.obj(A("SvSctOb"), so);
|
|
Node root = read_tree(w.bytes());
|
|
std::vector<Issue> issues;
|
|
shapes::ScriptObjects back;
|
|
ReadArchive ra(root.children, issues, "");
|
|
ra.obj(A("SvSctOb"), back);
|
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
|
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
|
CHECK(back.extras.size() == 3 && back.encounters.size() == 4);
|
|
CHECK(back.extras[0].name == "traps" && back.extras[0].obj.traps.traps.size() == 1);
|
|
CHECK(back.extras[0].obj.traps.traps[0].sys == 272);
|
|
CHECK(back.extras[1].obj.grandMenace.gmch == 21);
|
|
CHECK(back.extras[2].obj.indSys.extra.empty()); // the empty frame really is empty
|
|
CHECK(back.encounters[0].obj.swarm.deslarva == 1088);
|
|
CHECK(back.encounters[0].obj.swarm.infest.size() == 1 &&
|
|
back.encounters[0].obj.swarm.infest[0].mtrn == 2147483647);
|
|
CHECK(back.encounters[1].obj.monitor.base.designs.size() == 1 &&
|
|
back.encounters[1].obj.monitor.spawns.size() == 1);
|
|
CHECK(back.encounters[1].obj.monitor.spawns[0].scnm == "_AsteroidMonitor");
|
|
CHECK(back.encounters[2].obj.vonNeumann.sken && back.encounters[2].obj.vonNeumann.ntm == 25);
|
|
CHECK(back.encounters[3].id == 8 && back.encounters[3].obj.unknown.size() == 1);
|
|
Writer w2;
|
|
WriteArchive wa2(w2);
|
|
wa2.obj(A("SvSctOb"), back);
|
|
CHECK(w2.bytes() == w.bytes());
|
|
|
|
// The key really is what selects the body: read the same bytes with the wrong
|
|
// EncID and the Swarm record no longer parses as a Swarm.
|
|
shapes::EncounterObject wrong;
|
|
wrong.select(17); // CrowRuins over Swarm bytes
|
|
CHECK(wrong.id == 17 && wrong.swarm.asg.empty());
|
|
}
|
|
|
|
// --- 8c. the AIAgent custom-data block -------------------------------------------------
|
|
// Every container in the block has count 0 in all four saves we hold, so the real
|
|
// data exercises the frames and none of the element layouts. This test populates
|
|
// each of them, which is the only check those layouts get.
|
|
static void test_aiagent_block() {
|
|
shapes::StrategyAIAgent ai;
|
|
|
|
// Game::AttribMap: a "." count then two "." strings per entry. The second pair
|
|
// is empty on both sides -- an empty string is four zero bytes, the same as the
|
|
// int 0, which is exactly how a mistyped string field stays invisible.
|
|
ai.attrib.entries = {{"aggression", "high"}, {"", ""}};
|
|
|
|
// AIWeightMap keys are FRAMES (Mars::StreamableEnum, or ShipSectionID's two
|
|
// ints), never bare ints, and each key is followed by a "." float.
|
|
ai.turnPriorities.entries.push_back({{7, {}}, 0.5f});
|
|
ai.situation.sections.entries.push_back({{2, 44, {}}, 1.5f});
|
|
ai.situation.weaponFamilies.entries.push_back({{3, {}}, -2.0f});
|
|
ai.playerHate.entries.push_back({{16, {}}, 0.25f});
|
|
|
|
ai.requestStamps.push_back({16, 4, {}});
|
|
// `dsh` counts (pid, trns) pairs walked out of a map; trns is a VectorHelper<int>.
|
|
ai.designShares.push_back({16, {1, 2, 3}});
|
|
ai.designShares.push_back({32, {}});
|
|
ai.borderStability.push_back({16, 5});
|
|
ai.menaceBlacklist.push_back({272, 9});
|
|
ai.designNames.names.push_back({{5, 37, {}}, "Ravenous"});
|
|
ai.designNames.names.push_back({{5, 63, {}}, ""}); // an empty design name
|
|
ai.aiHivJ = 3;
|
|
ai.sdFlT = 7;
|
|
ai.newAlliances.push_back({11, {}}); // framed element, not a bare int
|
|
ai.newAlliances.push_back({12, {}});
|
|
ai.lnat = 4;
|
|
ai.lat = 5;
|
|
shapes::StrategyAIAgent::AISysEntry sys;
|
|
sys.id = 48;
|
|
ai.systems.push_back(sys);
|
|
shapes::AICombatReport rep;
|
|
rep.crTrn = 2;
|
|
rep.crPce = true;
|
|
rep.crSys.value = 272;
|
|
rep.crPlSv2.rpBon = 1.25f;
|
|
rep.crPlSv2.rpBonT = 3;
|
|
rep.crPlSv2.savBonus = 4;
|
|
rep.crPlSv2.maintHF = true;
|
|
rep.crPlSv2.tacReports.push_back(Node::int32("TRid", 9)); // carried, not typed
|
|
ai.combatReports.push_back(rep);
|
|
ai.colonyLosses.push_back({2, 272, 16});
|
|
ai.provocations.push_back({16, 450.0f});
|
|
ai.techScores = {2, 4, 6};
|
|
ai.fct = 1;
|
|
ai.autoPeaceRuns.push_back({16, 2, 5, {}});
|
|
|
|
Writer w;
|
|
WriteArchive wa(w);
|
|
wa.obj(A("CD"), ai);
|
|
Node root = read_tree(w.bytes());
|
|
std::vector<Issue> issues;
|
|
shapes::StrategyAIAgent back;
|
|
ReadArchive ra(root.children, issues, "");
|
|
ra.obj(A("CD"), back);
|
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
|
CHECK_EQ(count(issues, Issue::Warn), size_t(0));
|
|
|
|
CHECK(back.attrib.entries.size() == 2);
|
|
CHECK(back.attrib.entries[0].key == "aggression" && back.attrib.entries[0].value == "high");
|
|
CHECK(back.attrib.entries[1].key.empty() && back.attrib.entries[1].value.empty());
|
|
CHECK(back.turnPriorities.entries.size() == 1 && back.turnPriorities.entries[0].key.value == 7);
|
|
CHECK(back.turnPriorities.entries[0].weight == 0.5f);
|
|
CHECK(back.situation.sections.entries.size() == 1 && back.situation.sections.entries[0].key.a == 2 &&
|
|
back.situation.sections.entries[0].key.b == 44);
|
|
CHECK(back.situation.weaponFamilies.entries[0].weight == -2.0f);
|
|
CHECK(back.requestStamps.size() == 1 && back.requestStamps[0].pid == 16 && back.requestStamps[0].trn == 4);
|
|
CHECK(back.designShares.size() == 2 && back.designShares[0].turns.size() == 3 &&
|
|
back.designShares[0].turns[2] == 3 && back.designShares[1].turns.empty());
|
|
CHECK(back.borderStability.size() == 1 && back.borderStability[0].b == 5);
|
|
CHECK(back.menaceBlacklist.size() == 1 && back.menaceBlacklist[0].a == 272);
|
|
CHECK(back.designNames.names.size() == 2 && back.designNames.names[0].name == "Ravenous" &&
|
|
back.designNames.names[0].section.b == 37 && back.designNames.names[1].name.empty());
|
|
CHECK(back.newAlliances.size() == 2 && back.newAlliances[1].value == 12);
|
|
CHECK(back.lnat == 4 && back.lat == 5);
|
|
CHECK(back.systems.size() == 1 && back.systems[0].id == 48);
|
|
CHECK(back.combatReports.size() == 1 && back.combatReports[0].crPce &&
|
|
back.combatReports[0].crSys.value == 272 && back.combatReports[0].crPlSv2.maintHF &&
|
|
back.combatReports[0].crPlSv2.tacReports.size() == 1);
|
|
CHECK(back.colonyLosses.size() == 1 && back.colonyLosses[0].sysID == 272);
|
|
CHECK(back.provocations.size() == 1 && back.provocations[0].value == 450.0f);
|
|
CHECK(back.techScores.size() == 3 && back.techScores[1] == 4);
|
|
CHECK(back.autoPeaceRuns.size() == 1 && back.autoPeaceRuns[0].tn1 == 5);
|
|
|
|
Writer w2;
|
|
WriteArchive wa2(w2);
|
|
wa2.obj(A("CD"), back);
|
|
CHECK(w2.bytes() == w.bytes());
|
|
|
|
// A StreamableEnum element is a frame, not a scalar: `nalat`'s two elements must
|
|
// come back as complex nodes, which is what a bare-int typing would get wrong.
|
|
const Node* cd = &root.children[0];
|
|
const Node* nalat = nullptr;
|
|
for (const Node& n : cd->children)
|
|
if (n.name == "nalat") nalat = &n;
|
|
CHECK(nalat != nullptr);
|
|
if (nalat) {
|
|
CHECK(nalat->children.size() == 3); // the "." count plus two elements
|
|
CHECK(!nalat->children[0].is_complex());
|
|
CHECK(nalat->children[1].is_complex() && nalat->children[2].is_complex());
|
|
}
|
|
}
|
|
|
|
// CDT plus the CD frames, exactly as SaveGame drives them.
|
|
struct CdOnly {
|
|
static constexpr const char* kStreamName = "";
|
|
shapes::CdTable cdTable;
|
|
std::vector<shapes::CustomDataBlock> customData;
|
|
template <class Ar>
|
|
void io(Ar& ar) {
|
|
ar.obj(A("CDT"), cdTable);
|
|
size_t k = 0;
|
|
ar.repeat("CD", customData, [&](Ar& a, shapes::CustomDataBlock& e) {
|
|
e.select(k < cdTable.ids.size() ? cdTable.ids[k] : std::string());
|
|
++k;
|
|
a.obj(A("CD"), e);
|
|
});
|
|
}
|
|
};
|
|
|
|
// The CD frames carry no key: the id at the same ordinal in CDT selects the body.
|
|
static void test_customdata_dispatch() {
|
|
CdOnly doc;
|
|
doc.cdTable.ids = {"Player.00000016.TurnCommands_v5", "Player.00000032.AIAgent"};
|
|
shapes::CustomDataBlock tc;
|
|
tc.select(doc.cdTable.ids[0]);
|
|
tc.turnCommands.playerID = 16;
|
|
tc.turnCommands.hasResearchRate = true;
|
|
tc.turnCommands.researchRate = 0.97f;
|
|
tc.turnCommands.buildOrders.push_back({1, 608, 384, 0});
|
|
// A two-hop route: the fleet-move element ends in a COUNTED vector of system
|
|
// ids, not a fixed pair, so a multi-hop order is longer than a single-hop one.
|
|
shapes::TcFleetMove mv;
|
|
mv.fleetID = 688;
|
|
mv.route = {432, 512};
|
|
tc.turnCommands.fleetMoves.push_back(mv);
|
|
doc.customData.push_back(tc);
|
|
shapes::CustomDataBlock agent;
|
|
agent.select(doc.cdTable.ids[1]);
|
|
agent.aiAgent.fct = 9;
|
|
agent.aiAgent.techScores = {2, 4};
|
|
doc.customData.push_back(agent);
|
|
|
|
Writer w;
|
|
WriteArchive wa(w);
|
|
doc.io(wa);
|
|
Node root = read_tree(w.bytes());
|
|
std::vector<Issue> issues;
|
|
CdOnly back;
|
|
ReadArchive ra(root.children, issues, "");
|
|
back.io(ra);
|
|
CHECK_EQ(count(issues, Issue::Error), size_t(0));
|
|
CHECK(back.customData.size() == 2);
|
|
CHECK(back.customData[0].which == shapes::CustomDataBlock::Which::TurnCmds);
|
|
{
|
|
const shapes::TurnCommands& t = back.customData[0].turnCommands;
|
|
CHECK(t.playerID == 16 && t.hasResearchRate && t.researchRate == 0.97f);
|
|
CHECK(!t.hasResearchTarget && !t.hasResearchBoost && !t.hasCivilianRatios);
|
|
CHECK(t.buildOrders.size() == 1 && t.buildOrders[0].b == 608 && t.buildOrders[0].c == 384);
|
|
CHECK(t.fleetMoves.size() == 1 && t.fleetMoves[0].fleetID == 688);
|
|
CHECK(t.fleetMoves[0].route.size() == 2 && t.fleetMoves[0].route[1] == 512);
|
|
CHECK(t.extra.empty()); // the 27 lists account for the whole tail
|
|
}
|
|
CHECK(back.customData[1].which == shapes::CustomDataBlock::Which::AIAgent);
|
|
CHECK(back.customData[1].aiAgent.fct == 9 && back.customData[1].aiAgent.techScores.size() == 2);
|
|
Writer w2;
|
|
WriteArchive wa2(w2);
|
|
back.io(wa2);
|
|
CHECK(w2.bytes() == w.bytes());
|
|
|
|
// The same bytes with the ids swapped select the other way round: nothing in the
|
|
// CD frame itself says which body it holds.
|
|
shapes::CustomDataBlock probe;
|
|
probe.select("Player.00000032.AIAgentX");
|
|
CHECK(probe.which == shapes::CustomDataBlock::Which::Unknown);
|
|
probe.select(".AIAgent");
|
|
CHECK(probe.which == shapes::CustomDataBlock::Which::AIAgent);
|
|
probe.select("Player.00000016.TurnCommands_v5");
|
|
CHECK(probe.which == shapes::CustomDataBlock::Which::TurnCmds);
|
|
// The layout is only known for _v5. A different version must fall back to the
|
|
// carried Node rather than be decoded with a stale shape.
|
|
probe.select("Player.00000016.TurnCommands_v6");
|
|
CHECK(probe.which == shapes::CustomDataBlock::Which::Unknown);
|
|
}
|
|
|
|
// --- 9. gzip container ---------------------------------------------------------------
|
|
static void test_gzip() {
|
|
Bytes data;
|
|
for (int i = 0; i < 5000; ++i) data.push_back(uint8_t(i * 7));
|
|
Bytes gz = gzip(data.data(), data.size());
|
|
CHECK(is_gzip(gz.data(), gz.size()));
|
|
CHECK(gunzip(gz.data(), gz.size()) == data);
|
|
CHECK(inflate_container(data.data(), data.size()) == data); // not gzip: passthrough
|
|
bool threw = false;
|
|
try {
|
|
Bytes bad = gz;
|
|
bad[bad.size() / 2] ^= 0xff;
|
|
gunzip(bad.data(), bad.size());
|
|
} catch (const GzipError&) {
|
|
threw = true;
|
|
}
|
|
CHECK(threw);
|
|
}
|
|
|
|
// --- 10. dump formatting: Python float repr and JSON quoting ---------------------------
|
|
static void test_dump_format() {
|
|
CHECK_EQ(py_float_repr(240.0), std::string("240.0"));
|
|
CHECK_EQ(py_float_repr(1.0), std::string("1.0"));
|
|
CHECK_EQ(py_float_repr(0.0), std::string("0.0"));
|
|
CHECK_EQ(py_float_repr(double(3.4028235e38f)), std::string("3.4028234663852886e+38"));
|
|
CHECK_EQ(py_float_repr(double(bits_f32(0x0000002a))), std::string("5.885453550164232e-44"));
|
|
CHECK_EQ(py_float_repr(1e-05), std::string("1e-05"));
|
|
CHECK_EQ(py_float_repr(0.0001), std::string("0.0001"));
|
|
CHECK_EQ(py_float_repr(1e16), std::string("1e+16"));
|
|
CHECK_EQ(py_float_repr(1234567890123456.0), std::string("1234567890123456.0"));
|
|
CHECK_EQ(py_float_repr(double(-0.18487215f)), std::string("-0.18487215042114258"));
|
|
CHECK_EQ(py_float_repr(double(2.4307494f)), std::string("2.4307494163513184"));
|
|
CHECK_EQ(json_quote_cp1252("a\"b\\c\n"), std::string("\"a\\\"b\\\\c\\n\""));
|
|
CHECK_EQ(json_quote_cp1252(std::string("Kor\x92Voth")), std::string("\"Kor\\u2019Voth\""));
|
|
CHECK_EQ(json_quote_cp1252(std::string("\x81")), std::string("\"\\ufffd\""));
|
|
CHECK_EQ(json_quote_cp1252(std::string("caf\xe9")), std::string("\"caf\\u00e9\""));
|
|
}
|
|
|
|
int main() {
|
|
test_primitives_bytes();
|
|
test_frames();
|
|
test_resync();
|
|
test_cp1252();
|
|
test_hints();
|
|
test_raw_frame();
|
|
test_typed_summary();
|
|
test_typed_conditionals();
|
|
test_typed_string_array();
|
|
test_svsctob_variants();
|
|
test_aiagent_block();
|
|
test_customdata_dispatch();
|
|
test_gzip();
|
|
test_dump_format();
|
|
std::printf("test_stream: %s (%d failures)\n", fails ? "FAILED" : "ok", fails);
|
|
return fails ? 1 : 0;
|
|
}
|