Sped up the test suite with a parallel runner and a world cache
- run_tests.js discovers and runs each *_test.js in its own worker process (--jobs N to cap the pool, --file <name> for a single suite) - GameState caches the deterministic terrain, city sites and road network by (seed, mapConfig, civ count), rebuilding only the mutable per-game state - the snapshot ships mapConfig and the client rebuilds terrain from it, so the DOM tests can use the small map fixture and a non-default config renders right - shortestPaths can stop once every target city is settled
This commit is contained in:
@@ -48,6 +48,10 @@ export class MapView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.seed = -1;
|
this.seed = -1;
|
||||||
|
// Signature of the terrain currently built, so a snapshot with the same
|
||||||
|
// seed and config does not rebuild the world.
|
||||||
|
this._terrainSignature = null;
|
||||||
|
this.mapConfig = null;
|
||||||
this.topology = null;
|
this.topology = null;
|
||||||
this.tiles = {};
|
this.tiles = {};
|
||||||
this.terrainStats = null;
|
this.terrainStats = null;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const entityMethods = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
applySnapshot(state) {
|
applySnapshot(state) {
|
||||||
this.ensureTerrain(state.seed);
|
this.ensureTerrain(state.seed, state.mapConfig || null);
|
||||||
this.setRoads(state.roads || []);
|
this.setRoads(state.roads || []);
|
||||||
this.localCiv = state.viewer;
|
this.localCiv = state.viewer;
|
||||||
this._applyTerritory(state.territory);
|
this._applyTerritory(state.territory);
|
||||||
|
|||||||
@@ -18,16 +18,23 @@ import { OVERVIEW_BLOCK, OVERVIEW_FOG, CHUNK_SIZE, CHUNK_MARGIN, CHUNK_CACHE_LIM
|
|||||||
import { overviewColour, blendColours } from "./utils.js";
|
import { overviewColour, blendColours } from "./utils.js";
|
||||||
|
|
||||||
export const terrainMethods = {
|
export const terrainMethods = {
|
||||||
ensureTerrain(seed) {
|
ensureTerrain(seed, mapConfig = null) {
|
||||||
if (seed === this.seed) return;
|
// The snapshot carries the config the seed was generated with; fall back to
|
||||||
|
// the shared default for callers that predate it. A signature rather than a
|
||||||
|
// reference test keeps a fresh snapshot object from rebuilding the world.
|
||||||
|
const config = mapConfig || MAP_CONFIG;
|
||||||
|
const signature = `${seed}|${JSON.stringify(config)}`;
|
||||||
|
if (signature === this._terrainSignature) return;
|
||||||
|
this._terrainSignature = signature;
|
||||||
this.seed = seed;
|
this.seed = seed;
|
||||||
|
this.mapConfig = config;
|
||||||
// A new world brings a new road network; drop the old one (and the cache
|
// A new world brings a new road network; drop the old one (and the cache
|
||||||
// signature) before the chunks are rebuilt, so setRoads always installs the
|
// signature) before the chunks are rebuilt, so setRoads always installs the
|
||||||
// new tiles.
|
// new tiles.
|
||||||
this.roads = new Set();
|
this.roads = new Set();
|
||||||
this._roadsSignature = null;
|
this._roadsSignature = null;
|
||||||
this.topology = new MapTopology(MAP_CONFIG.mapSize, MAP_CONFIG.topology === "cylindrical");
|
this.topology = new MapTopology(config.mapSize, config.topology === "cylindrical");
|
||||||
const generator = new MapGenerator(MAP_CONFIG, this.topology);
|
const generator = new MapGenerator(config, this.topology);
|
||||||
generator.generate(seed);
|
generator.generate(seed);
|
||||||
this.tiles = generator.tiles;
|
this.tiles = generator.tiles;
|
||||||
this.terrainStats = new TerrainStats(this.tiles);
|
this.terrainStats = new TerrainStats(this.tiles);
|
||||||
|
|||||||
+2
-14
@@ -9,9 +9,7 @@
|
|||||||
// methods can freely call each other through `this`; the split is purely about
|
// methods can freely call each other through `this`; the split is purely about
|
||||||
// keeping each concern in a file small enough to read.
|
// keeping each concern in a file small enough to read.
|
||||||
|
|
||||||
import { MapTopology, key } from "./hex.js";
|
import { key } from "./hex.js";
|
||||||
import { MapGenerator } from "./map_generator.js";
|
|
||||||
import { TerrainStats } from "./terrain_stats.js";
|
|
||||||
import { HexPathfinder } from "./hex_pathfinder.js";
|
import { HexPathfinder } from "./hex_pathfinder.js";
|
||||||
import { randomSeed } from "./rng.js";
|
import { randomSeed } from "./rng.js";
|
||||||
import {
|
import {
|
||||||
@@ -158,17 +156,7 @@ export class GameState {
|
|||||||
configure(civs, worldSeed = 0) {
|
configure(civs, worldSeed = 0) {
|
||||||
this.civilisations = civs.slice();
|
this.civilisations = civs.slice();
|
||||||
this.seed = worldSeed !== 0 ? worldSeed | 0 : randomSeed();
|
this.seed = worldSeed !== 0 ? worldSeed | 0 : randomSeed();
|
||||||
this.topology = new MapTopology(
|
this._buildTerrain();
|
||||||
this.mapConfig.mapSize,
|
|
||||||
this.mapConfig.topology === "cylindrical"
|
|
||||||
);
|
|
||||||
this._generator = new MapGenerator(this.mapConfig, this.topology);
|
|
||||||
this._generator.generate(this.seed);
|
|
||||||
this.tiles = this._generator.tiles;
|
|
||||||
this.landCells = this._generator.landCells;
|
|
||||||
this.terrainStats = new TerrainStats(this.tiles);
|
|
||||||
this._minMovementCost = this.terrainStats.minMovementCostMultiplier();
|
|
||||||
this._maxStepLength = this._computeMaxStepLength();
|
|
||||||
|
|
||||||
this._resetCollections();
|
this._resetCollections();
|
||||||
for (let i = 0; i < this.civilisations.length; i++) {
|
for (let i = 0; i < this.civilisations.length; i++) {
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export const serializationMethods = {
|
|||||||
serializeShared() {
|
serializeShared() {
|
||||||
return {
|
return {
|
||||||
seed: this.seed,
|
seed: this.seed,
|
||||||
|
// The world-generation config the seed was generated with. The browser
|
||||||
|
// rebuilds the same terrain from it, so a game on a non-default map size
|
||||||
|
// or shape is rendered correctly instead of falling back to the default.
|
||||||
|
mapConfig: this.mapConfig,
|
||||||
totalHours: this.totalHours,
|
totalHours: this.totalHours,
|
||||||
civs: this.civilisations.map((c) => ({ id: c.id, name: c.name })),
|
civs: this.civilisations.map((c) => ({ id: c.id, name: c.name })),
|
||||||
protos: this.protoUnits.map((p) => ({ id: p.id, name: p.name })),
|
protos: this.protoUnits.map((p) => ({ id: p.id, name: p.name })),
|
||||||
|
|||||||
@@ -5,15 +5,90 @@
|
|||||||
import { ECONOMY } from "../data/economy.js";
|
import { ECONOMY } from "../data/economy.js";
|
||||||
import { ROADS } from "../data/roads.js";
|
import { ROADS } from "../data/roads.js";
|
||||||
import { buildRoadNetwork } from "../roads.js";
|
import { buildRoadNetwork } from "../roads.js";
|
||||||
|
import { MapTopology, key, parseKey } from "../hex.js";
|
||||||
|
import { MapGenerator } from "../map_generator.js";
|
||||||
|
import { TerrainStats } from "../terrain_stats.js";
|
||||||
import { Random } from "../rng.js";
|
import { Random } from "../rng.js";
|
||||||
import { key, parseKey } from "../hex.js";
|
|
||||||
|
// World generation is a pure function of (seed, mapConfig): the same inputs
|
||||||
|
// always produce the same terrain, city sites and road network. Caching the
|
||||||
|
// shape lets a new game with a known seed -- a server restart, or one test
|
||||||
|
// fixture after another -- skip regenerating the whole world every time. Only
|
||||||
|
// the immutable terrain pieces are shared; every mutable collection is rebuilt
|
||||||
|
// per GameState.
|
||||||
|
const WORLD_CACHE_LIMIT = 16;
|
||||||
|
const terrainCache = new Map();
|
||||||
|
const settlementCache = new Map();
|
||||||
|
|
||||||
|
function cachePut(cache, cacheKey, value) {
|
||||||
|
cache.set(cacheKey, value);
|
||||||
|
if (cache.size > WORLD_CACHE_LIMIT) cache.delete(cache.keys().next().value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyCoords(coords) {
|
||||||
|
return { x: coords.x, y: coords.y };
|
||||||
|
}
|
||||||
|
|
||||||
export const worldMethods = {
|
export const worldMethods = {
|
||||||
|
// Builds or reuses the topology, tiles and terrain stats for `this.seed` and
|
||||||
|
// `this.mapConfig`.
|
||||||
|
_buildTerrain() {
|
||||||
|
const cacheKey = `${this.seed}|${JSON.stringify(this.mapConfig)}`;
|
||||||
|
const cached = terrainCache.get(cacheKey);
|
||||||
|
if (cached) {
|
||||||
|
this.topology = cached.topology;
|
||||||
|
this.tiles = cached.tiles;
|
||||||
|
this.landCells = cached.landCells;
|
||||||
|
this.terrainStats = cached.terrainStats;
|
||||||
|
this._generator = cached.generator;
|
||||||
|
this._minMovementCost = cached.minMovementCost;
|
||||||
|
this._maxStepLength = cached.maxStepLength;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.topology = new MapTopology(
|
||||||
|
this.mapConfig.mapSize,
|
||||||
|
this.mapConfig.topology === "cylindrical"
|
||||||
|
);
|
||||||
|
this._generator = new MapGenerator(this.mapConfig, this.topology);
|
||||||
|
this._generator.generate(this.seed);
|
||||||
|
this.tiles = this._generator.tiles;
|
||||||
|
this.landCells = this._generator.landCells;
|
||||||
|
this.terrainStats = new TerrainStats(this.tiles);
|
||||||
|
this._minMovementCost = this.terrainStats.minMovementCostMultiplier();
|
||||||
|
this._maxStepLength = this._computeMaxStepLength();
|
||||||
|
cachePut(terrainCache, cacheKey, {
|
||||||
|
topology: this.topology,
|
||||||
|
tiles: this.tiles,
|
||||||
|
landCells: this.landCells,
|
||||||
|
terrainStats: this.terrainStats,
|
||||||
|
generator: this._generator,
|
||||||
|
minMovementCost: this._minMovementCost,
|
||||||
|
maxStepLength: this._maxStepLength,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
_placeSettlements() {
|
_placeSettlements() {
|
||||||
const capitals = this._placeCapitals(this.civilisations.length);
|
// City sites and the road network depend only on the seed, the map config
|
||||||
for (const site of this._placeCities(capitals)) this._spawnCity(site);
|
// and how many nations are playing, so they are cached alongside the
|
||||||
|
// terrain and reused when a known world is started again.
|
||||||
|
const cacheKey = `${this.seed}|${JSON.stringify(this.mapConfig)}|${this.civilisations.length}`;
|
||||||
|
const cached = settlementCache.get(cacheKey);
|
||||||
|
const capitals = cached
|
||||||
|
? cached.capitals.map(copyCoords)
|
||||||
|
: this._placeCapitals(this.civilisations.length);
|
||||||
|
const sites = cached
|
||||||
|
? cached.sites.map((site) => ({ ...site, coords: copyCoords(site.coords) }))
|
||||||
|
: this._placeCities(capitals);
|
||||||
|
for (const site of sites) this._spawnCity(site);
|
||||||
this._rebuildCityIndex();
|
this._rebuildCityIndex();
|
||||||
this._generateRoads();
|
this._generateRoads(cached ? cached.roads : null);
|
||||||
|
if (!cached) {
|
||||||
|
cachePut(settlementCache, cacheKey, {
|
||||||
|
capitals: capitals.map(copyCoords),
|
||||||
|
sites: sites.map((site) => ({ ...site, coords: copyCoords(site.coords) })),
|
||||||
|
roads: new Set(this.roads),
|
||||||
|
});
|
||||||
|
}
|
||||||
for (let i = 0; i < Math.min(this.civilisations.length, capitals.length); i++) {
|
for (let i = 0; i < Math.min(this.civilisations.length, capitals.length); i++) {
|
||||||
this._spawnUnit(capitals[i], i, this.protoUnits[0]);
|
this._spawnUnit(capitals[i], i, this.protoUnits[0]);
|
||||||
}
|
}
|
||||||
@@ -99,13 +174,17 @@ export const worldMethods = {
|
|||||||
// Lays the pre-generated road network. Cities are founded with a road, so
|
// Lays the pre-generated road network. Cities are founded with a road, so
|
||||||
// they are free nodes while the network is drawn; the chosen links are the
|
// they are free nodes while the network is drawn; the chosen links are the
|
||||||
// greedy spanner over every possible city-to-city route.
|
// greedy spanner over every possible city-to-city route.
|
||||||
_generateRoads() {
|
_generateRoads(roadKeys = null) {
|
||||||
for (const city of this.cities) {
|
for (const city of this.cities) {
|
||||||
if (!city.improvements.includes(ROADS.improvementName)) {
|
if (!city.improvements.includes(ROADS.improvementName)) {
|
||||||
city.improvements.push(ROADS.improvementName);
|
city.improvements.push(ROADS.improvementName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.roads = buildRoadNetwork(this.cities, this.topology, this.tiles);
|
// A cached world hands back the (read-only) road tiles; copy them so no
|
||||||
|
// state can alter the cache.
|
||||||
|
this.roads = roadKeys
|
||||||
|
? new Set(roadKeys)
|
||||||
|
: buildRoadNetwork(this.cities, this.topology, this.tiles);
|
||||||
},
|
},
|
||||||
|
|
||||||
_spawnUnit(center, civIndex, proto, homeCityId = null) {
|
_spawnUnit(center, civIndex, proto, homeCityId = null) {
|
||||||
|
|||||||
+23
-3
@@ -76,11 +76,17 @@ class MinHeap {
|
|||||||
// Dijkstra over the land tiles from `source`, costing each step through
|
// Dijkstra over the land tiles from `source`, costing each step through
|
||||||
// `stepCost(coords, tile)`. Returns the distance to every reachable tile and
|
// `stepCost(coords, tile)`. Returns the distance to every reachable tile and
|
||||||
// the parent needed to rebuild the route.
|
// the parent needed to rebuild the route.
|
||||||
export function shortestPaths(source, topology, tiles, stepCost) {
|
//
|
||||||
|
// `targets` (optional) is a set of canonical keys the caller actually needs:
|
||||||
|
// once every one of them is settled the search stops, which keeps the
|
||||||
|
// road-network build from flooding the whole continent from each city.
|
||||||
|
export function shortestPaths(source, topology, tiles, stepCost, targets = null) {
|
||||||
const sourceKey = key(source.x, source.y);
|
const sourceKey = key(source.x, source.y);
|
||||||
const distance = new Map([[sourceKey, 0]]);
|
const distance = new Map([[sourceKey, 0]]);
|
||||||
const parent = new Map();
|
const parent = new Map();
|
||||||
const settled = new Set();
|
const settled = new Set();
|
||||||
|
const remaining = targets ? new Set(targets) : null;
|
||||||
|
if (remaining) remaining.delete(sourceKey);
|
||||||
const heap = new MinHeap();
|
const heap = new MinHeap();
|
||||||
heap.push(0, source);
|
heap.push(0, source);
|
||||||
while (!heap.isEmpty()) {
|
while (!heap.isEmpty()) {
|
||||||
@@ -88,6 +94,10 @@ export function shortestPaths(source, topology, tiles, stepCost) {
|
|||||||
const currentKey = key(current.x, current.y);
|
const currentKey = key(current.x, current.y);
|
||||||
if (settled.has(currentKey)) continue;
|
if (settled.has(currentKey)) continue;
|
||||||
settled.add(currentKey);
|
settled.add(currentKey);
|
||||||
|
if (remaining) {
|
||||||
|
remaining.delete(currentKey);
|
||||||
|
if (remaining.size === 0 && distance.has(currentKey)) break;
|
||||||
|
}
|
||||||
for (const neighbour of topology.neighbours(current.x, current.y)) {
|
for (const neighbour of topology.neighbours(current.x, current.y)) {
|
||||||
const nk = key(neighbour.x, neighbour.y);
|
const nk = key(neighbour.x, neighbour.y);
|
||||||
const tile = tiles[nk];
|
const tile = tiles[nk];
|
||||||
@@ -154,8 +164,18 @@ export function buildRoadNetwork(cities, topology, tiles, options = {}) {
|
|||||||
for (const k of cityKeys) roads.add(k);
|
for (const k of cityKeys) roads.add(k);
|
||||||
if (cities.length < 2) return roads;
|
if (cities.length < 2) return roads;
|
||||||
|
|
||||||
// Every possible link, with the cheapest route and its cost.
|
// Every possible link, with the cheapest route and its cost. Each search may
|
||||||
const searches = cities.map((city) => shortestPaths(city.coords, topology, tiles, stepCost));
|
// stop as soon as it has reached every other city, instead of flooding the
|
||||||
|
// whole continent.
|
||||||
|
const searches = cities.map((city, index) =>
|
||||||
|
shortestPaths(
|
||||||
|
city.coords,
|
||||||
|
topology,
|
||||||
|
tiles,
|
||||||
|
stepCost,
|
||||||
|
cityKeys.filter((_, other) => other !== index)
|
||||||
|
)
|
||||||
|
);
|
||||||
const edges = [];
|
const edges = [];
|
||||||
for (let a = 0; a < cities.length; a++) {
|
for (let a = 0; a < cities.length; a++) {
|
||||||
for (let b = a + 1; b < cities.length; b++) {
|
for (let b = a + 1; b < cities.length; b++) {
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import { TestCase } from "./framework/test_case.js";
|
import { TestCase } from "./framework/test_case.js";
|
||||||
import { setupDom, teardownDom, FakeWebSocket } from "./framework/dom.js";
|
import { setupDom, teardownDom, FakeWebSocket } from "./framework/dom.js";
|
||||||
import { CIVILISATIONS } from "../shared/data.js";
|
import { CIVILISATIONS } from "../shared/data.js";
|
||||||
import { defaultState } from "./framework/helpers.js";
|
import { smallState } from "./framework/helpers.js";
|
||||||
|
|
||||||
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ export class AppTest extends TestCase {
|
|||||||
|
|
||||||
joinSocket.simulateMessage({ t: "civ_assigned", civ: 1 });
|
joinSocket.simulateMessage({ t: "civ_assigned", civ: 1 });
|
||||||
this.assertTrue(env.$("#screen-game").hasClass("active"));
|
this.assertTrue(env.$("#screen-game").hasClass("active"));
|
||||||
joinSocket.simulateMessage({ t: "game_state", state: defaultState().snapshot(1) });
|
joinSocket.simulateMessage({ t: "game_state", state: smallState().snapshot(1) });
|
||||||
this.assertEqual(env.$("#civ-name").text(), "Britain");
|
this.assertEqual(env.$("#civ-name").text(), "Britain");
|
||||||
this.assertFalse(env.$("#stat-pop").text() === "0");
|
this.assertFalse(env.$("#stat-pop").text() === "0");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -135,7 +135,7 @@ export class AppTest extends TestCase {
|
|||||||
this.assertEqual(setupMessage.setup.player_civ, 0);
|
this.assertEqual(setupMessage.setup.player_civ, 0);
|
||||||
this.assertEqual(setupMessage.setup.civilisations, ["france", "britain", "slovenia"]);
|
this.assertEqual(setupMessage.setup.civilisations, ["france", "britain", "slovenia"]);
|
||||||
|
|
||||||
socket.simulateMessage({ t: "game_state", state: defaultState(["france", "britain", "slovenia"], 4242).snapshot(0) });
|
socket.simulateMessage({ t: "game_state", state: smallState(["france", "britain", "slovenia"], 4242).snapshot(0) });
|
||||||
this.assertEqual(env.$("#civ-name").text(), "France");
|
this.assertEqual(env.$("#civ-name").text(), "France");
|
||||||
this.assertFalse(env.$("#stat-pop").text() === "0");
|
this.assertFalse(env.$("#stat-pop").text() === "0");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { TestCase } from "./framework/test_case.js";
|
import { TestCase } from "./framework/test_case.js";
|
||||||
import { setupDom, teardownDom } from "./framework/dom.js";
|
import { setupDom, teardownDom } from "./framework/dom.js";
|
||||||
import { smallState, defaultState, mapLayers } from "./framework/helpers.js";
|
import { smallState, mapLayers } from "./framework/helpers.js";
|
||||||
import { key } from "../shared/hex.js";
|
import { key } from "../shared/hex.js";
|
||||||
import { PROTO_UNITS, CIVILISATIONS, STATUS_BATTLE, BATTLE } from "../shared/data.js";
|
import { PROTO_UNITS, CIVILISATIONS, STATUS_BATTLE, BATTLE } from "../shared/data.js";
|
||||||
import { MapView } from "../client/js/map_view.js";
|
import { MapView } from "../client/js/map_view.js";
|
||||||
@@ -488,7 +488,7 @@ export class CombatTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_client_prediction_mirrors_occupancy() {
|
async test_client_prediction_mirrors_occupancy() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const pair = findEmptyLandPair(state);
|
const pair = findEmptyLandPair(state);
|
||||||
const attacker = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
|
const attacker = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
|
||||||
state._spawnUnit(pair.b, 1, state.protoUnits[0]);
|
state._spawnUnit(pair.b, 1, state.protoUnits[0]);
|
||||||
@@ -529,7 +529,7 @@ export class CombatTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_client_prediction_charges_onto_the_target() {
|
async test_client_prediction_charges_onto_the_target() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const pair = findEmptyLandPair(state);
|
const pair = findEmptyLandPair(state);
|
||||||
const artillery = state._spawnUnit(pair.a, 0, state.protoUnits[2]);
|
const artillery = state._spawnUnit(pair.a, 0, state.protoUnits[2]);
|
||||||
state._spawnUnit(pair.b, 1, state.protoUnits[0]);
|
state._spawnUnit(pair.b, 1, state.protoUnits[0]);
|
||||||
@@ -553,7 +553,7 @@ export class CombatTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_client_animates_the_charge_onto_the_enemy() {
|
async test_client_animates_the_charge_onto_the_enemy() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const pair = findEmptyLandPair(state);
|
const pair = findEmptyLandPair(state);
|
||||||
const attacker = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
|
const attacker = state._spawnUnit(pair.a, 0, state.protoUnits[0]);
|
||||||
state._spawnUnit(pair.b, 1, state.protoUnits[0]);
|
state._spawnUnit(pair.b, 1, state.protoUnits[0]);
|
||||||
@@ -577,7 +577,7 @@ export class CombatTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_client_draws_half_size_stacks_with_the_battle_icon_between_them() {
|
async test_client_draws_half_size_stacks_with_the_battle_icon_between_them() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { b } = engagement(state);
|
const { b } = engagement(state);
|
||||||
|
|
||||||
const env = await setupDom();
|
const env = await setupDom();
|
||||||
|
|||||||
+25
-25
@@ -2,7 +2,7 @@ import { TestCase } from "./framework/test_case.js";
|
|||||||
import { setupDom, teardownDom } from "./framework/dom.js";
|
import { setupDom, teardownDom } from "./framework/dom.js";
|
||||||
import { GameScreen } from "../client/js/game_screen.js";
|
import { GameScreen } from "../client/js/game_screen.js";
|
||||||
import { BUILDINGS, CIVILISATIONS } from "../shared/data.js";
|
import { BUILDINGS, CIVILISATIONS } from "../shared/data.js";
|
||||||
import { defaultState, adjacentLand } from "./framework/helpers.js";
|
import { smallState, adjacentLand } from "./framework/helpers.js";
|
||||||
|
|
||||||
function stubNetwork() {
|
function stubNetwork() {
|
||||||
return {
|
return {
|
||||||
@@ -37,7 +37,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
screen.enter();
|
screen.enter();
|
||||||
this.assertTrue(env.$("#screen-game").hasClass("active"));
|
this.assertTrue(env.$("#screen-game").hasClass("active"));
|
||||||
|
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
this.assertEqual(env.$("#civ-name").text(), "France");
|
this.assertEqual(env.$("#civ-name").text(), "France");
|
||||||
this.assertEqual(env.$("#player-name").text(), "Carol");
|
this.assertEqual(env.$("#player-name").text(), "Carol");
|
||||||
@@ -77,7 +77,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
screen.onState(defaultState().snapshot(0));
|
screen.onState(smallState().snapshot(0));
|
||||||
env.$("#civ-flag").click();
|
env.$("#civ-flag").click();
|
||||||
this.assertFalse(env.$("#modal-nation").hasClass("hidden"));
|
this.assertFalse(env.$("#modal-nation").hasClass("hidden"));
|
||||||
this.assertSize(env.$("#government-list .row-card"), 3);
|
this.assertSize(env.$("#government-list .row-card"), 3);
|
||||||
@@ -93,7 +93,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
state.budgets.set(0, 1.0e15);
|
state.budgets.set(0, 1.0e15);
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
||||||
@@ -120,7 +120,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
||||||
screen.map.onCitySelected(capital.id);
|
screen.map.onCitySelected(capital.id);
|
||||||
@@ -143,7 +143,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
state.budgets.set(0, 1.0e15);
|
state.budgets.set(0, 1.0e15);
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
||||||
@@ -173,7 +173,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
const network = stubNetwork();
|
const network = stubNetwork();
|
||||||
const screen = new GameScreen(network, makeConfig());
|
const screen = new GameScreen(network, makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
const raw = state.units.find((u) => u.civ === 0);
|
const raw = state.units.find((u) => u.civ === 0);
|
||||||
const unit = screen._unit(raw.id);
|
const unit = screen._unit(raw.id);
|
||||||
@@ -204,7 +204,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
const network = stubNetwork();
|
const network = stubNetwork();
|
||||||
const screen = new GameScreen(network, makeConfig());
|
const screen = new GameScreen(network, makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const home = state.units.find((u) => u.civ === 0);
|
const home = state.units.find((u) => u.civ === 0);
|
||||||
const artillery = state._spawnUnit(
|
const artillery = state._spawnUnit(
|
||||||
home.coords,
|
home.coords,
|
||||||
@@ -268,7 +268,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
||||||
screen.map.onTileRequested(capital.coords);
|
screen.map.onTileRequested(capital.coords);
|
||||||
@@ -320,7 +320,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
screen.onState(defaultState().snapshot(0));
|
screen.onState(smallState().snapshot(0));
|
||||||
|
|
||||||
const before = screen.map.camera.x;
|
const before = screen.map.camera.x;
|
||||||
env.$(env.window).trigger(env.$.Event("keydown", { key: "d" }));
|
env.$(env.window).trigger(env.$.Event("keydown", { key: "d" }));
|
||||||
@@ -353,7 +353,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
state.research.set(0, 1.0e6);
|
state.research.set(0, 1.0e6);
|
||||||
state.budgets.set(0, 1.0e15);
|
state.budgets.set(0, 1.0e15);
|
||||||
const method = state.technologies.findIndex((t) => t.id === "scientific_method");
|
const method = state.technologies.findIndex((t) => t.id === "scientific_method");
|
||||||
@@ -380,7 +380,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const mountain = state.landCells.find(
|
const mountain = state.landCells.find(
|
||||||
(c) => state.tiles[`${c.x},${c.y}`].terrainType === "Mountain"
|
(c) => state.tiles[`${c.x},${c.y}`].terrainType === "Mountain"
|
||||||
);
|
);
|
||||||
@@ -418,7 +418,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const plain = state.landCells.find(
|
const plain = state.landCells.find(
|
||||||
(c) => state.tiles[`${c.x},${c.y}`].terrainType === "Land" &&
|
(c) => state.tiles[`${c.x},${c.y}`].terrainType === "Land" &&
|
||||||
!state.cityAt(c) && state.topology.neighbours(c.x, c.y).some((n) => state._isLand(n))
|
!state.cityAt(c) && state.topology.neighbours(c.x, c.y).some((n) => state._isLand(n))
|
||||||
@@ -448,7 +448,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
const network = stubNetwork();
|
const network = stubNetwork();
|
||||||
const screen = new GameScreen(network, makeConfig());
|
const screen = new GameScreen(network, makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const goal = { x: 900, y: 900 };
|
const goal = { x: 900, y: 900 };
|
||||||
@@ -478,7 +478,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
const network = stubNetwork();
|
const network = stubNetwork();
|
||||||
const screen = new GameScreen(network, makeConfig());
|
const screen = new GameScreen(network, makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const goal = { x: 902, y: 902 };
|
const goal = { x: 902, y: 902 };
|
||||||
@@ -499,7 +499,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
const network = stubNetwork();
|
const network = stubNetwork();
|
||||||
const screen = new GameScreen(network, makeConfig());
|
const screen = new GameScreen(network, makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
state.requestDeclareWar(0, 1);
|
state.requestDeclareWar(0, 1);
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
@@ -522,7 +522,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
const network = stubNetwork();
|
const network = stubNetwork();
|
||||||
const screen = new GameScreen(network, makeConfig());
|
const screen = new GameScreen(network, makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
screen.onState(defaultState().snapshot(0));
|
screen.onState(smallState().snapshot(0));
|
||||||
env.$("#civ-flag").click();
|
env.$("#civ-flag").click();
|
||||||
env.$("#modal-nation .tab[data-tab=diplomacy]").click();
|
env.$("#modal-nation .tab[data-tab=diplomacy]").click();
|
||||||
const row = env.$("#diplomacy-list tbody tr").first();
|
const row = env.$("#diplomacy-list tbody tr").first();
|
||||||
@@ -541,7 +541,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
state.requestDeclareWar(0, 1);
|
state.requestDeclareWar(0, 1);
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
env.$("#news-button").click();
|
env.$("#news-button").click();
|
||||||
@@ -558,7 +558,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
// The first snapshot is history: it must not flood the feed.
|
// The first snapshot is history: it must not flood the feed.
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
this.assertSize(env.$("#game-feed .feed-line"), 0);
|
this.assertSize(env.$("#game-feed .feed-line"), 0);
|
||||||
@@ -578,7 +578,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
|
|
||||||
state.requestDeclareWar(1, 0);
|
state.requestDeclareWar(1, 0);
|
||||||
@@ -598,7 +598,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
@@ -627,7 +627,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
const network = stubNetwork();
|
const network = stubNetwork();
|
||||||
const screen = new GameScreen(network, makeConfig());
|
const screen = new GameScreen(network, makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
@@ -659,7 +659,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
const network = stubNetwork();
|
const network = stubNetwork();
|
||||||
const screen = new GameScreen(network, makeConfig());
|
const screen = new GameScreen(network, makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const capital = state.units.find((u) => u.civ === 0);
|
const capital = state.units.find((u) => u.civ === 0);
|
||||||
const infantry = state._spawnUnit(capital.coords, 0, state.protoUnits[0]);
|
const infantry = state._spawnUnit(capital.coords, 0, state.protoUnits[0]);
|
||||||
const artillery = state._spawnUnit(capital.coords, 0, state.protoUnits[2]);
|
const artillery = state._spawnUnit(capital.coords, 0, state.protoUnits[2]);
|
||||||
@@ -686,7 +686,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
const network = stubNetwork();
|
const network = stubNetwork();
|
||||||
const screen = new GameScreen(network, makeConfig());
|
const screen = new GameScreen(network, makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const coords = findEnemyTile(state);
|
const coords = findEnemyTile(state);
|
||||||
this.assertNotNull(coords, "found an enemy tile");
|
this.assertNotNull(coords, "found an enemy tile");
|
||||||
state.tilePopulation.set(`${coords.x},${coords.y}`, 100000);
|
state.tilePopulation.set(`${coords.x},${coords.y}`, 100000);
|
||||||
@@ -717,7 +717,7 @@ export class GameScreenTest extends TestCase {
|
|||||||
try {
|
try {
|
||||||
const screen = new GameScreen(stubNetwork(), makeConfig());
|
const screen = new GameScreen(stubNetwork(), makeConfig());
|
||||||
screen.enter();
|
screen.enter();
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
screen.onState(state.snapshot(0));
|
screen.onState(state.snapshot(0));
|
||||||
screen._selectUnits([unit.id]);
|
screen._selectUnits([unit.id]);
|
||||||
|
|||||||
@@ -39,6 +39,22 @@ export class GameStateTest extends TestCase {
|
|||||||
this.assertEqual(a.territory, b.territory);
|
this.assertEqual(a.territory, b.territory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The generated world is cached and reused for a repeated seed, so the
|
||||||
|
// mutable pieces must be rebuilt per game rather than shared.
|
||||||
|
test_reused_worlds_are_independent() {
|
||||||
|
const a = smallState();
|
||||||
|
const b = smallState();
|
||||||
|
const roads = b.roads.size;
|
||||||
|
const population = b.cities[0].population;
|
||||||
|
a.roads.add("999999,999999");
|
||||||
|
a.cities[0].population = -1;
|
||||||
|
a.units[0].coords = { x: 0, y: 0 };
|
||||||
|
this.assertSize(b.roads, roads, "the cached roads are copied per game");
|
||||||
|
this.assertNotEqual(b.cities[0].population, -1, "cities are not shared");
|
||||||
|
this.assertNotEqual(b.units[0].coords, a.units[0].coords, "units are not shared");
|
||||||
|
this.assertEqual(b.cities[0].population, population, "the other game is untouched");
|
||||||
|
}
|
||||||
|
|
||||||
test_foreign_territory_is_a_wall_unless_targeted() {
|
test_foreign_territory_is_a_wall_unless_targeted() {
|
||||||
const state = smallState(["france", "britain", "poland"]);
|
const state = smallState(["france", "britain", "poland"]);
|
||||||
const mover = state.units.find((u) => u.civ === 0);
|
const mover = state.units.find((u) => u.civ === 0);
|
||||||
|
|||||||
+30
-28
@@ -3,7 +3,7 @@ import { setupDom, teardownDom } from "./framework/dom.js";
|
|||||||
import { MapView, inwardOffset } from "../client/js/map_view.js";
|
import { MapView, inwardOffset } from "../client/js/map_view.js";
|
||||||
import { mapToLocal, COL_STEP } from "../shared/hex.js";
|
import { mapToLocal, COL_STEP } from "../shared/hex.js";
|
||||||
import { CIVILISATIONS, PROTO_UNITS } from "../shared/data.js";
|
import { CIVILISATIONS, PROTO_UNITS } from "../shared/data.js";
|
||||||
import { defaultState, mapLayers, adjacentLand, seaTile } from "./framework/helpers.js";
|
import { defaultState, smallState, mapLayers, adjacentLand, seaTile } from "./framework/helpers.js";
|
||||||
|
|
||||||
export class MapViewTest extends TestCase {
|
export class MapViewTest extends TestCase {
|
||||||
async build(state) {
|
async build(state) {
|
||||||
@@ -18,6 +18,8 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_renders_terrain_territory_fog_and_entities() {
|
async test_renders_terrain_territory_fog_and_entities() {
|
||||||
|
// A full-size map, so the viewport genuinely covers only part of it and the
|
||||||
|
// chunking is exercised; the other view tests use the small fixture.
|
||||||
const state = defaultState();
|
const state = defaultState();
|
||||||
const { env, map, snapshot } = await this.build(state);
|
const { env, map, snapshot } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
@@ -39,7 +41,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_renders_the_pre_generated_road_network() {
|
async test_renders_the_pre_generated_road_network() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map, snapshot } = await this.build(state);
|
const { env, map, snapshot } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
this.assertSize(map.roads, snapshot.roads.length, "the snapshot's road tiles are mirrored");
|
this.assertSize(map.roads, snapshot.roads.length, "the snapshot's road tiles are mirrored");
|
||||||
@@ -53,7 +55,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_unit_views_show_their_owners_flag() {
|
async test_unit_views_show_their_owners_flag() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env } = await this.build(state);
|
const { env } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const flags = env.$("#layer-entities .unit .unit-flag");
|
const flags = env.$("#layer-entities .unit .unit-flag");
|
||||||
@@ -68,7 +70,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_centers_on_the_capital() {
|
async test_centers_on_the_capital() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
||||||
@@ -82,7 +84,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_tile_at_screen_round_trips() {
|
async test_tile_at_screen_round_trips() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
const capital = state.cities.find((c) => c.civ === 0 && c.isCapital);
|
||||||
@@ -96,7 +98,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_wrapped_tile_lookup_stays_in_range() {
|
async test_wrapped_tile_lookup_stays_in_range() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const coords = map.tileAtScreen(100000, 300);
|
const coords = map.tileAtScreen(100000, 300);
|
||||||
@@ -109,7 +111,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_terrain_wraps_across_the_seam() {
|
async test_terrain_wraps_across_the_seam() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
this.assertGreater(map._period, 0, "the map has a wrap period");
|
this.assertGreater(map._period, 0, "the map has a wrap period");
|
||||||
@@ -134,7 +136,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_chunks_meet_seamlessly_at_the_wrap() {
|
async test_chunks_meet_seamlessly_at_the_wrap() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
// Sit on the seam (just past the east edge) so both the east-edge chunk
|
// Sit on the seam (just past the east edge) so both the east-edge chunk
|
||||||
@@ -166,7 +168,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_dragging_wraps_with_transforms_not_layout() {
|
async test_dragging_wraps_with_transforms_not_layout() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const rec = map._chunks.values().next().value;
|
const rec = map._chunks.values().next().value;
|
||||||
@@ -186,7 +188,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_wrap_shift_picks_the_copy_nearest_the_camera() {
|
async test_wrap_shift_picks_the_copy_nearest_the_camera() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const period = map._period;
|
const period = map._period;
|
||||||
@@ -201,7 +203,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_zooming_out_switches_to_the_overview() {
|
async test_zooming_out_switches_to_the_overview() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
this.assertFalse(map.lod, "detailed at close zoom");
|
this.assertFalse(map.lod, "detailed at close zoom");
|
||||||
@@ -240,7 +242,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_neighbouring_borders_are_inset_not_overlaid() {
|
async test_neighbouring_borders_are_inset_not_overlaid() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
// Bring the two-tile territory into view so its chunk is materialised.
|
// Bring the two-tile territory into view so its chunk is materialised.
|
||||||
@@ -267,7 +269,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_selecting_a_unit_marks_its_view() {
|
async test_selecting_a_unit_marks_its_view() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map, snapshot } = await this.build(state);
|
const { env, map, snapshot } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const unit = snapshot.units.find((u) => u.civ === 0);
|
const unit = snapshot.units.find((u) => u.civ === 0);
|
||||||
@@ -280,7 +282,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_predict_move_starts_a_local_route() {
|
async test_predict_move_starts_a_local_route() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map, snapshot } = await this.build(state);
|
const { env, map, snapshot } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const unit = snapshot.units.find((u) => u.civ === 0);
|
const unit = snapshot.units.find((u) => u.civ === 0);
|
||||||
@@ -300,7 +302,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_plan_move_computes_a_route_without_predicting() {
|
async test_plan_move_computes_a_route_without_predicting() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map, snapshot } = await this.build(state);
|
const { env, map, snapshot } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const unit = snapshot.units.find((u) => u.civ === 0);
|
const unit = snapshot.units.find((u) => u.civ === 0);
|
||||||
@@ -321,7 +323,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_plan_move_reuses_a_shared_route() {
|
async test_plan_move_reuses_a_shared_route() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map, snapshot } = await this.build(state);
|
const { env, map, snapshot } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const unit = snapshot.units.find((u) => u.civ === 0);
|
const unit = snapshot.units.find((u) => u.civ === 0);
|
||||||
@@ -344,7 +346,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_pan_moves_the_camera() {
|
async test_pan_moves_the_camera() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const beforeX = map.camera.x;
|
const beforeX = map.camera.x;
|
||||||
@@ -358,7 +360,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_syncs_and_draws_an_authoritative_path() {
|
async test_syncs_and_draws_an_authoritative_path() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const goal = adjacentLand(state, unit);
|
const goal = adjacentLand(state, unit);
|
||||||
state.requestMove(unit.id, goal);
|
state.requestMove(unit.id, goal);
|
||||||
@@ -417,7 +419,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_right_click_on_a_unit_icon_orders_the_selected_stack() {
|
async test_right_click_on_a_unit_icon_orders_the_selected_stack() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const goal = adjacentLand(state, unit);
|
const goal = adjacentLand(state, unit);
|
||||||
const target = state._spawnUnit(goal, 0, state.protoUnits[0]);
|
const target = state._spawnUnit(goal, 0, state.protoUnits[0]);
|
||||||
@@ -448,7 +450,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_left_click_selects_city_or_clears() {
|
async test_left_click_selects_city_or_clears() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map, snapshot } = await this.build(state);
|
const { env, map, snapshot } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
let chosen = null;
|
let chosen = null;
|
||||||
@@ -478,7 +480,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_city_marker_click_reports_the_city() {
|
async test_city_marker_click_reports_the_city() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map, snapshot } = await this.build(state);
|
const { env, map, snapshot } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
let chosen = null;
|
let chosen = null;
|
||||||
@@ -495,7 +497,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_clear_selection_notifies() {
|
async test_clear_selection_notifies() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const { env, map, snapshot } = await this.build(state);
|
const { env, map, snapshot } = await this.build(state);
|
||||||
try {
|
try {
|
||||||
const unit = snapshot.units.find((u) => u.civ === 0);
|
const unit = snapshot.units.find((u) => u.civ === 0);
|
||||||
@@ -513,7 +515,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_stacked_units_show_one_icon_with_a_count_badge() {
|
async test_stacked_units_show_one_icon_with_a_count_badge() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
@@ -531,7 +533,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_right_click_on_a_friendly_stack_opens_the_menu() {
|
async test_right_click_on_a_friendly_stack_opens_the_menu() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
@@ -551,7 +553,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_stack_ids_group_units_sharing_a_tile() {
|
async test_stack_ids_group_units_sharing_a_tile() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
@@ -564,7 +566,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_right_click_on_a_friendly_stack_while_selected_moves_there() {
|
async test_right_click_on_a_friendly_stack_while_selected_moves_there() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
const second = state._spawnUnit(unit.coords, 0, state.protoUnits[0]);
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
@@ -584,7 +586,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async test_right_click_on_an_enemy_stack_attacks_it() { const state = defaultState();
|
async test_right_click_on_an_enemy_stack_attacks_it() { const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const goal = adjacentLand(state, unit);
|
const goal = adjacentLand(state, unit);
|
||||||
state._spawnUnit(goal, 1, state.protoUnits[0]);
|
state._spawnUnit(goal, 1, state.protoUnits[0]);
|
||||||
@@ -605,7 +607,7 @@ export class MapViewTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_ctrl_right_click_schedules_a_destination() {
|
async test_ctrl_right_click_schedules_a_destination() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const unit = state.units.find((u) => u.civ === 0);
|
const unit = state.units.find((u) => u.civ === 0);
|
||||||
const goal = adjacentLand(state, unit);
|
const goal = adjacentLand(state, unit);
|
||||||
const { env, map } = await this.build(state);
|
const { env, map } = await this.build(state);
|
||||||
|
|||||||
+165
-116
@@ -1,127 +1,176 @@
|
|||||||
// Test runner. Instantiates every suite, calls its `test_*` methods and prints
|
// Test runner. Every `tests/*_test.js` file is discovered automatically and run
|
||||||
// a summary, exiting non-zero when anything fails. Run with:
|
// as a suite. By default the files are farmed out across a pool of worker
|
||||||
|
// processes, so the whole run is bounded by the slowest file rather than their
|
||||||
|
// sum. A single file can be run in-process for debugging.
|
||||||
//
|
//
|
||||||
// node tests/run_tests.js
|
// node tests/run_tests.js # all files, parallel
|
||||||
|
// node tests/run_tests.js --jobs 4 # cap the worker pool
|
||||||
|
// node tests/run_tests.js --file map_view_test.js
|
||||||
|
//
|
||||||
|
// In worker mode (`--file`) the file's exported TestCase classes are run and a
|
||||||
|
// single machine-readable result line is printed for the parent to collect.
|
||||||
|
|
||||||
import { HexTest } from "./hex_test.js";
|
import { readdirSync } from "node:fs";
|
||||||
import { NoiseTest } from "./noise_test.js";
|
import { spawn } from "node:child_process";
|
||||||
import { RngTest } from "./rng_test.js";
|
import { fileURLToPath } from "node:url";
|
||||||
import { DataTest } from "./data_test.js";
|
import { dirname } from "node:path";
|
||||||
import { MapGeneratorTest } from "./map_generator_test.js";
|
import { cpus } from "node:os";
|
||||||
import { HexPathfinderTest } from "./hex_pathfinder_test.js";
|
|
||||||
import { TerrainStatsTest } from "./terrain_stats_test.js";
|
|
||||||
import { RoadsTest } from "./roads_test.js";
|
|
||||||
import { EconomyTest } from "./economy_test.js";
|
|
||||||
import { TextFormatTest } from "./text_format_test.js";
|
|
||||||
import { GameClockTest } from "./game_clock_test.js";
|
|
||||||
import { LoginManagerTest } from "./login_manager_test.js";
|
|
||||||
import { RulesTest } from "./rules_test.js";
|
|
||||||
import { CombatTest } from "./combat_test.js";
|
|
||||||
import { StackingTest, PillageTest } from "./stacking_test.js";
|
|
||||||
import { SiegeTest } from "./siege_test.js";
|
|
||||||
import { AttackOrderTest, ScheduledOrderTest } from "./orders_test.js";
|
|
||||||
import { GameStateTest } from "./game_state_test.js";
|
|
||||||
import { GameStateBuildingsTest } from "./game_state_buildings_test.js";
|
|
||||||
import { GameStateGovernmentTest } from "./game_state_government_test.js";
|
|
||||||
import { GameStateTechnologyTest } from "./game_state_technology_test.js";
|
|
||||||
import { DiplomacyStateTest, DiplomacyServerTest } from "./diplomacy_test.js";
|
|
||||||
import { ConflictsStateTest, ConflictsModalTest } from "./conflicts_test.js";
|
|
||||||
import { GameServerTest, GameServerOrdersTest, GameServerLoginTest, GameServerLobbyTest } from "./game_server_test.js";
|
|
||||||
import { ServerStaticTest, ServerLoopTest, ServerProtocolTest } from "./server_test.js";
|
|
||||||
import { WebSocketProtocolTest } from "./websocket_test.js";
|
|
||||||
import { NetTest } from "./net_test.js";
|
|
||||||
import { MapViewTest } from "./map_view_test.js";
|
|
||||||
import { AirTest } from "./air_test.js";
|
|
||||||
import { CityModalTest, NationModalTest, ConfirmModalTest, NewsModalTest } from "./modals_test.js";
|
|
||||||
import { GameScreenTest } from "./game_screen_test.js";
|
|
||||||
import { GameFeedTest } from "./feed_test.js";
|
|
||||||
import { AppTest } from "./app_test.js";
|
|
||||||
import {
|
|
||||||
OrderStateRegressionTest,
|
|
||||||
EconomyOrderEdgesTest,
|
|
||||||
AttritionTest,
|
|
||||||
CasualtyRecordingTest,
|
|
||||||
CatalogueSplitTest,
|
|
||||||
TextFormatRegressionTest,
|
|
||||||
} from "./regression_test.js";
|
|
||||||
|
|
||||||
const SUITES = [
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
HexTest,
|
const SELF = fileURLToPath(import.meta.url);
|
||||||
NoiseTest,
|
const RESULT_MARKER = "__TEST_RESULT__";
|
||||||
RngTest,
|
const MAX_JOBS = 8;
|
||||||
DataTest,
|
|
||||||
MapGeneratorTest,
|
|
||||||
HexPathfinderTest,
|
|
||||||
TerrainStatsTest,
|
|
||||||
RoadsTest,
|
|
||||||
EconomyTest,
|
|
||||||
TextFormatTest,
|
|
||||||
GameClockTest,
|
|
||||||
LoginManagerTest,
|
|
||||||
RulesTest,
|
|
||||||
CombatTest,
|
|
||||||
StackingTest,
|
|
||||||
PillageTest,
|
|
||||||
SiegeTest,
|
|
||||||
AttackOrderTest,
|
|
||||||
ScheduledOrderTest,
|
|
||||||
GameStateTest,
|
|
||||||
GameStateBuildingsTest,
|
|
||||||
GameStateGovernmentTest,
|
|
||||||
GameStateTechnologyTest,
|
|
||||||
DiplomacyStateTest,
|
|
||||||
DiplomacyServerTest,
|
|
||||||
ConflictsStateTest,
|
|
||||||
ConflictsModalTest,
|
|
||||||
GameServerTest,
|
|
||||||
GameServerOrdersTest,
|
|
||||||
GameServerLoginTest,
|
|
||||||
GameServerLobbyTest,
|
|
||||||
ServerStaticTest,
|
|
||||||
ServerLoopTest,
|
|
||||||
ServerProtocolTest,
|
|
||||||
WebSocketProtocolTest,
|
|
||||||
NetTest,
|
|
||||||
MapViewTest,
|
|
||||||
AirTest,
|
|
||||||
CityModalTest,
|
|
||||||
NationModalTest,
|
|
||||||
ConfirmModalTest,
|
|
||||||
NewsModalTest,
|
|
||||||
GameScreenTest,
|
|
||||||
GameFeedTest,
|
|
||||||
AppTest,
|
|
||||||
OrderStateRegressionTest,
|
|
||||||
EconomyOrderEdgesTest,
|
|
||||||
AttritionTest,
|
|
||||||
CasualtyRecordingTest,
|
|
||||||
CatalogueSplitTest,
|
|
||||||
TextFormatRegressionTest,
|
|
||||||
];
|
|
||||||
|
|
||||||
let passed = 0;
|
function flagValue(args, flag) {
|
||||||
const failures = [];
|
const inline = args.find((arg) => arg.startsWith(`${flag}=`));
|
||||||
|
if (inline) return inline.slice(flag.length + 1);
|
||||||
|
const index = args.indexOf(flag);
|
||||||
|
return index >= 0 ? args[index + 1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
for (const Suite of SUITES) {
|
// Every test file in this directory, in a stable order.
|
||||||
const instance = new Suite();
|
function discoverFiles() {
|
||||||
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(instance))
|
return readdirSync(HERE)
|
||||||
.filter((name) => name.startsWith("test_") && name !== "constructor")
|
.filter((name) => name.endsWith("_test.js"))
|
||||||
.sort();
|
.sort();
|
||||||
for (const method of methods) {
|
}
|
||||||
try {
|
|
||||||
await instance[method]();
|
// The TestCase classes a module exports, with their sorted test methods. Using
|
||||||
passed += 1;
|
// the exports rather than a hand-written list means a new test file is picked
|
||||||
} catch (error) {
|
// up with no runner edit.
|
||||||
failures.push(`${Suite.name}::${method}: ${error.message}`);
|
function testClasses(module) {
|
||||||
|
const classes = [];
|
||||||
|
for (const value of Object.values(module)) {
|
||||||
|
if (typeof value !== "function" || !value.prototype) continue;
|
||||||
|
const methods = Object.getOwnPropertyNames(value.prototype)
|
||||||
|
.filter((name) => name.startsWith("test_") && name !== "constructor")
|
||||||
|
.sort();
|
||||||
|
if (methods.length > 0) classes.push({ ctor: value, methods });
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runFile(file) {
|
||||||
|
const started = Date.now();
|
||||||
|
const module = await import(new URL(file, import.meta.url));
|
||||||
|
let passed = 0;
|
||||||
|
const failures = [];
|
||||||
|
for (const { ctor, methods } of testClasses(module)) {
|
||||||
|
for (const method of methods) {
|
||||||
|
const instance = new ctor();
|
||||||
|
try {
|
||||||
|
await instance[method]();
|
||||||
|
passed += 1;
|
||||||
|
} catch (error) {
|
||||||
|
failures.push(`${ctor.name}::${method}: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return { file, passed, failures, ms: Date.now() - started };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("");
|
// Worker mode: run one file, then exit. When spawned by the parent it prints a
|
||||||
if (failures.length === 0) {
|
// machine-readable marker; run by hand it prints the usual summary. The write
|
||||||
console.log(`PASS: ${passed} test(s) passed.`);
|
// callback guarantees output reaches the parent before the process ends.
|
||||||
process.exit(0);
|
async function runWorker(file) {
|
||||||
|
const result = await runFile(file);
|
||||||
|
const code = result.failures.length === 0 ? 0 : 1;
|
||||||
|
if (process.env.TISMO_WORKER === "1") {
|
||||||
|
const line = `\n${RESULT_MARKER}${JSON.stringify(result)}\n`;
|
||||||
|
process.stdout.write(line, () => process.exit(code));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
if (result.failures.length === 0) console.log(`PASS: ${result.passed} test(s) passed.`);
|
||||||
|
else {
|
||||||
|
console.log(`FAIL: ${result.passed} passed, ${result.failures.length} failed.`);
|
||||||
|
for (const failure of result.failures) console.log(` - ${failure}`);
|
||||||
|
}
|
||||||
|
process.exit(code);
|
||||||
}
|
}
|
||||||
console.log(`FAIL: ${passed} passed, ${failures.length} failed.`);
|
|
||||||
for (const failure of failures) console.log(` - ${failure}`);
|
function runChild(file) {
|
||||||
process.exit(1);
|
return new Promise((resolve) => {
|
||||||
|
const started = Date.now();
|
||||||
|
const child = spawn(process.execPath, [SELF, "--file", file], {
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
env: { ...process.env, TISMO_WORKER: "1" },
|
||||||
|
});
|
||||||
|
let out = "";
|
||||||
|
let err = "";
|
||||||
|
child.stdout.on("data", (chunk) => (out += chunk));
|
||||||
|
child.stderr.on("data", (chunk) => (err += chunk));
|
||||||
|
child.on("close", (code) => {
|
||||||
|
const index = out.lastIndexOf(RESULT_MARKER);
|
||||||
|
if (index >= 0) {
|
||||||
|
const line = out.slice(index + RESULT_MARKER.length).split("\n")[0];
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(line));
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// fall through to the failure record below
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve({
|
||||||
|
file,
|
||||||
|
passed: 0,
|
||||||
|
failures: [`${file}: worker produced no result (exit ${code}) ${err.trim()}`],
|
||||||
|
ms: Date.now() - started,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs `worker` over `items`, never more than `jobs` at a time. Results keep
|
||||||
|
// their input order so the summary is deterministic.
|
||||||
|
async function pool(items, jobs, worker) {
|
||||||
|
const results = new Array(items.length);
|
||||||
|
let next = 0;
|
||||||
|
const run = async () => {
|
||||||
|
while (next < items.length) {
|
||||||
|
const index = next;
|
||||||
|
next += 1;
|
||||||
|
results[index] = await worker(items[index]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await Promise.all(Array.from({ length: Math.min(jobs, items.length) }, run));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
const fileArg = flagValue(args, "--file");
|
||||||
|
if (fileArg) {
|
||||||
|
await runWorker(fileArg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requested = Number(flagValue(args, "--jobs")) || cpus().length;
|
||||||
|
const jobs = Math.max(1, Math.min(requested, MAX_JOBS));
|
||||||
|
const files = discoverFiles();
|
||||||
|
const results = await pool(files, jobs, runChild);
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
const failures = [];
|
||||||
|
for (const result of results) {
|
||||||
|
passed += result.passed;
|
||||||
|
failures.push(...result.failures);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
for (const result of results.slice().sort((a, b) => b.ms - a.ms)) {
|
||||||
|
const seconds = (result.ms / 1000).toFixed(1).padStart(5);
|
||||||
|
const status = result.failures.length === 0 ? "ok " : "FAIL";
|
||||||
|
console.log(`${status} ${seconds}s ${result.file}`);
|
||||||
|
}
|
||||||
|
console.log("");
|
||||||
|
if (failures.length === 0) {
|
||||||
|
console.log(`PASS: ${passed} test(s) passed.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
console.log(`FAIL: ${passed} passed, ${failures.length} failed.`);
|
||||||
|
for (const failure of failures) console.log(` - ${failure}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { TestCase } from "./framework/test_case.js";
|
import { TestCase } from "./framework/test_case.js";
|
||||||
import { setupDom, teardownDom } from "./framework/dom.js";
|
import { setupDom, teardownDom } from "./framework/dom.js";
|
||||||
import { smallState, defaultState, mapLayers, SEED } from "./framework/helpers.js";
|
import { smallState, mapLayers, SEED } from "./framework/helpers.js";
|
||||||
import { parseKey } from "../shared/hex.js";
|
import { parseKey } from "../shared/hex.js";
|
||||||
import { CIVILISATIONS, PROTO_UNITS, statusById, STATUS_ENCIRCLED, STATUS_BESIEGED } from "../shared/data.js";
|
import { CIVILISATIONS, PROTO_UNITS, statusById, STATUS_ENCIRCLED, STATUS_BESIEGED } from "../shared/data.js";
|
||||||
import { MapView } from "../client/js/map_view.js";
|
import { MapView } from "../client/js/map_view.js";
|
||||||
@@ -168,7 +168,7 @@ export class SiegeTest extends TestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async test_the_client_draws_status_icons_over_units_and_cities() {
|
async test_the_client_draws_status_icons_over_units_and_cities() {
|
||||||
const state = defaultState();
|
const state = smallState();
|
||||||
const city = findBesiegableCity(state, 0);
|
const city = findBesiegableCity(state, 0);
|
||||||
this.assertNotNull(city, "found a city to besiege");
|
this.assertNotNull(city, "found a city to besiege");
|
||||||
const unit = state._spawnUnit(city.coords, 0, state.protoUnits[0]);
|
const unit = state._spawnUnit(city.coords, 0, state.protoUnits[0]);
|
||||||
|
|||||||
Reference in New Issue
Block a user