summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTor Andersson <tor@ccxvii.net>2025-04-28 22:09:29 +0200
committerTor Andersson <tor@ccxvii.net>2025-04-29 01:16:25 +0200
commit48e39e44dbe267f8945e9d597e61fd8aa3dfb376 (patch)
treec75e854fadc20d827cd5b422c5ab0f1a45cdf1d2
parent7a93787dfe5cdaba3eed98ed8edd19674186430b (diff)
downloadserver-48e39e44dbe267f8945e9d597e61fd8aa3dfb376.tar.gz
Improved fuzzing.
-rwxr-xr-xbin/rtt-fuzz13
-rwxr-xr-xbin/rtt-fuzz-rand32
-rwxr-xr-xbin/rtt-fuzz-rand-n12
-rwxr-xr-xbin/rtt-help1
-rwxr-xr-xbin/rtt-import3
-rwxr-xr-xbin/rtt-patch10
-rwxr-xr-xbin/rtt-run2
-rwxr-xr-xbin/rtt-show-game2
-rwxr-xr-xbin/rtt-tm-unban-tick4
-rwxr-xr-xbin/rtt-update-elo1
-rw-r--r--docs/module/fuzzer.md30
-rw-r--r--docs/module/rules.md5
-rw-r--r--docs/overview/architecture.md14
-rw-r--r--docs/server/production.md6
-rw-r--r--docs/server/toolbox.md5
-rw-r--r--package.json3
-rw-r--r--tools/fuzz.js344
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