sots-engine/tests/game_config/manifest_tests.cpp

191 lines
7.6 KiB
C++

// game::config manifest tests: the token-pair loop (comments, CRLF, the dropped trailing
// entry, atoi typing, truncation), the path spellings, and the MSVC 2010 sort replica.
// All samples are hand-written.
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <random>
#include <string>
#include <vector>
#include "game/config/manifest_loader.h"
#include "game/config/msvc_sort.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 void test_atoi() {
CHECK(manifest_atoi("12") == 12);
CHECK(manifest_atoi("-7") == -7);
CHECK(manifest_atoi("+3") == 3);
CHECK(manifest_atoi(" 41x") == 41);
CHECK(manifest_atoi("abc") == 0);
CHECK(manifest_atoi("") == 0);
CHECK(manifest_atoi("-") == 0);
CHECK(manifest_atoi("12.9") == 12);
CHECK(manifest_atoi("4294967297") == 1); // wraps modulo 2^32
}
static void test_pairs_basic() {
const std::string text =
"// Only add to this list: always use new IDs\r\n"
"// ID's need not be ordered here, but they must be unique.\r\n"
"\r\n"
"1 alpha.weapon\r\n"
"2 beta.weapon\r\n"
"// DELETED - 3\r\n"
"4 gamma.weapon\r\n"
"// REMOVED - 5 delta.weapon\r\n"
"6 epsilon.weapon\r\n";
const auto p = manifest_pairs(text);
CHECK(p.size() == 4);
if (p.size() == 4) {
CHECK(p[0].id == 1 && p[0].file == "alpha.weapon");
CHECK(p[1].id == 2 && p[1].file == "beta.weapon");
CHECK(p[2].id == 4 && p[2].file == "gamma.weapon");
CHECK(p[3].id == 6 && p[3].file == "epsilon.weapon");
}
}
static void test_pairs_trailing_newline_rule() {
// The last value token touches the end of the input: read as AtEnd, so the entry is lost.
const auto dropped = manifest_pairs("1 a.weapon\r\n2 b.weapon");
CHECK(dropped.size() == 1);
CHECK(dropped.empty() || dropped[0].id == 1);
const auto kept = manifest_pairs("1 a.weapon\r\n2 b.weapon\r\n");
CHECK(kept.size() == 2);
const auto kept_space = manifest_pairs("1 a.weapon 2 b.weapon ");
CHECK(kept_space.size() == 2); // any trailing whitespace will do
const auto lf_only = manifest_pairs("7 x.weapon\n");
CHECK(lf_only.size() == 1 && lf_only[0].id == 7);
CHECK(manifest_pairs("").empty());
CHECK(manifest_pairs(" \r\n\r\n").empty());
CHECK(manifest_pairs("// just a comment\r\n").empty());
}
static void test_pairs_odd_tokens() {
// An odd token count: the trailing id is read (Ok), then no file -> loop ends.
const auto p = manifest_pairs("1 a.weapon\r\n2\r\n");
CHECK(p.size() == 1);
// Non-numeric first token: atoi gives 0, the pair is still produced.
const auto z = manifest_pairs("zz a.weapon\r\n");
CHECK(z.size() == 1 && z[0].id == 0 && z[0].file == "a.weapon");
// Tokens are whatever the tokenizer yields: a file name is any non-whitespace run.
const auto q = manifest_pairs("5 \"quoted name.weapon\"\r\n");
CHECK(q.size() == 1 && q[0].file == "quoted name.weapon");
// A comment glued to the id: `//` only counts at a token start.
const auto g = manifest_pairs("9//c x.weapon\r\n");
CHECK(g.size() == 1 && g[0].id == 9 && g[0].file == "x.weapon");
}
static void test_pairs_truncation() {
std::string longname(300, 'n');
const auto p = manifest_pairs("3 " + longname + "\r\n");
CHECK(p.size() == 1 && p[0].file.size() == kManifestTokenMax);
std::string longid = "12" + std::string(300, '9');
const auto q = manifest_pairs(longid + " f\r\n");
CHECK(q.size() == 1); // the id is atoi of the truncated digits (wraps); the pair survives
}
static void test_paths() {
CHECK(weapon_path("can_am.weapon") == "Weapons/can_am.weapon");
CHECK(section_manifest_path("Species/Human/sections") == "Species/Human/sections/_shipsections.txt");
CHECK(section_path("Species/Human/sections", "CRArmor.shipsection") == "Species/Human/sections/CRArmor.shipsection");
CHECK(std::strcmp(kWeaponManifest, "Weapons/_weapons.txt") == 0);
CHECK(kSpeciesCount == 7);
}
// ---- the sort replica ----------------------------------------------------------------------
struct Item {
int key;
int tag; // identity, to see where ties land
};
static bool key_less(const Item& a, const Item& b) { return a.key < b.key; }
static bool is_sorted_by_key(const std::vector<Item>& v) {
for (std::size_t i = 1; i < v.size(); ++i)
if (v[i].key < v[i - 1].key) return false;
return true;
}
static void test_sort_distinct() {
std::mt19937 rng(12345);
for (int n : {0, 1, 2, 3, 31, 32, 33, 40, 41, 64, 123, 500, 1000}) {
std::vector<Item> v;
for (int i = 0; i < n; ++i) v.push_back(Item{i, i});
std::shuffle(v.begin(), v.end(), rng);
std::vector<Item> ref = v;
msvc10::sort(v.begin(), v.end(), key_less);
std::sort(ref.begin(), ref.end(), key_less);
bool same = v.size() == ref.size();
for (std::size_t i = 0; same && i < v.size(); ++i) same = v[i].key == ref[i].key && v[i].tag == ref[i].tag;
CHECK(same);
}
}
static void test_sort_ties() {
std::mt19937 rng(777);
for (int n : {5, 32, 33, 123, 300}) {
std::vector<Item> v;
for (int i = 0; i < n; ++i) v.push_back(Item{(i * 7) % 11, i});
std::shuffle(v.begin(), v.end(), rng);
std::vector<Item> again = v;
msvc10::sort(v.begin(), v.end(), key_less);
msvc10::sort(again.begin(), again.end(), key_less);
CHECK(is_sorted_by_key(v));
bool same = true;
for (std::size_t i = 0; i < v.size(); ++i) same = same && v[i].tag == again[i].tag;
CHECK(same); // deterministic
std::vector<int> tags;
for (const Item& it : v) tags.push_back(it.tag);
std::sort(tags.begin(), tags.end());
bool perm = true;
for (int i = 0; i < n; ++i) perm = perm && tags[static_cast<std::size_t>(i)] == i;
CHECK(perm); // a permutation, nothing lost or duplicated
}
// At or below 32 elements the replica is an insertion sort: ties keep their input order.
std::vector<Item> small;
for (int i = 0; i < 20; ++i) small.push_back(Item{i % 3, i});
msvc10::sort(small.begin(), small.end(), key_less);
bool stable = true;
for (std::size_t i = 1; i < small.size(); ++i)
if (small[i].key == small[i - 1].key && small[i].tag < small[i - 1].tag) stable = false;
CHECK(stable);
}
static void test_sort_already_sorted_and_reversed() {
for (int n : {33, 100, 123}) {
std::vector<Item> v;
for (int i = 0; i < n; ++i) v.push_back(Item{i, i});
msvc10::sort(v.begin(), v.end(), key_less);
CHECK(is_sorted_by_key(v));
std::reverse(v.begin(), v.end());
msvc10::sort(v.begin(), v.end(), key_less);
CHECK(is_sorted_by_key(v));
std::vector<Item> all_same(static_cast<std::size_t>(n), Item{4, 0});
for (int i = 0; i < n; ++i) all_same[static_cast<std::size_t>(i)].tag = i;
msvc10::sort(all_same.begin(), all_same.end(), key_less);
CHECK(is_sorted_by_key(all_same));
}
}
int main() {
test_atoi();
test_pairs_basic();
test_pairs_trailing_newline_rule();
test_pairs_odd_tokens();
test_pairs_truncation();
test_paths();
test_sort_distinct();
test_sort_ties();
test_sort_already_sorted_and_reversed();
std::printf("%d passed, %d failed\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}