Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions tests/AUDIT_VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Fresh-master audit fixes — 2026-10-22

Baseline: `fd6dcff3ac7f6a5cf57fa0a255718b4dae142642` (master after #515).

## Changes and compatibility

- LinkedList reports invalid indices and returns before accessing or mutating nodes.
Lua getters/removal return the default value after reporting an error; setters
and insertion leave the list unchanged. Valid append-at-size remains supported.
- UnitIndexer rejects same-unit reentrant deindexing. Hooks still see the old
index, can deindex other units, and retain the outer indexing-unit context.
The object marks itself as releasing before running hooks, including when
callers directly destroy its UnitIndex.
- LastOrder re-exports Unit's canonical `abortOrder()` rather than declaring a
conflicting extension. This also preserves access for NoWurst consumers and
fixes the old helper's unpause/stop/pause inversion.
- `ORDERS_TO_HOLD` now counts the entire retained history, including the newest
order. Its default of three retains three rather than four. Consumers relying
on the accidental fourth entry need to configure a larger limit. A nonpositive
setting is clamped to one retained order.

## Reproducible native fixture

The opt-in `RegressionAuditTests` package tests public APIs using real unit orders
and native timers. `RegressionAbortOrderConsumer` deliberately imports NoWurst and
LastOrder: the same fixture can exercise the old helper and its replacement.
Ordinary LastOrderTests separately reproduce the old ambiguous-extension compile
error and prevent its return.

From the repository root:

```powershell
rtk proxy python tests/prepare_audit_regressions.py --backend lua --name audit-after
rtk proxy python tests/prepare_audit_regressions.py --backend jass --name audit-after
```

In each printed directory, build and launch the disposable map:

```powershell
rtk proxy grill build ExampleMap.w3x --quiet
rtk proxy grill run ExampleMap.w3x --quiet
```

The map must actually start. After the loading screen, automatic assertions run
and finish with `AUDIT COMPLETE`. The report is written to
`Documents/Warcraft III/CustomMapData/WurstStdlibRegressions.txt`. Check its fresh
timestamp, matching source-manifest build stamp and backend before accepting it.
The standalone runner additionally checks zero uncaught errors, exactly eleven
caught bounds errors, and completion of the native delayed abort assertion.
Keep this test package out of shipping maps.

Prepare a before-fix map by adding `--baseline-ref fd6dcff --baseline-file list`,
`index`, or `history`, with a distinct `--name`. Only that production package is
replaced; tests stay identical. The history baseline includes the original
abortOrder helper unchanged. Its NoWurst consumer can run in game even though a
normal implicit-Wurst consumer fails to compile against that duplicate extension.

## Recorded checks

- `grill typecheck --quiet`: passed.
- `grill test --quiet`: **752/752 passed**.
- Both map preparers: **six Python tests passed**.
- Native bridge guard: **seven self-tests passed**.
- Fresh background native runs used Warcraft III **3.0.1.24371**, at 1x speed,
with a single client. No foreground PC-control tool was needed for those runs.
The runner authenticated terminal results using per-run identity, a nonce,
sequence and checksum; screenshots or launcher success did not determine pass.

Final counts, map hashes, source fingerprints, run identities and failures are
recorded in `audit-native-results.json`. Fixed Lua and Jass runs require zero
failed assertions, zero uncaught errors and exactly eleven caught bounds errors.
Jass has six fewer assertions because a bounds error ends its child thread before
the getter/removal return-value check; Lua continues and checks those values too.

| Production variant | Backend | Passed / assertions | Failed |
| --- | --- | --- | --- |
| Baseline LinkedList only | Lua | 50 / 59 | 9 |
| Baseline UnitIndexer only | Lua | 67 / 70 | 3 |
| Baseline LastOrder only | Lua | 67 / 69 | 2 |
| All fixes | Lua | 69 / 69 | 0 |
| All fixes | Jass | 63 / 63 | 0 |

The before-fix runs must fail for the relevant reason: LinkedList overwrites or
damages list storage, recursive deindexing executes twice and errors, and the old
LastOrder retains a fourth order and leaves an unpaused unit paused. The native
abort check also proves the move order is cancelled and remains stopped across
timer ticks. This work changes no local-player/UI behavior; multiplayer input
coverage from the earlier suite is not claimed here.

The compile-only before check also produced `Call to function abortOrder is
ambiguous`, identifying the Unit and LastOrder declarations. After the fix,
ordinary and NoWurst importing consumers compile and their interpreter tests pass.

Grill's launcher can return status zero while printing a compiler/test failure.
Recorded outcomes use the compiler diagnostics and assertion/terminal results,
not that launcher exit code. Early adapter/build attempts and earlier fixture
iterations are not substituted for the final source-matched matrix.
4 changes: 4 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Runtime regression suite

For the LinkedList, UnitIndexer and LastOrder audit regressions, see
[AUDIT_VALIDATION.md](AUDIT_VALIDATION.md). That focused map uses real orders and
timers and requires no mouse/key interaction.

Run `rtk proxy python tests/check_native_bridges.py` to guard the single native
frame/key bridge. `--self-test` checks that the guard rejects per-pair triggers,
extra native actions and native-condition dispatch. This is a structural check,
Expand Down
169 changes: 169 additions & 0 deletions tests/audit-native-results.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
{
"warcraftVersion": "3.0.1.24371",
"gameSpeed": 1,
"clients": 1,
"baseline": "fd6dcff3ac7f6a5cf57fa0a255718b4dae142642",
"note": "Fresh background native runs. Each before case replaces exactly one production package; consumer tests are identical. Fixed runs have no uncaught errors and exactly eleven expected caught bounds errors. Process cleanup after the authenticated terminal verdict used the runner fallback.",
"runs": [
{
"variant": "list",
"backend": "lua",
"baseline_ref": "fd6dcff",
"baseline_file": "list",
"sourceBuild": "d45dd7f764777b27",
"mapSha256": "d149b52311d60c435846458ba576fa26fd95e894f6e1e740d1511e83169b580d",
"runId": "audit-2026-10-22T16-52-08-188Z",
"verdict": "FAIL",
"failure": null,
"payload": {
"suite": "audit",
"verdict": "FAIL",
"asserts": 59,
"failed": 9,
"failedIds": "LinkedList operation 0 index 3 preserves elements,LinkedList operation 0 index 3 preserves links and valid append,LinkedList operation 1 index 3 returns default,LinkedList operation 2 index -1 preserves size,LinkedList operation 2 index 3 returns default,LinkedList operation 2 index 3 preserves size,LinkedList operation 2 index 2 preserves size,LinkedList operation 3 index -1 preserves size",
"verdictProblems": "asserts-failed",
"eventKindsDropped": 0,
"metricsDropped": 0,
"errors": 0,
"caughtErrors": 11,
"errorMessages": [],
"metrics": {},
"events": {}
},
"runtimeErrors": {
"count": 0,
"caught": 11,
"messages": []
},
"shutdown": "forced"
},
{
"variant": "index",
"backend": "lua",
"baseline_ref": "fd6dcff",
"baseline_file": "index",
"sourceBuild": "2a0d60c295fa0460",
"mapSha256": "19b6bb477323115cb6564916b13aeae7f0161328a468d026bbed737ac378b74d",
"runId": "audit-2026-10-22T16-52-59-975Z",
"verdict": "FAIL",
"failure": null,
"payload": {
"suite": "audit",
"verdict": "FAIL",
"asserts": 70,
"failed": 3,
"failedIds": "recursive deindex is rejected,reentrant deindex crashed: E46,each unit deindex hook runs once",
"verdictProblems": "asserts-failed,caught-errors-12-expected-11",
"eventKindsDropped": 0,
"metricsDropped": 0,
"errors": 0,
"caughtErrors": 12,
"errorMessages": [],
"metrics": {},
"events": {}
},
"runtimeErrors": {
"count": 0,
"caught": 12,
"messages": []
},
"shutdown": "forced"
},
{
"variant": "history",
"backend": "lua",
"baseline_ref": "fd6dcff",
"baseline_file": "history",
"sourceBuild": "7a0dd4ab5a520911",
"mapSha256": "5404c1da29de0ca5552b7b900cc4672903569eeee46f5dcf2055b6704ef604f6",
"runId": "audit-2026-10-22T16-53-32-878Z",
"verdict": "FAIL",
"failure": null,
"payload": {
"suite": "audit",
"verdict": "FAIL",
"asserts": 69,
"failed": 2,
"failedIds": "real order history retains exactly three,abort leaves unpaused unit unpaused",
"verdictProblems": "asserts-failed",
"eventKindsDropped": 0,
"metricsDropped": 0,
"errors": 0,
"caughtErrors": 11,
"errorMessages": [],
"metrics": {},
"events": {}
},
"runtimeErrors": {
"count": 0,
"caught": 11,
"messages": []
},
"shutdown": "forced"
},
{
"variant": "fixed",
"backend": "lua",
"baseline_ref": null,
"baseline_file": null,
"sourceBuild": "2f8da48011d9042c",
"mapSha256": "4ee65703c687da1f7c3700be307614cd2f53c5d43b10291944dc7a383ff21351",
"runId": "audit-2026-10-22T16-54-07-755Z",
"verdict": "PASS",
"failure": null,
"payload": {
"suite": "audit",
"verdict": "PASS",
"asserts": 69,
"failed": 0,
"failedIds": "",
"verdictProblems": "",
"eventKindsDropped": 0,
"metricsDropped": 0,
"errors": 0,
"caughtErrors": 11,
"errorMessages": [],
"metrics": {},
"events": {}
},
"runtimeErrors": {
"count": 0,
"caught": 11,
"messages": []
},
"shutdown": "forced"
},
{
"variant": "fixed",
"backend": "jass",
"baseline_ref": null,
"baseline_file": null,
"sourceBuild": "19883326fe435202",
"mapSha256": "abcbfa9c4da91ea70f45b03d1576094916c8f9fe86617cc9ee6cdd63f402e316",
"runId": "audit-2026-10-22T16-54-41-366Z",
"verdict": "PASS",
"failure": null,
"payload": {
"suite": "audit",
"verdict": "PASS",
"asserts": 63,
"failed": 0,
"failedIds": "",
"verdictProblems": "",
"eventKindsDropped": 0,
"metricsDropped": 0,
"errors": 0,
"caughtErrors": 11,
"errorMessages": [],
"metrics": {},
"events": {}
},
"runtimeErrors": {
"count": 0,
"caught": 11,
"messages": []
},
"shutdown": "forced"
}
]
}
101 changes: 101 additions & 0 deletions tests/prepare_audit_regressions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Prepare an isolated map for the four fresh-master audit regressions."""
import argparse
import hashlib
import json
from pathlib import Path
import shutil
import subprocess

FILES = {
'list': 'wurst/data/LinkedList.wurst',
'index': 'wurst/util/UnitIndexer.wurst',
'history': 'wurst/event/LastOrder.wurst',
}


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--backend', choices=('lua', 'jass'), required=True)
parser.add_argument('--name', required=True)
parser.add_argument('--baseline-ref')
parser.add_argument('--baseline-file', choices=FILES)
args = parser.parse_args()
if not args.name or any(c not in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' for c in args.name):
parser.error('name must contain only letters, digits, hyphens and underscores')
if bool(args.baseline_ref) != bool(args.baseline_file):
parser.error('baseline-ref and baseline-file must be specified together')
root = Path(__file__).resolve().parents[1]
for name in ('tests/fixtures/RegressionBase.w3x', 'wurst.build', 'wurst_run.args',
'wurst/tests/RegressionChecks.wurst', 'wurst/tests/RegressionAuditTests.wurst',
'wurst/tests/RegressionAbortOrderConsumer.wurst'):
if not (root / name).is_file():
parser.error('Required regression input is missing: ' + name)
destination = root / '_build' / (args.name + '-' + args.backend)
destination.mkdir(parents=True, exist_ok=False)
for source in (root / 'wurst').rglob('*.wurst'):
relative = source.relative_to(root)
if source.name.endswith('Tests.wurst') or source.name.startswith('StdlibIngame') or 'tests' in relative.parts:
continue
target = destination / relative
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, target)
testdir = destination / 'wurst/tests'
testdir.mkdir()
for name in ('RegressionChecks.wurst', 'RegressionAuditTests.wurst', 'RegressionAbortOrderConsumer.wurst'):
shutil.copy2(root / 'wurst/tests' / name, testdir / name)
baseline_note = None
if args.baseline_file:
filename = FILES[args.baseline_file]
content = subprocess.check_output(['git', 'show', args.baseline_ref + ':' + filename], cwd=root).decode()
(destination / filename).write_text(content)
(testdir / 'AuditRegressionRunner.wurst').write_text('''package AuditRegressionRunner
import RegressionChecks
import RegressionAuditTests
import ClosureTimers
import ErrorHandling

var unexpectedErrors = 0
var expectedErrors = 0

class AuditErrors implements ErrorListener
\toverride function onError(string _message)
\t\tif suppressErrorMessages
\t\t\texpectedErrors++
\t\telse
\t\t\tunexpectedErrors++

init
\taddErrorListener(new AuditErrors())
\tdoAfter(1.) ->
\t\tregressionNote("Audit regressions; build=" + REGRESSION_BUILD + "; backend=" + (isLua ? "Lua" : "Jass"))
\t\tregressionGuard("LinkedList", () -> runLinkedListAuditRegressions())
\t\tregressionGuard("order history", () -> runOrderHistoryAuditRegressions())
\t\tregressionGuard("abort order", () -> runAbortOrderAuditRegressions())
\t\tregressionGuard("UnitIndexer", () -> runDeindexAuditRegressions())
\t\tdoAfter(0.5) ->
\t\t\tregressionCheck(abortAuditCompleted, "native abort timer completed")
\t\t\tregressionCheck(unexpectedErrors == 0, "zero uncaught runtime errors")
\t\t\tregressionCheck(expectedErrors == 11, "exactly eleven caught bounds errors")
\t\t\tregressionSummary("AUDIT COMPLETE")
''')
shutil.copy2(root / 'wurst.build', destination / 'wurst.build')
shutil.copy2(root / 'tests/fixtures/RegressionBase.w3x', destination / 'ExampleMap.w3x')
options = [x for x in (root / 'wurst_run.args').read_text().splitlines() if x.strip() != '-lua']
if args.backend == 'lua':
options.append('-lua')
(destination / 'wurst_run.args').write_text('\n'.join(options) + '\n')
hashes = {str(p.relative_to(destination)).replace('\\', '/'): hashlib.sha256(p.read_bytes()).hexdigest()
for p in sorted((destination / 'wurst').rglob('*.wurst'))}
stamp = hashlib.sha256(json.dumps([hashes, options], sort_keys=True).encode()).hexdigest()[:16]
support = testdir / 'RegressionChecks.wurst'
support.write_text(support.read_text().replace('"working-tree"', '"' + stamp + '"'))
(destination / 'source-manifest.json').write_text(json.dumps({
'build': stamp, 'backend': args.backend, 'baseline_ref': args.baseline_ref,
'baseline_file': args.baseline_file, 'baseline_note': baseline_note,
'files_before_stamp': hashes, 'options': options,
}, indent=2) + '\n')
print(destination)


if __name__ == '__main__':
main()
Loading
Loading