258 lines
12 KiB
C++
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;
|
|
}
|