// game::config unit tests: typed scanners, colour/vec3/rect rules, the pair stream (through // mars::text::parse_flat_kv), first-occurrence-wins and the diagnostics. All samples are // hand-written. #include #include #include #include #include #include "game/config/config_loader.h" using namespace game::config; static int g_fail = 0, g_pass = 0; #define CHECK(cond) \ do { \ if (cond) ++g_pass; \ else { ++g_fail; std::printf("FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); } \ } while (0) static bool same_bits(float a, float b) { return std::memcmp(&a, &b, sizeof a) == 0; } static const double kScale = 0.017453292519943295; // pi/180 static void test_scan_int() { std::int32_t v = 77; CHECK(scan_int("10", v) && v == 10); CHECK(scan_int(" -5", v) && v == -5); CHECK(scan_int("10.7", v) && v == 10); // %d stops at the dot CHECK(scan_int("+3x", v) && v == 3); CHECK(scan_int("\t\n42", v) && v == 42); v = 77; CHECK(!scan_int("abc", v) && v == 77); // no digits: untouched CHECK(!scan_int("", v) && v == 77); CHECK(!scan_int("-", v) && v == 77); CHECK(!scan_int(".5", v) && v == 77); CHECK(scan_int("4294967296", v) && v == 0); // wraps like the CRT accumulator CHECK(scan_int("2147483648", v) && v == -2147483647 - 1); std::size_t pos = 0; CHECK(scan_int_at("1 2", pos, v) && v == 1 && pos == 1); CHECK(scan_int_at("1 2", pos, v) && v == 2 && pos == 3); CHECK(!scan_int_at("1 2", pos, v) && pos == 3); } static void test_scan_float() { float f = 9; CHECK(scan_float("0.5", f) && f == 0.5f); CHECK(scan_float("1e3", f) && f == 1000.0f); CHECK(scan_float(".5", f) && f == 0.5f); CHECK(scan_float("5.", f) && f == 5.0f); CHECK(scan_float("-.25", f) && f == -0.25f); CHECK(scan_float(" 7", f) && f == 7.0f); CHECK(scan_float("1e", f) && f == 1.0f); // dangling exponent: mantissa only CHECK(scan_float("2E+2z", f) && f == 200.0f); CHECK(scan_float("0.1", f) && same_bits(f, 0.1f)); CHECK(scan_float("3.14159265358979", f) && same_bits(f, 3.14159265358979f)); CHECK(scan_float("1.5abc", f) && f == 1.5f); f = 9; CHECK(!scan_float("abc", f) && f == 9); CHECK(!scan_float(".", f) && f == 9); CHECK(!scan_float("", f) && f == 9); CHECK(!scan_float("-e5", f) && f == 9); } static float c255(int v) { return static_cast(static_cast(v) / 255.0); } static void test_colour() { float c[4]; parse_colour("48 29 2", c); CHECK(same_bits(c[0], c255(48)) && same_bits(c[1], c255(29)) && same_bits(c[2], c255(2)) && c[3] == 1.0f); parse_colour("", c); CHECK(c[0] == 1.0f && c[1] == 1.0f && c[2] == 1.0f && c[3] == 1.0f); // all default 255 parse_colour("300 -5 0 128", c); CHECK(c[0] == 1.0f && c[1] == 0.0f && c[2] == 0.0f && same_bits(c[3], c255(128))); // clamped parse_colour("12 34", c); CHECK(same_bits(c[0], c255(12)) && same_bits(c[1], c255(34)) && c[2] == 1.0f && c[3] == 1.0f); parse_colour(" 0\t190 195 255", c); CHECK(c[0] == 0.0f && same_bits(c[1], c255(190)) && same_bits(c[2], c255(195)) && c[3] == 1.0f); parse_colour("1 2 x 4", c); // the scan stops at the first non-number; the rest default CHECK(same_bits(c[0], c255(1)) && same_bits(c[1], c255(2)) && c[2] == 1.0f && c[3] == 1.0f); float prev = -1; bool mono = true; for (int i = 0; i <= 255; ++i) { parse_colour(std::to_string(i), c); if (!(c[0] >= 0 && c[0] <= 1 && c[0] > prev)) mono = false; prev = c[0]; } CHECK(mono); } static void test_vec3_rect() { float v[3] = {9, 9, 9}; CHECK(parse_vec3("0 0 50.0", v) == 3 && v[0] == 0 && v[1] == 0 && v[2] == 50.0f); float w[3] = {9, 9, 9}; CHECK(parse_vec3("1.5 x", w) == 1 && w[0] == 1.5f && w[1] == 9 && w[2] == 9); // the rest untouched CHECK(parse_vec3("", w) == 0); std::int32_t r[4] = {9, 9, 9, 9}; CHECK(parse_rect("16 6\t122 40", r) == 4 && r[0] == 16 && r[1] == 6 && r[2] == 122 && r[3] == 40); std::int32_t q[4] = {9, 9, 9, 9}; CHECK(parse_rect("1 2.5 3 4", q) == 2 && q[0] == 1 && q[1] == 2 && q[2] == 9); // ".5" stops %d } static void test_scale() { CHECK(same_bits(scale_float(90.0f, kScale), static_cast(90.0 * kScale))); CHECK(same_bits(scale_float(3.0f, kScale), 0.052359879f)); CHECK(same_bits(scale_float(85.0f, kScale), 1.48352981f)); CHECK(same_bits(scale_float(-60.0f, kScale), -1.04719758f)); CHECK(scale_float(0.0f, kScale) == 0.0f); CHECK(scale_float(2.0f, 0.5) == 1.0f); } static void test_pairs() { auto eq = [](const std::vector& p, std::vector> want) { if (p.size() != want.size()) return false; for (std::size_t i = 0; i < p.size(); ++i) if (p[i].key != want[i].first || p[i].value != want[i].second) return false; return true; }; CHECK(eq(pairs("A 1\nB 2\n"), {{"A", "1"}, {"B", "2"}})); CHECK(eq(pairs("A 1\r\nB 2\r\n"), {{"A", "1"}, {"B", "2"}})); CHECK(eq(pairs("// c\nA 1 // t\n\n\t B\t\"x y\"\n"), {{"A", "1"}, {"B", "x y"}})); // duplicates are all delivered, in order (first-wins is applied by apply()) CHECK(eq(pairs("A 1\nA 2\na 3\n"), {{"A", "1"}, {"A", "2"}, {"a", "3"}})); // the last pair touches end-of-file: lost; a trailing blank or comment keeps it CHECK(eq(pairs("A 1\nB 2"), {{"A", "1"}})); CHECK(eq(pairs("A 1\nB 2 "), {{"A", "1"}, {"B", "2"}})); CHECK(eq(pairs("A 1\nB 2 // c"), {{"A", "1"}, {"B", "2"}})); // a key alone takes the next line's key as its value (the stream is not line-based) CHECK(eq(pairs("A\nB 2\nC 3\n"), {{"A", "B"}, {"2", "C"}})); // three tokens on a line: the third starts the next pair CHECK(eq(pairs("A 1 2\nB 3\n"), {{"A", "1"}, {"2", "B"}})); // blocks are skipped whole, nesting included; unclosed runs to the end; stray } ignored CHECK(eq(pairs("A 1\nblk {\n X 1\n sub {\n Y 2\n }\n Z 3\n}\nB 2\n"), {{"A", "1"}, {"B", "2"}})); CHECK(eq(pairs("A 1\nblk {\n X 1\n"), {{"A", "1"}})); CHECK(eq(pairs("}\nA 1\n}\nB 2\n"), {{"A", "1"}, {"B", "2"}})); CHECK(eq(pairs("vars\n{\nA 1\nB 2\n}\n"), {})); // a glued comment after a quoted value is still a comment CHECK(eq(pairs("COL \"0 0 0\"// \" 92 76 20\"\n"), {{"COL", "0 0 0"}})); CHECK(eq(pairs("E \"\"\n"), {{"E", ""}})); } struct Table { std::int32_t i = 7; float f = 1.5f; float s = 2.0f; float c[4] = {0, 0, 0, 0}; float v[3] = {0, 0, 0}; std::int32_t r[4] = {0, 0, 0, 0}; std::string str; std::vector slots() { return {Slot{"COUNT", Kind::Int, &i, nullptr, false}, Slot{"RATE", Kind::Float, &f, nullptr, false}, Slot{"ANGLE", Kind::FloatScaled, &s, nullptr, false}, Slot{"TINT", Kind::Colour, c, nullptr, false}, Slot{"OFFSET", Kind::Vec3, v, nullptr, false}, Slot{"BOX", Kind::Rect, r, nullptr, false}, Slot{"NAME", Kind::String, &str, nullptr, false}}; } }; static const unsigned kSlots = 7; static void test_apply() { std::vector log; auto lg = [&log](const std::string& l) { log.push_back(l); }; ExternWriter ext = [](Slot& s, std::string_view v) { *static_cast(s.storage) = std::string(v); }; { Table t; std::vector sl = t.slots(); Stats st = apply("Data/x.txt", "count 3\nRATE 0.25\nAngle 90\nTINT \"255 0 128\"\nOFFSET \"0 0 50.0\"\nBOX \"16 6 122 40\"\nname \"spy_node\"\n", sl, 0.5, lg, ext); CHECK(st.applied == kSlots && st.unknown == 0 && st.duplicate == 0 && st.missing == 0); CHECK(t.i == 3 && t.f == 0.25f && t.s == 45.0f); CHECK(t.c[0] == 1.0f && t.c[1] == 0.0f && same_bits(t.c[2], c255(128)) && t.c[3] == 1.0f); CHECK(t.v[2] == 50.0f && t.r[2] == 122 && t.str == "spy_node"); CHECK(log.empty()); for (const Slot& s : sl) CHECK(s.consumed); } { // first occurrence wins; the repeat is logged as multiply defined Table t; std::vector sl = t.slots(); log.clear(); Stats st = apply("f", "COUNT 1\nCOUNT 2\ncount 3\n", sl, 1, lg); CHECK(t.i == 1 && st.applied == 1 && st.duplicate == 2 && st.missing == kSlots - 1); CHECK(log.size() == 2 + kSlots - 1 && log[0] == "[f] COUNT not recognized or is multiply defined." && log[1] == "[f] count not recognized or is multiply defined."); CHECK(log[2] == "[f] RATE expected but not found."); } { // unknown keys are ignored and logged; absent keys keep their defaults Table t; std::vector sl = t.slots(); log.clear(); Stats st = apply("f", "NOPE 5\nRATE 2\n", sl, 1, lg); CHECK(st.unknown == 1 && st.applied == 1 && st.missing == kSlots - 1); CHECK(t.i == 7 && t.f == 2.0f && t.s == 2.0f && t.c[0] == 0.0f); CHECK(log[0] == "[f] NOPE not recognized or is multiply defined."); } { // a value the scanner rejects leaves an int/float word alone but still counts as consumed Table t; std::vector sl = t.slots(); Stats st = apply("f", "COUNT abc\nRATE x\n", sl, 1, nullptr); CHECK(t.i == 7 && t.f == 1.5f && st.applied == 2); CHECK(sl[0].consumed && sl[1].consumed); } { // scaled float: the word is multiplied even when the scan fails Table t; std::vector sl = t.slots(); apply("f", "ANGLE x\n", sl, 0.25, nullptr); CHECK(t.s == 0.5f); } { // string slots without an ExternWriter are left alone (but consumed) Table t; std::vector sl = t.slots(); Stats st = apply("f", "NAME foo\n", sl, 1, nullptr); CHECK(t.str.empty() && st.applied == 1 && sl[6].consumed); } { // colour with a bare (unquoted) value: only the first token is the value, the rest // become keys -- exactly the failure mode the data avoids by quoting colours Table t; std::vector sl = t.slots(); log.clear(); Stats st = apply("f", "TINT 10 20 30\nCOUNT 1\n", sl, 1, lg); CHECK(same_bits(t.c[0], c255(10)) && t.c[1] == 1.0f); CHECK(st.unknown == 1 && log[0] == "[f] 20 not recognized or is multiply defined."); // "20 30" pair CHECK(t.i == 1); } { // no trailing newline: the last pair never arrives Table t; std::vector sl = t.slots(); Stats st = apply("f", "COUNT 1\nRATE 0.5", sl, 1, nullptr); CHECK(t.i == 1 && t.f == 1.5f && st.missing == kSlots - 1); } { // a file that is one big block sets nothing Table t; std::vector sl = t.slots(); Stats st = apply("f", "vars\n{\nCOUNT 1\nRATE 0.5\n}\n", sl, 1, nullptr); CHECK(st.applied == 0 && st.missing == kSlots && t.i == 7); } { // Unknown-kind slot: never written, consumed when present std::int32_t w = 5; std::vector sl = {Slot{"W", Kind::Unknown, &w, nullptr, false}}; Stats st = apply("f", "W 9\n", sl, 1, nullptr); CHECK(w == 5 && st.applied == 1); } CHECK(width(Kind::Colour) == 16 && width(Kind::Int) == 4 && width(Kind::FloatScaled) == 4); CHECK(width(Kind::Vec3) == 12 && width(Kind::Rect) == 16 && width(Kind::String) == 24 && width(Kind::Unknown) == 4); CHECK(std::string(kind_name(Kind::Colour)) == "colour" && std::string(kind_name(Kind::FloatScaled)) == "fscaled"); CHECK(std::string(kind_name(Kind::String)) == "string" && std::string(kind_name(Kind::Vec3)) == "vec3"); } int main() { test_scan_int(); test_scan_float(); test_colour(); test_vec3_rect(); test_scale(); test_pairs(); test_apply(); std::printf("game_config unit tests: %d passed, %d failed\n", g_pass, g_fail); return g_fail ? 1 : 0; }