sots-engine/tests/game_config/unit_tests.cpp

258 lines
12 KiB
C++

// 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 <cmath>
#include <cstdio>
#include <cstring>
#include <string>
#include <vector>
#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<float>(static_cast<double>(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<float>(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<Pair>& p, std::vector<std::pair<std::string, std::string>> 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<Slot> 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<std::string> log;
auto lg = [&log](const std::string& l) { log.push_back(l); };
ExternWriter ext = [](Slot& s, std::string_view v) { *static_cast<std::string*>(s.storage) = std::string(v); };
{
Table t;
std::vector<Slot> 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<Slot> 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<Slot> 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<Slot> 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<Slot> 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<Slot> 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<Slot> 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<Slot> 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<Slot> 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<Slot> 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;
}