sots-engine/tests/shim_trace/test_diff.cpp

256 lines
11 KiB
C++

// Snapshot / Scratch / diff rules (TRACE_FORMAT.md section 6), mirrored on tracecmp.compare_tv.
#include <cmath>
#include <cstring>
#include <limits>
#include <string>
#include <vector>
#include "check.h"
#include "shim/trace/tracer.h"
using namespace shim::trace;
static std::vector<DiffEntry> diff(const Tv& a, const Tv& b, HookPolicy pol = HookPolicy{}) {
std::vector<DiffEntry> out;
diff_tv(a, b, pol, "ret", out);
return out;
}
static std::string why(const std::vector<DiffEntry>& d, std::size_t i = 0) { return i < d.size() ? d[i].why : "<none>"; }
static std::string path(const std::vector<DiffEntry>& d, std::size_t i = 0) { return i < d.size() ? d[i].path : "<none>"; }
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<std::uintptr_t>(0x10)), tv::ptr(static_cast<std::uintptr_t>(0x20))).empty());
CHECK_STR(why(diff(tv::ptr(static_cast<std::uintptr_t>(0)), tv::ptr(static_cast<std::uintptr_t>(0x20)))), "exact");
CHECK_STR(why(diff(tv::ptr(static_cast<std::uintptr_t>(0x20)), tv::ptr(static_cast<std::uintptr_t>(0)))), "exact");
CHECK(diff(tv::ptr(static_cast<std::uintptr_t>(0)), tv::ptr(static_cast<std::uintptr_t>(0))).empty());
HookPolicy exact;
exact.ptr_exact = true;
CHECK_STR(why(diff(tv::ptr(static_cast<std::uintptr_t>(0x10)), tv::ptr(static_cast<std::uintptr_t>(0x20)), exact)), "exact");
CHECK(diff(tv::ptr(static_cast<std::uintptr_t>(0x10)), tv::ptr(static_cast<std::uintptr_t>(0x10)), exact).empty());
}
static void floats() {
const float nanf = std::numeric_limits<float>::quiet_NaN();
const double inf = std::numeric_limits<double>::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<float>(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<Tv> v) { return tv::list(std::move(v)); };
auto S = [](std::vector<Tv> 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<std::size_t>(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<SideEntry> sa;
std::vector<OursSide> 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<DiffEntry> d;
CHECK(diff_outputs(tv::boolean(true), sa, tv::boolean(true), sb, HookPolicy{}, d));
CHECK_EQ(d.size(), static_cast<std::size_t>(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<const std::uint8_t*>(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<std::size_t>(4));
mem[2] = 9;
Snapshot after = before.recapture(r);
CHECK_EQ(static_cast<int>(before.data[2]), 3); // the snapshot is a copy
CHECK_EQ(static_cast<int>(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<std::size_t>(1));
CHECK_EQ(s.size(0), static_cast<std::size_t>(4));
s.as<std::uint8_t>(0)[0] = 42;
CHECK_EQ(static_cast<int>(mem[0]), 1);
CHECK_EQ(static_cast<int>(before.data[0]), 1);
CHECK_EQ(static_cast<int>(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");
}