diff options
author | Tor Andersson <tor@ccxvii.net> | 2025-04-28 22:09:29 +0200 |
---|---|---|
committer | Tor Andersson <tor@ccxvii.net> | 2025-04-29 01:16:25 +0200 |
commit | 48e39e44dbe267f8945e9d597e61fd8aa3dfb376 (patch) | |
tree | c75e854fadc20d827cd5b422c5ab0f1a45cdf1d2 | |
parent | 7a93787dfe5cdaba3eed98ed8edd19674186430b (diff) | |
download | server-48e39e44dbe267f8945e9d597e61fd8aa3dfb376.tar.gz |
Improved fuzzing.
-rwxr-xr-x | bin/rtt-fuzz | 13 | ||||
-rwxr-xr-x | bin/rtt-fuzz-rand | 32 | ||||
-rwxr-xr-x | bin/rtt-fuzz-rand-n | 12 | ||||
-rwxr-xr-x | bin/rtt-help | 1 | ||||
-rwxr-xr-x | bin/rtt-import | 3 | ||||
-rwxr-xr-x | bin/rtt-patch | 10 | ||||
-rwxr-xr-x | bin/rtt-run | 2 | ||||
-rwxr-xr-x | bin/rtt-show-game | 2 | ||||
-rwxr-xr-x | bin/rtt-tm-unban-tick | 4 | ||||
-rwxr-xr-x | bin/rtt-update-elo | 1 | ||||
-rw-r--r-- | docs/module/fuzzer.md | 30 | ||||
-rw-r--r-- | docs/module/rules.md | 5 | ||||
-rw-r--r-- | docs/overview/architecture.md | 14 | ||||
-rw-r--r-- | docs/server/production.md | 6 | ||||
-rw-r--r-- | docs/server/toolbox.md | 5 | ||||
-rw-r--r-- | package.json | 3 | ||||
-rw-r--r-- | tools/fuzz.js | 344 |
17 files changed, 290 insertions, 197 deletions
diff --git a/bin/rtt-fuzz b/bin/rtt-fuzz index d7f2aef..464ff34 100755 --- a/bin/rtt-fuzz +++ b/bin/rtt-fuzz @@ -1,6 +1,6 @@ #!/bin/bash -TITLE=$1 +export TITLE=$1 shift if [ ! -f ./public/$TITLE/rules.js ] @@ -9,8 +9,11 @@ then exit 1 fi -mkdir -p fuzzer/corpus-$TITLE +if [ -z $(npm ls -p jsfuzz) ] +then + echo Installing "jsfuzz" package. + npm install -s --no-save jsfuzz +fi -RULES=../public/$TITLE/rules.js \ - npx jazzer tools/fuzz.js --sync fuzzer/corpus-$TITLE "$@" -- -exact_artifact_path=/dev/null | \ - tee fuzzer/log-$TITLE.txt +mkdir -p fuzzer/corpus-$TITLE +npx jsfuzz tools/fuzz.js fuzzer/corpus-$TITLE --exact-artifact-path=/dev/null | tee fuzzer/log-$TITLE.txt diff --git a/bin/rtt-fuzz-rand b/bin/rtt-fuzz-rand new file mode 100755 index 0000000..38878c3 --- /dev/null +++ b/bin/rtt-fuzz-rand @@ -0,0 +1,32 @@ +#!/usr/bin/env -S node + +"use strict" + +const fs = require("fs") +const crypto = require("crypto") + +if (process.argv.length < 3) { + console.error("rtt-fuzz-rand TITLE") + process.exit(1) +} + +process.env.TITLE = process.argv[2] + +const { fuzz } = require("../tools/fuzz.js") + +fs.mkdir("fuzzer", ()=>{}) + +if (process.argv.length > 3) { + fuzz(parseInt(process.argv[3])) +} else { + // run for an hour-ish + var i, n, a, b + for (i = 0; i < 3600; ++i) { + a = b = Date.now() + for (n = 0; b < a + 5_000; ++n) { + fuzz(crypto.randomInt(1, 2**48)) + b = Date.now() + } + console.log("# " + Math.round( (1000 * n) / (b-a) ) + " runs/second") + } +} diff --git a/bin/rtt-fuzz-rand-n b/bin/rtt-fuzz-rand-n new file mode 100755 index 0000000..7d3c969 --- /dev/null +++ b/bin/rtt-fuzz-rand-n @@ -0,0 +1,12 @@ +#!/bin/bash + +# count number of actual cores +CORES=$(lscpu --all --parse=SOCKET,CORE | grep -v '^#' | sort -u | wc -l) +# CORES=$(nproc) + +for P in $(seq $CORES) +do + ./bin/rtt-fuzz-rand $1 & +done + +wait $(jobs -p) diff --git a/bin/rtt-help b/bin/rtt-help index 5f6815c..57e42d1 100755 --- a/bin/rtt-help +++ b/bin/rtt-help @@ -16,6 +16,7 @@ game management module development foreach -- run a command for each module fuzz -- fuzz test a module + fuzz-rand -- fuzz test a module (random) game debugging show-chat -- show game chat (for moderation) diff --git a/bin/rtt-import b/bin/rtt-import index 2e4295b..53be292 100755 --- a/bin/rtt-import +++ b/bin/rtt-import @@ -1,4 +1,5 @@ #!/usr/bin/env -S node +"use strict" const fs = require("fs") const sqlite3 = require("better-sqlite3") @@ -30,7 +31,7 @@ for (let file of input) { game.setup.notice = options.notice if (game.setup.notice === undefined) - game.setup.notice = "" + game.setup.notice = file if (game.setup.options === undefined) game.setup.options = "{}" diff --git a/bin/rtt-patch b/bin/rtt-patch index ec0b795..2abb431 100755 --- a/bin/rtt-patch +++ b/bin/rtt-patch @@ -1,5 +1,7 @@ #!/usr/bin/env -S node +"use strict" + const sqlite3 = require("better-sqlite3") let db = new sqlite3("db") @@ -7,7 +9,7 @@ let db = new sqlite3("db") let select_game = db.prepare("select * from games where game_id=?") let select_replay = db.prepare("select * from game_replay where game_id=?") -let delete_replay = db.prepare("delete from game_replay where game_id=? and replay_id>?") +let delete_replay = db.prepare("delete from game_replay where game_id=?") let insert_replay = db.prepare("insert into game_replay (game_id,replay_id,role,action,arguments) values (?,?,?,?,?)") let select_last_snap = db.prepare("select max(snap_id) from game_snap where game_id=?").pluck() @@ -260,8 +262,8 @@ function patch_game(game_id, {validate_actions=true, save_snaps=true, delete_und db.exec("begin") if (need_to_rewrite) { - delete_replay.run(game_id, skip_replay_id) - for (item of replay) + delete_replay.run(game_id) + for (let item of replay) if (!item.remove) insert_replay.run(game_id, item.replay_id, item.role, item.action, item.arguments) } @@ -269,7 +271,7 @@ function patch_game(game_id, {validate_actions=true, save_snaps=true, delete_und if (save_snaps) { delete_snap.run(game_id, start_snap_id) let snap_id = start_snap_id - for (item of replay) + for (let item of replay) if (item.save) insert_snap.run(game_id, ++snap_id, item.replay_id, item.state) } diff --git a/bin/rtt-run b/bin/rtt-run index 6873e2f..b0f9b3e 100755 --- a/bin/rtt-run +++ b/bin/rtt-run @@ -1,7 +1,7 @@ #!/bin/bash while true do - nodemon --exitcrash server.js + npx nodemon --exitcrash server.js echo echo "Restarting soon!" echo "Hit Ctl-C to exit." diff --git a/bin/rtt-show-game b/bin/rtt-show-game index ece9e49..f055449 100755 --- a/bin/rtt-show-game +++ b/bin/rtt-show-game @@ -3,5 +3,5 @@ if [ -n "$1" ] then sqlite3 db "select json_remove(json_remove(state, '$.undo'), '$.log') from game_state where game_id = $1" else - echo "usage: rtt-show-state GAME" + echo "usage: rtt-show-game GAME" fi diff --git a/bin/rtt-tm-unban-tick b/bin/rtt-tm-unban-tick index 7294e81..d98a8d8 100755 --- a/bin/rtt-tm-unban-tick +++ b/bin/rtt-tm-unban-tick @@ -12,7 +12,7 @@ create temporary view tm_lift_ban_view as select user_id, name, - date(timeout_last), + date(timeout_last) as timeout_date, timeout_total, games_since_timeout, (games_since_timeout > timeout_total) and (julianday() > julianday(timeout_last)+14) as lift_ban @@ -23,7 +23,7 @@ create temporary view tm_lift_ban_view as order by lift_ban desc, timeout_last asc ; -select * from tm_lift_ban_view; +select name, timeout_date, timeout_total, games_since_timeout from tm_lift_ban_view where lift_ban; delete from tm_banned where user_id in (select user_id from tm_lift_ban_view where lift_ban) returning user_id; diff --git a/bin/rtt-update-elo b/bin/rtt-update-elo index 538964a..9e04224 100755 --- a/bin/rtt-update-elo +++ b/bin/rtt-update-elo @@ -1,4 +1,5 @@ #!/usr/bin/env -S node +"use strict" // Recompute Elo ratings from scratch! diff --git a/docs/module/fuzzer.md b/docs/module/fuzzer.md index d69b992..d576693 100644 --- a/docs/module/fuzzer.md +++ b/docs/module/fuzzer.md @@ -1,9 +1,4 @@ -# Fuzzing the Troops! - -We use [Jazzer.js](https://github.com/CodeIntelligenceTesting/jazzer.js/) -as a coverage-guided fuzzer for automatic testing of module rules. - -## What is fuzzing? +# Fuzz the Troops! Fuzzing or fuzz testing is an automated software testing technique that involves providing invalid, unexpected, or random data as inputs to a computer @@ -16,35 +11,34 @@ The fuzzer can detect the following types of errors: * Dead-end game states where no other actions are available (besides "undo"). * A game taking an excessive number of steps. This could indicate infinite loops and other logical flaws in the rules. -Work files are written to the "fuzzer" directory. +Crash dumps are written to the "fuzzer" directory. ## Running -Start the fuzzer: - - bash tools/fuzz.sh title [ jazzer options... ] +There are two fuzzers available: -This will run jazzer until you stop it or it has found too many errors. +A fuzzer that uses the "jsfuzz" package. +With this fuzzer every title gets its own "fuzzer/corpus-title" sub-directory. +The corpus helps the fuzzer find interesting game states in future runs. -To keep an eye on the crashes, you can watch the fuzzer/log-title.txt file: + rtt fuzz TITLE - tail -f fuzzer/log-title.txt +A simple fuzzer that plays completely randomly: -Each fuzzed title gets its own "fuzzer/corpus-title" sub-directory. -The corpus helps the fuzzer find interesting game states in future runs. + rtt fuzz-rand TITLE -To create a code coverage report pass the `--cov` option to fuzz.sh. +The fuzzer will run until you stop it or it has found too many errors. ## Debug When the fuzzer finds a crash, it saves the game state and replay log to a JSON file. You can import the crashed game state like so: - node tools/import-game.js fuzzer/dump-title-*.json + rtt import fuzzer/dump-title-*.json The imported games don't have snapshots. You can recreate them with the patch-game tool. - node tools/patch-game.js game_id + rtt patch GAME ## Avoidance diff --git a/docs/module/rules.md b/docs/module/rules.md index ed99188..7f9ae2d 100644 --- a/docs/module/rules.md +++ b/docs/module/rules.md @@ -170,6 +170,11 @@ There's also a global scope for the main game data (via the G namespace). --- +The state stack is implmented as a linked list (G.L is the head of the linked +list, and G.L.L is the next state down the stack, etc.) Invoking call pushes a +new state at the top of the stack; goto replaces the current top of the stack, +and end pops the stack. + ## States The "states" where we wait for user input are kept in the S table. diff --git a/docs/overview/architecture.md b/docs/overview/architecture.md index 5865de8..7601d04 100644 --- a/docs/overview/architecture.md +++ b/docs/overview/architecture.md @@ -32,24 +32,24 @@ The following files contain the code and styling for the client display: ## Tools -The "tools" directory holds a number of other useful scripts for administrating the server and debugging modules. +The "rtt" command (in bin/rtt) is useful for administrating the server and debugging modules. - bash tools/export-game.sh game_id > file.json + rtt export game_id > file.json Export full game state to a JSON file. - node tools/import-game.js file.json + rtt import file.json Import a game from an export JSON file. - node tools/patchgame.js game_id + rtt patch game_id Patch game state for one game (by replaying the action log). - node tools/patchgame.js title_id + rtt patch title_id Patch game state for all active games of one module. - bash tools/undo.sh game_id + rtt undo game_id Undo an action by removing the last entry in the replay log and running patchgame.js - bash tools/showgame.sh game_id + rtt show-game game_id Print game state JSON object for debugging. <!-- diff --git a/docs/server/production.md b/docs/server/production.md index 4344630..a559f5e 100644 --- a/docs/server/production.md +++ b/docs/server/production.md @@ -91,13 +91,13 @@ Run the archive and purge scripts as part of the backup cron job. Copy game state data of finished games into archive database. - sqlite3 < tools/archive.sql + rtt archive-backup Delete game state data of finished games over a certain age. - sqlite3 < tools/purge.sql + rtt archive-prune Restore archived game state. - bash tools/unarchive.sh game_id + rtt archive-restore game_id diff --git a/docs/server/toolbox.md b/docs/server/toolbox.md index ebbabaf..f589901 100644 --- a/docs/server/toolbox.md +++ b/docs/server/toolbox.md @@ -15,6 +15,10 @@ Alternatively, you can edit your .profile to add the server bin directory to you PATH=$PATH:$HOME/server/bin +Check that the command works by running the command to create the database: + + rtt init + ## Commands database management @@ -34,6 +38,7 @@ module development foreach -- run a command for each module fuzz -- fuzz test a module + fuzz-rand -- fuzz test a module (random) game debugging diff --git a/package.json b/package.json index 7f3414a..cd8ba48 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,5 @@ "pug": "^3.0.3", "utf-8-validate": "^6.0.5", "ws": "^8.18.1" - }, - "devDependencies": { - "@jazzer.js/core": "^2.1.0" } } diff --git a/tools/fuzz.js b/tools/fuzz.js index a547f68..e9863cb 100644 --- a/tools/fuzz.js +++ b/tools/fuzz.js @@ -1,204 +1,244 @@ "use strict" -const crypto = require('crypto') -const fs = require("fs") -const path = require("path") -const { FuzzedDataProvider } = require("@jazzer.js/core") +const fs = require("node:fs") +const crypto = require("node:crypto") -const RULES_JS_FILE = process.env.RULES || "rules.js" -const MAX_ERRORS = parseInt(process.env.MAX_ERRORS || 100) -const MAX_STEPS = parseInt(process.env.MAX_STEPS || 10000) -const TIMEOUT = parseInt(process.env.TIMEOUT || 250) +var MAX_TIMEOUT = parseInt(process.env.MAX_TIMEOUT || 250) +var MAX_ERRORS = parseInt(process.env.MAX_ERRORS || 100) +var MAX_STEPS = parseInt(process.env.MAX_STEPS || 10000) +var TITLE = process.env.TITLE +var SCENARIO = process.env.SCENARIO +var scenarios -console.log(`Loading fuzzer ${RULES_JS_FILE}`) +var rules = require("../public/" + TITLE + "/rules.js") +if (Array.isArray(rules.scenarios)) + scenarios = rules.scenarios.filter(n => !n.startsWith("Random")) +else + scenarios = Object.values(rules.scenarios).flat().filter(n => !n.startsWith("Random")) -const rules = require(RULES_JS_FILE) -const title_id = path.basename(path.dirname(RULES_JS_FILE)) +var errors = 0 -var error_count = 0 +console.log("Fuzzing", { TITLE, MAX_TIMEOUT, MAX_ERRORS, MAX_STEPS }) globalThis.RTT_FUZZER = true -exports.fuzz = function (fuzzerInputData) { - var data = new FuzzedDataProvider(fuzzerInputData) - if (data.remainingBytes < 16) { - // insufficient bytes to start - return +class MyRandomProvider { + constructor(seed) { + this.seed = seed } + get remainingBytes() { + return 1 << 20 + } + consumeIntegralInRange(min, max) { + var range = max - min + 1 + this.seed = Number(BigInt(this.seed) * 5667072534355537n % 9007199254740881n) + // this.seed = this.seed * 200105 % 34359738337 + return min + this.seed % range + } + pickValue(array) { + return array[this.consumeIntegralInRange(0, array.length - 1)] + } +} - var scenarios = Array.isArray(rules.scenarios) ? rules.scenarios : Object.values(rules.scenarios).flat() - var scenario = data.pickValue(scenarios) - if (scenario.startsWith("Random")) - return +class MyBufferProvider { + constructor(data) { + this.data = data + this.offset = 0 + } + get remainingBytes() { + return this.data.length - this.offset + } + consumeIntegralInRange(min, max) { + if (min >= max) return min + if (this.offset >= this.data.length) return min + var range = max - min + 1 + var n = Math.min(this.data.length - this.offset, Math.ceil(Math.log2(range) / 8)) + var result = this.data.readUIntBE(this.offset, n) + this.offset += n + return min + (result % range) + } + pickValue(array) { + return array[this.consumeIntegralInRange(0, array.length - 1)] + } +} - var timeout = Date.now() + TIMEOUT +function object_copy(original) { + var copy, i, n, v + if (Array.isArray(original)) { + n = original.length + copy = new Array(n) + for (i = 0; i < n; ++i) { + v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = object_copy(v) + else + copy[i] = v + } + return copy + } else { + copy = {} + for (i in original) { + v = original[i] + if (typeof v === "object" && v !== null) + copy[i] = object_copy(v) + else + copy[i] = v + } + return copy + } +} - var options = {} // TODO: randomize options +function list_roles(scenario, options) { + if (typeof rules.roles === "function") + return rules.roles(scenario, options) + return rules.roles +} + +function list_actions(R, V) { + var actions = [] + if (V.actions) { + for (var act in V.actions) { + var arg = V.actions[act] + if (act === "undo") { + // never undo + } else if (arg === 0 || arg === false) { + // disabled button + } else if (arg === 1 || arg === true) { + // enabled button + actions.push([ R, act ]) + } else if (Array.isArray(arg)) { + // action with arguments + for (arg of arg) { + if (typeof arg !== "number" && typeof arg !== "string") + throw new Error("invalid action: " + act + " " + arg) + actions.push([ R, act, arg ]) + } + } else if (typeof arg === "string") { + // julius caesar string-button + actions.push([ R, act ]) + } else { + throw new Error("invalid action: " + act + " " + arg) + } + } + } + return actions +} - var roles = rules.roles - if (typeof roles === "function") - roles = roles(scenario, options) +function fuzz(input) { + var timeout = Date.now() + MAX_TIMEOUT + var steps = 0 + + var data + if (typeof input === "number") + data = new MyRandomProvider(input) + else + data = new MyBufferProvider(input) var seed = data.consumeIntegralInRange(1, 2 ** 35 - 31) + var scenario = SCENARIO ?? data.pickValue(scenarios) + var options = {} // TODO: select random options + var roles = list_roles(scenario, options) var ctx = { - player_count: roles.length, - players: roles.map((r, ix) => ({ role: r, name: "rtt-fuzzer-" + (ix+1) })), + seed: input, + setup: { + title_id: TITLE, + scenario, + options, + player_count: roles.length, + }, + players: roles.map((r, ix) => ({ role: r, name: "Fuzz" + (ix+1) })), scenario, options, + state: null, replay: [], - state: {}, - active: null, - step: 0, } - ctx.replay.push([ null, ".setup", [ seed, scenario, options ] ]) - ctx.state = rules.setup(seed, scenario, options) + var G, R, V, actions, action, prev_G - while (ctx.state.active && ctx.state.active !== "None") { + try { + ctx.state = G = rules.setup(seed, scenario, options) + } catch (e) { + return log_crash(e, ctx) + } - // insufficient bytes to continue - if (data.remainingBytes < 16) - return + ctx.replay.push([ null, ".setup", [ seed, scenario, options ] ]) - ctx.active = ctx.state.active + while (G.active && G.active !== "None" && data.remainingBytes > 0) { // If multiple players can act, we'll pick a random player to go first. - if (Array.isArray(ctx.active)) - ctx.active = data.pickValue(ctx.active) - if (ctx.active === "Both") - ctx.active = data.pickValue(roles) + if (Array.isArray(G.active)) + R = data.pickValue(G.active) + else if (G.active === "Both") + R = data.pickValue(roles) + else + R = G.active try { - ctx.view = rules.view(ctx.state, ctx.active) + V = rules.view(G, R) + if (V.prompt && V.prompt.startsWith("TODO:")) + throw new Error(V.prompt) + actions = list_actions(R, V) + if (actions.length === 0) + throw new Error("NoMoreActions") } catch (e) { return log_crash(e, ctx) } - if (ctx.step > MAX_STEPS) - return log_crash("MaxSteps", ctx) - if (Date.now() > timeout) - return log_crash("Timeout", ctx) - - if (ctx.view.prompt && ctx.view.prompt.startsWith("TODO:")) - return log_crash(ctx.view.prompt, ctx) - - if (!ctx.view.actions) - return log_crash("NoMoreActions", ctx) - - var actions = Object.entries(ctx.view.actions).filter(([ action, args ]) => { - // remove undo from action list (useful to test for dead-ends) - if (action === "undo") - return false - // remove disabled buttons from action list - if (args === 0 || args === false) - return false - return true - }) - - if (actions.length === 0) - return log_crash("NoMoreActions", ctx) - - var [ action, args ] = data.pickValue(actions) - var arg = undefined - if (Array.isArray(args)) { - for (arg of args) { - if (typeof arg !== "number") - return log_crash(`BadActionArgs: ${action} ${JSON.stringify(args)}`, ctx) - } - arg = data.pickValue(args) - } else if (args !== 1 && args !== true) { - return log_crash(`BadActionArgs: ${action} ${JSON.stringify(args)}`, ctx) - } - - var prev_state = object_copy(ctx.state) - + action = data.pickValue(actions) + prev_G = object_copy(G) try { - ctx.state = rules.action(ctx.state, ctx.active, action, arg) + ctx.state = G = rules.action(G, action[0], action[1], action[2]) + ctx.replay.push(action) if (typeof rules.assert === "function") - rules.assert(ctx.state) + rules.assert(G) } catch (e) { - ctx.state = prev_state - return log_crash(e, ctx, action, arg) + return log_crash(e, ctx, action) } - ctx.replay.push([ ctx.active, action, arg ]) - - if (ctx.state.undo.length > 0) { - if (String(prev_state.active) !== String(ctx.state.active)) - return log_crash("UndoAfterActiveChange", ctx, action, arg) - if (prev_state.seed !== ctx.state.seed) - return log_crash("UndoAfterRandom", ctx, action, arg) + if (G.undo.length > 0) { + if (String(prev_G.active) !== String(G.active)) + return log_crash("BadUndo (active " + prev_G.active + " to " + G.active + ", " + G.undo.length + ")", ctx, action) + if (prev_G.seed !== G.seed) + return log_crash("BadUndo (seed " + prev_G.seed + " to " + G.seed + ", " + G.undo.length + ")", ctx, action) } - ctx.step += 1 - } -} + if (++steps > MAX_STEPS) + return log_crash("MaxSteps", ctx) -function log_crash(message, ctx, action = undefined, arg = undefined) { - if (message instanceof Error) - message = message.stack - - var line = `ERROR=${message}` - line += `\n\tTITLE=${title_id} ACTIVE=${ctx.active} STATE=${ctx.state?.state ?? ctx.state?.L?.P} STEP=${ctx.step}` - line += "SETUP=" + JSON.stringify(ctx.replay[0][2]) - if (action !== undefined) { - line += `\n\t\tACTION=${action}` - if (arg !== undefined) - line += JSON.stringify(arg) + if (Date.now() > timeout) + return log_crash("Timeout at " + steps + " steps", ctx) } +} - var game = { - setup: { - title_id, - scenario: ctx.scenario, - options: ctx.options, - player_count: ctx.player_count, - }, +function log_crash(message, ctx, action) { + console.log("ERROR", message) + console.log("\tSETUP", JSON.stringify(ctx.replay[0][2])) + console.log("\tSTATE", JSON.stringify(ctx.state?.state ?? ctx.state?.L?.P ?? null)) + if (ctx.state.L !== void 0) + console.log("\tSTACK", JSON.stringify(ctx.state.L)) + if (action !== void 0) + console.log("\tACTION", JSON.stringify(action)) + + var hash + if (typeof ctx.seed === "number") + hash = String(ctx.seed) + else + hash = crypto.createHash("sha1").update(ctx.seed).digest("hex") + + var json = JSON.stringify({ + setup: ctx.setup, players: ctx.players, state: ctx.state, replay: ctx.replay, - } - - var json = JSON.stringify(game) - var hash = crypto.createHash("sha1").update(json).digest("hex") - var dump = `fuzzer/dump-${title_id}-${hash}.json` + }) - line += "\n\tDUMP=" + dump + var dump = `fuzzer/${TITLE}-${hash}.json` + fs.writeFileSync(dump, json) + console.log("\trtt import", dump) - if (!fs.existsSync(dump)) { - console.log(line) - fs.writeFileSync(dump, json) - } else { - console.log(line) - } - - if (++error_count >= MAX_ERRORS) + if (++errors >= MAX_ERRORS) throw new Error("too many errors") } -function object_copy(original) { - var copy, i, n, v - if (Array.isArray(original)) { - n = original.length - copy = new Array(n) - for (i = 0; i < n; ++i) { - v = original[i] - if (typeof v === "object" && v !== null) - copy[i] = object_copy(v) - else - copy[i] = v - } - return copy - } else { - copy = {} - for (i in original) { - v = original[i] - if (typeof v === "object" && v !== null) - copy[i] = object_copy(v) - else - copy[i] = v - } - return copy - } -} +exports.fuzz = fuzz |