256 lines
11 KiB
C++
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");
|
|
}
|