sots-engine/tests/shim_trace/test_emitter.cpp

425 lines
18 KiB
C++

// Emitter golden tests (byte-exact strings derived from TRACE_FORMAT.md section 4 and the
// mkfixture.py reference emitter) + a fixture file that oracle_emit.py regenerates with
// mkfixture's own emit_record() and compares byte for byte.
#include "shim/trace/emitter.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <limits>
#include <string>
#include <vector>
#include "check.h"
#include "harness.h"
using namespace shim::trace;
static std::string esc(const std::string& s) {
Buf b;
emit_esc(b, s);
return b.str();
}
static std::string escw(std::vector<std::uint16_t> u) {
Buf b;
emit_esc_w(b, u.data(), u.size());
return b.str();
}
static std::string tvs(const Tv& v) { return canonical(v); }
static const char* kSha512 = "110009dcee21620b166f3abfecb5eff7a873be729d1c2d53822e7acc5f34eb9b";
static void golden_strings() {
// section 4 rule 2; the same inputs test_tracecmp.py feeds mkfixture.esc()
CHECK_STR(esc("plain"), "\"plain\"");
CHECK_STR(esc("R\xe9sum\xe9"), "\"R\\u00e9sum\\u00e9\"");
CHECK_STR(esc("Tab\tKey"), "\"Tab\\tKey\"");
CHECK_STR(esc("Quote\"d"), "\"Quote\\\"d\"");
CHECK_STR(esc("Back\\slash"), "\"Back\\\\slash\"");
CHECK_STR(esc("nl\nx"), "\"nl\\nx\"");
CHECK_STR(esc("cr\rx"), "\"cr\\rx\"");
CHECK_STR(esc("\x01\x7f\xff\x80"), "\"\\u0001\\u007f\\u00ff\\u0080\"");
CHECK_STR(esc("\x08\x0c"), "\"\\u0008\\u000c\"");
CHECK_STR(esc(std::string("a\0b", 3)), "\"a\\u0000b\"");
CHECK_STR(escw({0x41, 0x20ac, 0x7e, 0x7f, 0xffff, 0x0a}), "\"A\\u20ac~\\u007f\\uffff\\n\"");
}
static void golden_numbers() {
CHECK_STR(tvs(tv::f32(1.5f)), "{\"t\":\"f32\",\"v\":1.5}");
CHECK_STR(tvs(tv::f32(0.1f)), "{\"t\":\"f32\",\"v\":0.100000001}");
CHECK_STR(tvs(tv::f32(-2.5e-7f)), "{\"t\":\"f32\",\"v\":-2.49999999e-07}");
CHECK_STR(tvs(tv::f32(1e30f)), "{\"t\":\"f32\",\"v\":1.00000002e+30}");
CHECK_STR(tvs(tv::f32(3.0f)), "{\"t\":\"f32\",\"v\":3}");
CHECK_STR(tvs(tv::f32(123456789.0f)), "{\"t\":\"f32\",\"v\":123456792}");
CHECK_STR(tvs(tv::f32(std::numeric_limits<float>::quiet_NaN())), "{\"t\":\"f32\",\"v\":\"nan\"}");
CHECK_STR(tvs(tv::f32(-std::numeric_limits<float>::infinity())), "{\"t\":\"f32\",\"v\":\"-inf\"}");
CHECK_STR(tvs(tv::f64(std::numeric_limits<double>::infinity())), "{\"t\":\"f64\",\"v\":\"inf\"}");
CHECK_STR(tvs(tv::f64(0.1)), "{\"t\":\"f64\",\"v\":0.10000000000000001}");
CHECK_STR(tvs(tv::f64(1e300)), "{\"t\":\"f64\",\"v\":1.0000000000000001e+300}");
CHECK_STR(tvs(tv::f64(0.0)), "{\"t\":\"f64\",\"v\":0}");
CHECK_STR(tvs(tv::f64(-0.0)), "{\"t\":\"f64\",\"v\":-0}");
CHECK_STR(tvs(tv::u64(18446744073709551615ull)), "{\"t\":\"u64\",\"v\":\"18446744073709551615\"}");
CHECK_STR(tvs(tv::u64(9007199254740992ull)), "{\"t\":\"u64\",\"v\":\"9007199254740992\"}"); // 2^53 -> string
CHECK_STR(tvs(tv::u64(9007199254740991ull)), "{\"t\":\"u64\",\"v\":9007199254740991}"); // 2^53-1 -> int
CHECK_STR(tvs(tv::i64(-9007199254740992ll)), "{\"t\":\"i64\",\"v\":\"-9007199254740992\"}");
CHECK_STR(tvs(tv::i64(-9007199254740991ll)), "{\"t\":\"i64\",\"v\":-9007199254740991}");
CHECK_STR(tvs(tv::i8(-128)), "{\"t\":\"i8\",\"v\":-128}");
CHECK_STR(tvs(tv::i16(32767)), "{\"t\":\"i16\",\"v\":32767}");
CHECK_STR(tvs(tv::i32(-3).named("value")), "{\"t\":\"i32\",\"v\":-3,\"n\":\"value\"}");
CHECK_STR(tvs(tv::u8(255)), "{\"t\":\"u8\",\"v\":255}");
CHECK_STR(tvs(tv::u16(65535)), "{\"t\":\"u16\",\"v\":65535}");
CHECK_STR(tvs(tv::u32(4294967295u)), "{\"t\":\"u32\",\"v\":4294967295}");
CHECK_STR(tvs(tv::boolean(true)), "{\"t\":\"bool\",\"v\":true}");
CHECK_STR(tvs(tv::null()), "{\"t\":\"null\",\"v\":null}");
CHECK_STR(tvs(tv::ptr(static_cast<std::uintptr_t>(0xa36fd0)).named("table")), "{\"t\":\"ptr\",\"v\":\"0x00a36fd0\",\"n\":\"table\"}");
CHECK_STR(tvs(tv::ptr(static_cast<std::uintptr_t>(0))), "{\"t\":\"ptr\",\"v\":\"0x00000000\"}");
CHECK_STR(tvs(tv::enum_(2, "MF_0").named("f")), "{\"t\":\"enum\",\"v\":2,\"name\":\"MF_0\",\"n\":\"f\"}");
CHECK_STR(tvs(tv::enum_(-1)), "{\"t\":\"enum\",\"v\":-1}");
CHECK_STR(tvs(tv::str("R\xe9sum\xe9").named("key")), "{\"t\":\"str\",\"v\":\"R\\u00e9sum\\u00e9\",\"n\":\"key\"}");
CHECK_STR(tvs(tv::str(nullptr)), "{\"t\":\"ptr\",\"v\":\"0x00000000\"}");
const std::uint16_t w[] = {0x41, 0x20ac};
CHECK_STR(tvs(tv::wstr(w, 2)), "{\"t\":\"wstr\",\"v\":\"A\\u20ac\"}");
CHECK_STR(tvs(tv::json("{\"a\":[1,2.5,\"x\"]}")), "{\"t\":\"json\",\"v\":{\"a\":[1,2.5,\"x\"]}}");
}
static void golden_bytes_and_containers() {
CHECK_STR(tvs(tv::bytes("\x00\xff", 2, 256)),
"{\"t\":\"bytes\",\"n\":2,\"sha256\":\"06eb7d6a69ee19e5fbdf749018d3d2abfa04bcbd1365db312eb86dc7169389b8\",\"hex\":\"00ff\"}");
CHECK_STR(tvs(tv::bytes("", 0, 256)),
"{\"t\":\"bytes\",\"n\":0,\"sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"hex\":\"\"}");
std::vector<std::uint8_t> big;
for (int k = 0; k < 2; ++k)
for (int i = 0; i < 256; ++i) big.push_back(static_cast<std::uint8_t>(i));
CHECK_STR(tvs(tv::bytes(big.data(), big.size(), 256)),
std::string("{\"t\":\"bytes\",\"n\":512,\"sha256\":\"") + kSha512 +
"\",\"head\":\"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f\"}");
// exactly inline_max stays inline; inline_max+1 does not
CHECK(tvs(tv::bytes(big.data(), 256, 256)).find("\"hex\"") != std::string::npos);
CHECK(tvs(tv::bytes(big.data(), 257, 256)).find("\"head\"") != std::string::npos);
// bytes never carry a name even if one is set
CHECK_STR(tvs(tv::bytes("", 0, 256).named("x")),
"{\"t\":\"bytes\",\"n\":0,\"sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"hex\":\"\"}");
std::vector<Tv> items;
items.push_back(tv::i32(1));
items.push_back(tv::str("a"));
CHECK_STR(tvs(tv::list(items)), "{\"t\":\"list\",\"v\":[{\"t\":\"i32\",\"v\":1},{\"t\":\"str\",\"v\":\"a\"}]}");
CHECK_STR(tvs(tv::set(items).named("s")), "{\"t\":\"set\",\"v\":[{\"t\":\"i32\",\"v\":1},{\"t\":\"str\",\"v\":\"a\"}],\"n\":\"s\"}");
CHECK_STR(tvs(tv::list({})), "{\"t\":\"list\",\"v\":[]}");
Tv st = tv::struct_();
st.add("id", tv::u32(7)).add("na\"me", tv::str("w7"));
CHECK_STR(tvs(st), "{\"t\":\"struct\",\"v\":{\"id\":{\"t\":\"u32\",\"v\":7},\"na\\\"me\":{\"t\":\"str\",\"v\":\"w7\"}}}");
CHECK_STR(tvs(tv::struct_()), "{\"t\":\"struct\",\"v\":{}}");
}
static void golden_records() {
Record r;
r.ts = 1;
r.hook = "X";
r.mode = Mode::Trace;
r.call_id = 900;
r.thread = 1;
Buf b;
emit_record(b, r);
// == mkfixture.invalid_lines()[0], the harness's own minimal valid record
CHECK_STR(b.str(), "{\"ts\":1,\"hook\":\"X\",\"mode\":\"trace\",\"call_id\":900,\"thread\":1,\"args\":[],\"ret\":null,\"side\":{}}\n");
Record c;
c.ts = 1;
c.hook = "X";
c.mode = Mode::Compare;
c.call_id = 4;
c.thread = 1;
c.depth = 0;
c.args.push_back(tv::i32(1).named("a"));
c.ret = tv::boolean(true);
SideEntry se;
se.name = "r";
se.before_kind = SideEntry::Value;
se.before = tv::null();
se.after = tv::i32(2);
c.side.push_back(se);
SideEntry sn;
sn.name = "q";
sn.before_kind = SideEntry::IsNull;
sn.after = tv::i32(0);
c.side.push_back(sn);
c.has_ours = true;
c.ours_ret = tv::boolean(false);
c.ours_side.push_back({"r", tv::i32(3)});
c.ours_side.push_back({"q", tv::i32(0)});
c.diverged = 1;
c.has_diff = true;
DiffEntry d;
d.path = "ret";
d.why = "exact";
d.orig = tv::boolean(true);
d.ours = tv::boolean(false);
c.diff.push_back(d);
DiffEntry d2;
d2.path = "side.r.after";
d2.why = "hash";
d2.first_diff_offset = 1;
d2.note = "x";
c.diff.push_back(d2);
DiffEntry d3;
d3.path = "side.x.after";
d3.why = "len";
d3.orig_raw = "3";
d3.ours_raw = "2";
c.diff.push_back(d3);
c.err = "e";
c.note = "n\xe9";
Buf cb;
emit_record(cb, c);
CHECK_STR(cb.str(),
"{\"ts\":1,\"hook\":\"X\",\"mode\":\"compare\",\"call_id\":4,\"thread\":1,\"depth\":0,"
"\"args\":[{\"t\":\"i32\",\"v\":1,\"n\":\"a\"}],\"ret\":{\"t\":\"bool\",\"v\":true},"
"\"side\":{\"r\":{\"before\":{\"t\":\"null\",\"v\":null},\"after\":{\"t\":\"i32\",\"v\":2}},"
"\"q\":{\"before\":null,\"after\":{\"t\":\"i32\",\"v\":0}}},"
"\"ours\":{\"ret\":{\"t\":\"bool\",\"v\":false},\"side\":{\"r\":{\"after\":{\"t\":\"i32\",\"v\":3}},"
"\"q\":{\"after\":{\"t\":\"i32\",\"v\":0}}}},\"diverged\":true,"
"\"diff\":[{\"path\":\"ret\",\"why\":\"exact\",\"orig\":{\"t\":\"bool\",\"v\":true},\"ours\":{\"t\":\"bool\",\"v\":false}},"
"{\"path\":\"side.r.after\",\"why\":\"hash\",\"orig\":null,\"ours\":null,\"first_diff_offset\":1,\"note\":\"x\"},"
"{\"path\":\"side.x.after\",\"why\":\"len\",\"orig\":3,\"ours\":2}],"
"\"err\":\"e\",\"note\":\"n\\u00e9\"}\n");
// replace record: no ours/diverged/diff; empty diff list still emits "diff":[] when flagged
Record p;
p.ts = 22;
p.hook = "Mars::ParseBlock";
p.mode = Mode::Replace;
p.call_id = 7;
p.thread = 3;
p.has_diff = true;
p.diverged = 0;
Buf pb;
emit_record(pb, p);
CHECK_STR(pb.str(), "{\"ts\":22,\"hook\":\"Mars::ParseBlock\",\"mode\":\"replace\",\"call_id\":7,\"thread\":3,\"args\":[],\"ret\":null,\"side\":{},\"diverged\":false,\"diff\":[]}\n");
Meta m;
m.build = "sots-engine test";
m.exe_sha256 = std::string(64, '0');
m.started = "2026-09-07T00:00:00Z";
m.inline_max = 256;
HookPolicy pa;
HookPolicy pb2;
pb2.ftol = 1e-6;
pb2.ftol_kind = "rel";
pb2.ptr_exact = true;
pb2.unordered = {"ret.v.items"};
m.hooks.emplace_back("Hook::A", pa);
m.hooks.emplace_back("Mars::ParseBlock", pb2);
Buf mb;
emit_meta(mb, m);
CHECK_STR(mb.str(),
"{\"meta\":{\"format\":1,\"build\":\"sots-engine test\",\"exe_sha256\":\"" + std::string(64, '0') +
"\",\"started\":\"2026-09-07T00:00:00Z\",\"inline_max\":256,\"hooks\":{"
"\"Hook::A\":{\"ftol\":0,\"ftol_kind\":\"abs\",\"ptr\":\"ignore\"},"
"\"Mars::ParseBlock\":{\"ftol\":9.9999999999999995e-07,\"ftol_kind\":\"rel\",\"ptr\":\"exact\",\"unordered\":[\"ret.v.items\"]}}}}\n");
Meta m0;
Buf m0b;
emit_meta(m0b, m0);
CHECK_STR(m0b.str(), "{\"meta\":{\"format\":1,\"build\":\"\",\"exe_sha256\":\"\",\"started\":\"\",\"inline_max\":256,\"hooks\":{}}}\n");
}
static void buf_growth() {
Buf b;
std::string big(100000, 'z');
b.put(big.data(), big.size());
b.printf("%s", big.c_str()); // > 128 chars: takes the grow path
CHECK_EQ(b.size(), static_cast<std::size_t>(200000));
CHECK(b.ok());
b.clear();
CHECK_EQ(b.size(), static_cast<std::size_t>(0));
// a 1 MiB bytes value inlines only when inline_max allows
std::vector<std::uint8_t> mb(1 << 20, 7);
Buf big1;
emit_tv(big1, tv::bytes(mb.data(), mb.size(), 1 << 20));
CHECK_EQ(big1.size(), static_cast<std::size_t>(2 * (1 << 20)) + std::string("{\"t\":\"bytes\",\"n\":1048576,\"sha256\":\"\",\"hex\":\"\"}").size() + 64);
CHECK(big1.ok());
}
// ---- oracle fixture: mirrored line for line in oracle_emit.py ----------------------------------
static std::vector<std::uint8_t> range_bytes(int n) {
std::vector<std::uint8_t> v;
for (int i = 0; i < n; ++i) v.push_back(static_cast<std::uint8_t>(i));
return v;
}
static void write_oracle_fixture(const std::string& path) {
Buf out;
Meta m;
m.build = "shim-trace test";
m.exe_sha256 = std::string(64, '0');
m.started = "2026-09-07T00:00:00Z";
m.inline_max = 256;
HookPolicy pa;
HookPolicy pb;
pb.ftol = 1e-6;
pb.ftol_kind = "rel";
pb.ptr_exact = true;
pb.unordered = {"ret.v.items"};
m.hooks.emplace_back("Hook::A", pa);
m.hooks.emplace_back("Mars::ParseBlock", pb);
emit_meta(out, m);
{ // rec0: str/i32/ptr args, bool ret, inline bytes region before/after
Record r;
r.ts = 1000; r.hook = "CfgVar_RegisterKey"; r.mode = Mode::Trace; r.call_id = 0; r.thread = 4120; r.depth = 0;
r.args.push_back(tv::str("R\xe9sum\xe9").named("key"));
r.args.push_back(tv::i32(-3).named("value"));
r.args.push_back(tv::ptr(static_cast<std::uintptr_t>(0x00a36fd0)).named("table"));
r.ret = tv::boolean(true);
auto fwd = range_bytes(16);
auto rev = fwd;
std::reverse(rev.begin(), rev.end());
SideEntry s;
s.name = "cfg_table"; s.before_kind = SideEntry::Value;
s.before = tv::bytes(fwd.data(), fwd.size(), 256);
s.after = tv::bytes(rev.data(), rev.size(), 256);
r.side.push_back(s);
emit_record(out, r);
}
{ // rec1: struct / list / set / enum / big ints / null / wstr
Record r;
r.ts = 1037; r.hook = "Manifest_Load"; r.mode = Mode::Trace; r.call_id = 1; r.thread = 4120;
r.args.push_back(tv::str("Weapons/_weapons.txt").named("path"));
r.ret = tv::i32(2).named("count");
Tv reg = tv::struct_();
std::vector<Tv> entries;
for (unsigned id : {7u, 9u}) {
Tv e = tv::struct_();
e.add("id", tv::u32(id));
e.add("name", tv::str(("w" + std::to_string(id) + ".weapon").c_str()));
entries.push_back(e);
}
reg.add("entries", tv::list(entries));
reg.add("deleted", tv::set({tv::u32(501), tv::u32(550), tv::u32(599)}));
reg.add("flags", tv::enum_(2, "MF_0"));
reg.add("big", tv::u64((1ull << 60) + 1));
reg.add("small", tv::u64(5));
reg.add("edge", tv::u64(1ull << 53));
reg.add("neg", tv::i64(-(1ll << 60)));
reg.add("nothing", tv::null());
const std::uint16_t w[] = {0x41, 0x20ac, 0x0000};
reg.add("w", tv::wstr(w, 3));
SideEntry s;
s.name = "registry";
s.after = reg;
r.side.push_back(s);
emit_record(out, r);
}
{ // rec2: every escape, json ret, f32/f64 lists, nan, before:null, hashed region
Record r;
r.ts = 1074; r.hook = "Mars::ParseBlock"; r.mode = Mode::Trace; r.call_id = 2; r.thread = 4124; r.depth = 1;
r.args.push_back(tv::str("Tab\tKey Quote\"d Back\\slash nl\nx cr\rx \x01\x7f\xff \x80").named("text"));
r.args.push_back(tv::u32(2).named("len"));
r.ret = tv::json("{\"weapon\":{\"count\":3,\"damage\":1.5,\"name\":\"Laser\\u00e9\",\"ok\":true,\"range\":2.25,\"tags\":[\"a\",\"b\"]}}");
SideEntry sc;
sc.name = "scratch";
sc.after = tv::list({tv::f32(1.5f), tv::f32(0.1f), tv::f32(-2.5e-7f), tv::f32(1e30f)});
r.side.push_back(sc);
SideEntry nb;
nb.name = "nanbox";
nb.after = tv::f32(std::numeric_limits<float>::quiet_NaN());
r.side.push_back(nb);
SideEntry inf;
inf.name = "infs";
inf.after = tv::list({tv::f64(std::numeric_limits<double>::infinity()), tv::f64(-std::numeric_limits<double>::infinity()),
tv::f64(0.1), tv::f64(1e300), tv::f64(0.0)});
r.side.push_back(inf);
auto big = range_bytes(256);
big.insert(big.end(), big.begin(), big.end());
SideEntry bl;
bl.name = "blob"; bl.before_kind = SideEntry::IsNull;
bl.after = tv::bytes(big.data(), big.size(), 256);
r.side.push_back(bl);
emit_record(out, r);
}
{ // rec3: compare, clean, small ints
Record r;
r.ts = 1111; r.hook = "CfgVar_RegisterKey"; r.mode = Mode::Compare; r.call_id = 3; r.thread = 4120;
r.args = {tv::i8(-128), tv::i16(32767), tv::u8(255), tv::u16(65535), tv::u64((1ull << 53) - 1)};
r.ret = tv::null();
r.has_ours = true;
r.ours_ret = tv::null();
r.diverged = 0;
r.has_diff = true;
emit_record(out, r);
}
{ // rec4: compare with divergences + note
Record r;
r.ts = 1148; r.hook = "Manifest_Load"; r.mode = Mode::Compare; r.call_id = 4; r.thread = 1;
r.ret = tv::i32(1);
SideEntry s;
s.name = "r";
s.after = tv::bytes("\x01\x02", 2, 256);
r.side.push_back(s);
r.has_ours = true;
r.ours_ret = tv::i32(2);
r.ours_side.push_back({"r", tv::bytes("\x01\x03", 2, 256)});
r.diverged = 1;
r.has_diff = true;
DiffEntry d1;
d1.path = "ret"; d1.why = "exact"; d1.orig = tv::i32(1); d1.ours = tv::i32(2);
r.diff.push_back(d1);
DiffEntry d2;
d2.path = "side.r.after"; d2.why = "hash"; d2.orig = tv::bytes("\x01\x02", 2, 256); d2.ours = tv::bytes("\x01\x03", 2, 256);
d2.first_diff_offset = 1;
r.diff.push_back(d2);
r.note = "hello";
emit_record(out, r);
}
{ // rec5: compare err (no ours)
Record r;
r.ts = 1185; r.hook = "X"; r.mode = Mode::Compare; r.call_id = 5; r.thread = 1;
r.diverged = 1;
r.has_diff = true;
DiffEntry d;
d.path = "call"; d.why = "err"; d.ours_raw = "\"ours threw\"";
r.diff.push_back(d);
r.err = "ours threw";
emit_record(out, r);
}
{ // rec6: replace
Record r;
r.ts = 1222; r.hook = "Y"; r.mode = Mode::Replace; r.call_id = 6; r.thread = 2;
r.args.push_back(tv::boolean(false).named("flag"));
r.ret = tv::ptr(static_cast<std::uintptr_t>(0));
emit_record(out, r);
}
CHECK(out.ok());
std::FILE* f = std::fopen(path.c_str(), "wb");
CHECK(f != nullptr);
if (f) {
std::fwrite(out.data(), 1, out.size(), f);
std::fclose(f);
}
}
int main(int argc, char** argv) {
golden_strings();
golden_numbers();
golden_bytes_and_containers();
golden_records();
buf_growth();
const std::string fixture = argc > 1 ? argv[1] : "emitter_oracle.jsonl";
write_oracle_fixture(fixture);
std::printf("oracle fixture: %s\n", fixture.c_str());
if (tracetest::harness_present()) {
// byte-exact against mkfixture.emit_record / emit_meta
const std::string script = argc > 2 ? argv[2] : "oracle_emit.py";
const int rc = tracetest::run_python(script, tracetest::harness_dir() + " " + fixture);
CHECK_EQ(rc, 0);
// and the harness reads it back as valid, with the injected divergence counted
const int tc = tracetest::run_tracecmp(fixture);
CHECK_EQ(tc, 1);
} else {
std::printf(" SKIP python oracle (harness absent)\n");
}
return tracetest::finish("shim_trace_emitter");
}