// Snapshot / Scratch / diff rules (TRACE_FORMAT.md section 6), mirrored on tracecmp.compare_tv. #include #include #include #include #include #include "check.h" #include "shim/trace/tracer.h" using namespace shim::trace; static std::vector diff(const Tv& a, const Tv& b, HookPolicy pol = HookPolicy{}) { std::vector out; diff_tv(a, b, pol, "ret", out); return out; } static std::string why(const std::vector& d, std::size_t i = 0) { return i < d.size() ? d[i].why : ""; } static std::string path(const std::vector& d, std::size_t i = 0) { return i < d.size() ? d[i].path : ""; } static void scalars() { CHECK(diff(tv::boolean(true), tv::boolean(true)).empty()); CHECK_STR(why(diff(tv::boolean(true), tv::boolean(false))), "exact"); CHECK_STR(why(diff(tv::i32(1), tv::u32(1))), "type"); // i32 vs u32 is a type divergence CHECK_STR(why(diff(tv::i32(1), tv::i32(2))), "exact"); CHECK(diff(tv::u64(1ull << 60), tv::u64(1ull << 60)).empty()); CHECK_STR(why(diff(tv::str("a"), tv::str("b"))), "exact"); CHECK(diff(tv::str("R\xe9"), tv::str("R\xe9")).empty()); const std::uint16_t w1[] = {1, 2}, w2[] = {1, 3}; CHECK_STR(why(diff(tv::wstr(w1, 2), tv::wstr(w2, 2))), "exact"); CHECK_STR(why(diff(tv::str("a"), tv::wstr(w1, 1))), "type"); CHECK(diff(tv::null(), tv::null()).empty()); CHECK_STR(why(diff(tv::null(), tv::i32(0))), "type"); CHECK(diff(tv::enum_(2, "A"), tv::enum_(2, "B")).empty()); // symbolic name is not compared CHECK_STR(why(diff(tv::enum_(2), tv::enum_(3))), "exact"); CHECK(diff(tv::json("{\"a\":1}"), tv::json("{\"a\":1}")).empty()); CHECK_STR(why(diff(tv::json("{\"a\":1}"), tv::json("{\"a\":2}"))), "exact"); } static void pointers() { // ignored by default; null vs non-null always diverges; exact when the policy says so CHECK(diff(tv::ptr(static_cast(0x10)), tv::ptr(static_cast(0x20))).empty()); CHECK_STR(why(diff(tv::ptr(static_cast(0)), tv::ptr(static_cast(0x20)))), "exact"); CHECK_STR(why(diff(tv::ptr(static_cast(0x20)), tv::ptr(static_cast(0)))), "exact"); CHECK(diff(tv::ptr(static_cast(0)), tv::ptr(static_cast(0))).empty()); HookPolicy exact; exact.ptr_exact = true; CHECK_STR(why(diff(tv::ptr(static_cast(0x10)), tv::ptr(static_cast(0x20)), exact)), "exact"); CHECK(diff(tv::ptr(static_cast(0x10)), tv::ptr(static_cast(0x10)), exact).empty()); } static void floats() { const float nanf = std::numeric_limits::quiet_NaN(); const double inf = std::numeric_limits::infinity(); CHECK(diff(tv::f32(nanf), tv::f32(nanf)).empty()); // nan == nan CHECK_STR(why(diff(tv::f32(nanf), tv::f32(0.f))), "exact"); CHECK(diff(tv::f64(inf), tv::f64(inf)).empty()); CHECK_STR(why(diff(tv::f64(inf), tv::f64(-inf))), "exact"); CHECK_STR(why(diff(tv::f32(1.f), tv::f64(1.0))), "type"); // never mix widths // the f32 rounding rule: a value stored as f32 compares at float32 precision CHECK(diff(tv::f32(0.1f), tv::f32(static_cast(0.1))).empty()); CHECK_STR(why(diff(tv::f32(1.0f), tv::f32(1.0f + 1e-5f))), "exact"); HookPolicy abs; abs.ftol = 1e-3; CHECK(diff(tv::f32(1.0f), tv::f32(1.0f + 1e-5f), abs).empty()); // mkfixture case #7 passes abs 1e-3 CHECK_STR(why(diff(tv::f32(1.0f), tv::f32(1.5f), abs)), "ftol"); // and reports "ftol" when a tolerance is set CHECK_STR(why(diff(tv::f32(nanf), tv::f32(1.f), abs)), "ftol"); // nan only equals nan, whatever the tolerance HookPolicy rel; rel.ftol = 1e-6; rel.ftol_kind = "rel"; CHECK(diff(tv::f64(1e6), tv::f64(1e6 + 0.5), rel).empty()); CHECK_STR(why(diff(tv::f64(1.0), tv::f64(1.0 + 1e-5), rel)), "ftol"); HookPolicy ulp; ulp.ftol = 2; ulp.ftol_kind = "ulp"; CHECK(diff(tv::f32(1.0f), tv::f32(std::nextafter(1.0f, 2.0f)), ulp).empty()); CHECK(diff(tv::f32(1.0f), tv::f32(std::nextafter(std::nextafter(1.0f, 2.0f), 2.0f)), ulp).empty()); CHECK_STR(why(diff(tv::f32(1.0f), tv::f32(std::nextafter(std::nextafter(std::nextafter(1.0f, 2.0f), 2.0f), 2.0f)), ulp)), "ftol"); CHECK(diff(tv::f32(-0.0f), tv::f32(0.0f), ulp).empty()); // -0 == 0 CHECK(diff(tv::f64(1.0), tv::f64(std::nextafter(1.0, 2.0)), ulp).empty()); } static void bytes() { const std::uint8_t a[4] = {1, 2, 3, 4}, b[4] = {1, 2, 9, 4}, c[3] = {1, 2, 3}; CHECK(diff(tv::bytes(a, 4, 256), tv::bytes(a, 4, 256)).empty()); auto d = diff(tv::bytes(a, 4, 256), tv::bytes(b, 4, 256)); CHECK_STR(why(d), "hash"); CHECK_EQ(d.empty() ? -1 : d[0].first_diff_offset, 2); // first differing byte offset (inline) d = diff(tv::bytes(a, 4, 256), tv::bytes(c, 3, 256)); CHECK_STR(why(d), "len"); d = diff(tv::bytes(a, 4, 2), tv::bytes(b, 4, 2)); // hashed (n > inline_max): no offset CHECK_STR(why(d), "hash"); CHECK_EQ(d.empty() ? 0 : d[0].first_diff_offset, -1); // same sha256 but different hex = corrupt log Tv x = tv::bytes(a, 4, 256), y = tv::bytes(a, 4, 256); y.data[0] = 7; d = diff(x, y); CHECK_STR(why(d), "hash"); CHECK(!d.empty() && d[0].note.find("corrupt") != std::string::npos); } static void containers() { auto L = [](std::vector v) { return tv::list(std::move(v)); }; auto S = [](std::vector v) { return tv::set(std::move(v)); }; CHECK(diff(L({tv::i32(1), tv::i32(2)}), L({tv::i32(1), tv::i32(2)})).empty()); auto d = diff(L({tv::i32(1), tv::i32(2)}), L({tv::i32(2), tv::i32(1)})); CHECK_STR(why(d), "exact"); CHECK_STR(path(d), "ret.v[0]"); CHECK(diff(S({tv::i32(1), tv::i32(2)}), S({tv::i32(2), tv::i32(1)})).empty()); // set order irrelevant CHECK_STR(why(diff(S({tv::i32(1), tv::i32(1)}), S({tv::i32(1), tv::i32(2)}))), "exact"); // multiset d = diff(L({tv::i32(1)}), L({tv::i32(1), tv::i32(2)})); CHECK_STR(why(d), "len"); CHECK_STR(d.empty() ? "" : d[0].orig_raw, "1"); CHECK_STR(d.empty() ? "" : d[0].ours_raw, "2"); HookPolicy un; un.unordered = {"ret"}; CHECK(diff(L({tv::i32(1), tv::i32(2)}), L({tv::i32(2), tv::i32(1)}), un).empty()); // policy makes a list a set Tv sa = tv::struct_(), sb = tv::struct_(); sa.add("id", tv::u32(1)).add("big", tv::u64(5)).add("name", tv::str("x")); sb.add("name", tv::str("x")).add("id", tv::u32(1)).add("big", tv::u64(5)); // key order irrelevant CHECK(diff(sa, sb).empty()); Tv sc = tv::struct_(); sc.add("id", tv::u32(1)).add("name", tv::str("y")).add("zz", tv::i8(0)); d = diff(sa, sc); // sorted: missing "big", extra "zz", then common keys in order -> name differs CHECK_EQ(d.size(), static_cast(3)); CHECK_STR(why(d, 0), "missing"); CHECK_STR(path(d, 0), "ret.v.big"); CHECK_STR(why(d, 1), "extra"); CHECK_STR(path(d, 1), "ret.v.zz"); CHECK_STR(why(d, 2), "exact"); CHECK_STR(path(d, 2), "ret.v.name"); // nested path: struct in list Tv e1 = tv::struct_(), e2 = tv::struct_(); e1.add("id", tv::u32(1)); e2.add("id", tv::u32(2)); d = diff(L({e1}), L({e2})); CHECK_STR(path(d), "ret.v[0].v.id"); } static void outputs() { std::vector sa; std::vector sb; SideEntry e; e.name = "cfg"; e.after = tv::i32(1); sa.push_back(e); e.name = "only_orig"; sa.push_back(e); sb.push_back({"cfg", tv::i32(2)}); sb.push_back({"only_ours", tv::i32(0)}); std::vector d; CHECK(diff_outputs(tv::boolean(true), sa, tv::boolean(true), sb, HookPolicy{}, d)); CHECK_EQ(d.size(), static_cast(3)); CHECK_STR(path(d, 0), "side.only_orig.after"); CHECK_STR(why(d, 0), "missing"); CHECK_STR(path(d, 1), "side.only_ours.after"); CHECK_STR(why(d, 1), "extra"); CHECK_STR(path(d, 2), "side.cfg.after"); CHECK_STR(why(d, 2), "exact"); d.clear(); CHECK(!diff_outputs(std::nullopt, {}, std::nullopt, {}, HookPolicy{}, d)); // void == void CHECK(diff_outputs(std::nullopt, {}, tv::i32(0), {}, HookPolicy{}, d)); // void vs value CHECK_STR(why(d), "extra"); } static Tv describe_pair(const void* p, std::size_t n, unsigned) { Tv s = tv::struct_(); const std::uint8_t* b = static_cast(p); s.add("lo", tv::u8(n > 0 ? b[0] : 0)); s.add("hi", tv::u8(n > 1 ? b[1] : 0)); return s; } static void snapshots() { std::uint8_t mem[4] = {1, 2, 3, 4}; Region r; r.name = "mem"; r.ptr = mem; r.size = sizeof mem; Snapshot before = Snapshot::capture(r); CHECK_STR(before.name, "mem"); CHECK_EQ(before.data.size(), static_cast(4)); mem[2] = 9; Snapshot after = before.recapture(r); CHECK_EQ(static_cast(before.data[2]), 3); // the snapshot is a copy CHECK_EQ(static_cast(after.data[2]), 9); // Scratch is a writable copy of `before`, independent of the live memory Scratch s({before}); CHECK_EQ(s.count(), static_cast(1)); CHECK_EQ(s.size(0), static_cast(4)); s.as(0)[0] = 42; CHECK_EQ(static_cast(mem[0]), 1); CHECK_EQ(static_cast(before.data[0]), 1); CHECK_EQ(static_cast(s.current(0).data[0]), 42); // default describe = bytes; custom describe = struct CHECK_STR(before.to_tv(256).type_name(), "bytes"); r.describe = &describe_pair; Snapshot st = Snapshot::capture(r); Tv t = st.to_tv(256); CHECK_STR(t.type_name(), "struct"); CHECK_STR(canonical(t), "{\"t\":\"struct\",\"v\":{\"lo\":{\"t\":\"u8\",\"v\":1},\"hi\":{\"t\":\"u8\",\"v\":2}}}"); // empty / null regions are fine Region z; z.name = "z"; Snapshot zs = Snapshot::capture(z); CHECK(zs.data.empty()); CHECK_STR(canonical(zs.to_tv(256)), "{\"t\":\"bytes\",\"n\":0,\"sha256\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\",\"hex\":\"\"}"); } static void config() { Config c; std::string err; CHECK(c.apply("hooks", "compare", &err)); CHECK(c.default_mode == Mode::Compare); CHECK(c.apply("hook.Game::Foo", "replace", &err)); CHECK(c.apply("hook.Game::Bar", "off", &err)); CHECK(c.apply("hook.Game::Bar", "trace", &err)); // later line wins CHECK(c.mode_for("Game::Foo") == Mode::Replace); CHECK(c.mode_for("Game::Bar") == Mode::Trace); CHECK(c.mode_for("Other") == Mode::Compare); CHECK(c.apply("trace.path", "C:\\SOTS\\x.jsonl", &err)); CHECK_STR(c.path, "C:\\SOTS\\x.jsonl"); CHECK(c.apply("trace.inline_max", "64", &err)); CHECK_EQ(c.inline_max, 64u); CHECK(c.apply("trace.flush", "lazy", &err)); CHECK(!c.flush_always); CHECK(!c.apply("unrelated", "x", &err)); // not ours err.clear(); CHECK(c.apply("hooks", "sometimes", &err)); // ours, but bad: reported, unchanged CHECK(!err.empty()); CHECK(c.default_mode == Mode::Compare); err.clear(); CHECK(c.apply("trace.inline_max", "lots", &err)); CHECK(!err.empty()); CHECK_EQ(c.inline_max, 64u); Mode m; CHECK(parse_mode("off", m) && m == Mode::Off); CHECK(!parse_mode("OFF", m)); CHECK_STR(mode_name(Mode::Replace), "replace"); } int main() { scalars(); pointers(); floats(); bytes(); containers(); outputs(); snapshots(); config(); return tracetest::finish("shim_trace_diff"); }