191 lines
7.6 KiB
C++
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;
|
|
}
|