16.1 Test runner (wowlua test)
Tests run outside the game, in wowlua's emulation of the WoW client
(WoW client emulation): a Lua 5.1 VM written in Rust with WoW's
Lua differences, taint and secret values, and an engine with the flavor's WoW
API, optionally with Blizzard's own UI code. Like the game it has no io,
os, require or file access. The framework ships with wowlua; tests need no
third-party library (decision T184).
16.1.1 Writing tests
| Files | Role |
|---|---|
tests/**/*.test.wowlua, src/**/*.test.wowlua |
Test files |
tests/mocks/**/*.wowlua |
Project mocks. Loaded before the addon, never treated as tests. |
other tests/**/*.wowlua |
Test helpers, importable from test files |
Test-only files are compiled with the test profile and never included in
addon builds. Importing wowlua:test or wowlua:mock from a non-test file is
error E8108. In test files, /x names build.src's x (the addon's
modules), and ./x a file next to the test file (helpers under tests/).
-- tests/tracker.test.wowluaimport test, beforeEach, expect from "wowlua:test"import * as mock from "wowlua:mock"import Tracker from "/tracker" local tracker: Tracker @beforeEachlocal function setup() mock.resetSpies() tracker = Tracker.new()end @test("records kills")local function recordsKills() tracker:record(2) expect(tracker:total()).toBe(2)end @test("flushes on logout")local function flushOnLogout() tracker:record() -- saved data is loaded: lifecycle already ran mock.fireEvent("PLAYER_LOGOUT") expect(MyAddonDB.kills).toBe(1)end @test("ticks every second")local function ticks() local spy = mock.spy() tracker:startTicker(spy) mock.advanceTime(3.5) expect(spy).toHaveBeenCalledTimes(3)end16.1.2 What a test run loads
For each test file and flavor, the runner:
- creates a fresh VM and installs
wowlua:mockfor that flavor, - loads the TOC's files up to the runtime (locale files, the runtime), then
the project mocks (
tests/mocks/, with what they import), - loads the rest of the whole addon in its computed load order,
compiled with the
testprofile (exactly as the generated TOC would load it, XML included, as the addon's code), - loads the test file after the helpers it imports (its top-level code runs),
- unless the file opted out, fires the lifecycle:
ADDON_LOADEDfor the addon,PLAYER_LOGIN, andPLAYER_ENTERING_WORLD(isInitialLogin = true), - runs
@beforeAll, then each test with its@beforeEach/@afterEachhooks, then@afterAll.
A test file opts out of step 5 with config({ lifecycle = "manual" }) from
wowlua:test at its top level, and then drives the lifecycle itself with
mock.login(). Tests within one file share the VM. Each file starts fresh.
An error while loading (a step above) fails the file, Blizzard's code's
included (with test.blizzard-ui: it runs without an error in the client,
so the engine must too). An error that reaches
the error handler during a test (an event handler's, a timer's) fails that
test, unless the test read it with mock.errors(). The addon's
Bindings.xml is read before its files load, as the client does.
16.1.3 wowlua:test
| Export | Purpose |
|---|---|
@test(name?, opts?) |
Marks a test function (top level); the name defaults to the function's; opts.timeout (virtual seconds) for its waits |
@beforeEach, @afterEach, @beforeAll, @afterAll |
Hooks, per file |
@skip(reason?), @only |
Test selection |
@flavors("standard", …) |
Run only for these flavors |
config({ lifecycle: "auto" | "manual", timeout?, profile? }) |
Per-file runner settings (profile: the world profile, read before anything loads) |
describe(name, fn) |
Grouping for runtime-generated tests (test(name)(fn) inside fn) |
expect(v) |
Matchers: toBe, toEqual (deep), toBeNil, toBeTruthy, toBeFalsy, toBeInstanceOf, toContain, toHaveLength, toBeCloseTo, toThrow, toHaveBeenCalled, toHaveBeenCalledWith, toHaveBeenCalledTimes, toMatchSnapshot; for widgets toBeShown, toBeVisible, toHaveText, toHaveSize, toBeAnchoredTo. Negation with expect(v).never.toBe(x). Type-checked (below). |
@cases(list) |
Runs a test once per case, the case's values as its arguments (below) |
@expectError(code, …) |
A compile-time test: the function must not compile, with these diagnostics (below) |
matcher(name, fn) |
Defines a typed matcher (below) |
@bench(name?) |
A benchmark, run by wowlua bench (M11), not by wowlua test |
Typed matchers (decision T214). expect is generic: expect(x) has the
type Expect<T> for the type T of x, and its matchers take T:
expect(count).toBe("3") is a type error, as is a matcher on a kind of
value it does not take (E8121: toHaveLength and toContain need a string
or a table, toBeCloseTo a number, toThrow and the spy matchers a
function). A spy keeps the type of the function it spies on, so
toHaveBeenCalledWith(...) checks its arguments against that function's
parameters. Matchers are called with a dot: expect returns a table of
them.
Parametrized tests (decision T215). @cases gives a test its cases;
each runs and is reported on its own, named after the test and the case's
values (doubles [2, 4]), and each is checked against the test function's
parameters (E8120 when a case does not fit):
@test("doubles")@cases({ { 1, 2 }, { 2, 4 }, { -3, -6 } })local function doubles(n: number, expected: number) expect(double(n)).toBe(expected)endWaiting (decision T216). A test may wait for the world, written as
straight-line code: mock.waitFor(event, predicate?) (the event's arguments
are returned), mock.waitFrames(n), mock.waitSeconds(s) and
mock.waitUntil(fn). A test runs as a coroutine; waiting advances the virtual
clock and the frames (60 a second, each running OnUpdate and the due
timers), so a test stays deterministic and runs instantly. A
wait longer than timeout virtual seconds (default 10, config({ timeout })
or @test(name, { timeout })) fails the test, naming what it waited for.
@test("loads the item first")local function itemLoads() tooltip:showItem(19019) local itemID = mock.waitFor("ITEM_DATA_LOAD_RESULT") expect(itemID).toBe(19019) expect(tooltip.frame).toHaveText("Thunderfury, Blessed Blade of the Windseeker")endWidget matchers (decision T217): toBeShown() (IsShown),
toBeVisible() (shown with every parent), toHaveText(text) (a font string,
an edit box, a button's text, or a frame with one font string), toHaveSize(w, h), toBeAnchoredTo(other, point?, relativePoint?), and
expect(GameTooltip).toHaveLines({ … }) for a tooltip's left and right texts,
line by line.
Snapshots (decision T218). expect(v).toMatchSnapshot(name?) compares a
value (serialized deterministically: keys sorted, numbers with at most two
decimals, frames as their widget tree with shown state, size, points and
text, then their regions and children) with the snapshot stored in
__snapshots__/<test file>.snap next to the test, keyed by the test's name
and name (else numbered); a missing snapshot is written, a different one
fails the test with a line diff, and wowlua test -u rewrites them (and,
when every test of the file ran, drops those no test matched). Snapshots
are reviewed in version control: each is a # <key> line and its text,
each line after | .
Compile-time tests (decision T219). @expectError marks a function that
must not compile: the test passes when the checker reports exactly these
diagnostics in it (by code, optionally { code, message }), and the function
is never run. Libraries pin down what their types reject, the stdlib its own
diagnostics:
@expectError("E2001")local function wrongArgument() tracker:record("two") -- record takes a numberendCustom matchers (decision T220). matcher(name, fn) adds a matcher to
expect, typed by fn's signature: fn(actual: T, ...): (boolean, string)
returns whether it passed and the message; a library exports its matchers and
a test file that imports it gets them, checked like the built-in ones.
Benchmarks and examples. @bench functions in test files are run by
wowlua bench (M11, decision T221); code examples in --- doc comments run
as tests with the language server's documentation work (M9 S8, decision
T222).
16.1.3.1 Module internals and mocked imports
A test file may import what a module does not export (decision T223):
import internal parse, Cache from "/tracker" binds the module's
file-level locals and types by name, as if exported, typed as the module has
them; only test files may (E8119 elsewhere). The module registers the
locals test files read at its end (their values then), in test builds only:
an addon build is unchanged.
mock.module(namespace, replacement) replaces a module's exports for the
rest of the test file (decision T224): namespace is the module's namespace
import (import * as clock from "/clock"), replacement the exports to
replace, checked against the module's export types (E8123 for one it does
not have, a type error for a mistyped one); the others stay. In a test
build the addon's files read their imports at each use, so every importer
sees the replacement (a value an importer stored before keeps the old
one).
16.1.4 wowlua:mock
wowlua:mock drives the emulated client (WoW client emulation):
every API function of the flavor exists, generated from Blizzard's
documentation with the client's argument checks, and reads a simulated
game world that tests set up and change. A change to the world fires the
events the client fires for it, in the client's order (decision T201).
Functions the flavor lacks are absent, as in the game; a function without
behaviour raises "<name> is not mocked" unless a test fakes it. A widget
method without behaviour follows its name: SetX(...) and EnableX(...)
store their arguments, GetX() returns what SetX stored, IsX() whether
its first value was true (IsXEnabled reads EnableX), and another method
that returns nothing does nothing; only a method with results and nothing
stored is not mocked.
@test("shows the target's name")local function targetName() local hogger = mock.spawnUnit({ name = "Hogger", level = 11, reaction = "hostile", health = 666 }) mock.player:target(hogger) -- PLAYER_TARGET_CHANGED expect(MyFrame.title:GetText()).toBe("Hogger") hogger:damage(100) -- UNIT_HEALTH ("target") mock.player:enterCombat() -- PLAYER_REGEN_DISABLED; combat restrictions apply expect(issecretvalue(UnitHealth("target"))).toBe(true)endThe world.
| Part | Controls |
|---|---|
| Units | mock.player, mock.spawnUnit(data) returning a unit: name, GUID (generated unless given), level, class, race, sex, reaction ("hostile", "neutral", "friendly"), player or NPC, health and power, dead or alive, distance from the player (yards). Unit methods act and fire events: unit:damage(n), unit:heal(n), unit:kill(), unit:setPower(type, n), unit:applyAura(data), unit:removeAura(id), unit:castSpell(id, opts?) (UnitCastingInfo / UnitChannelInfo, UNIT_SPELLCAST_START then SUCCEEDED and STOP, or the channel's; opts.castTime in seconds, else the spell data's, else 1.5; the order unverified), unit:interruptCast(), unit:setDistance(yards) (CheckInteractDistance, UnitInRange, UnitDistanceSquared), unit:despawn(). mock.setPlayer(data) sets the player's data without events. |
| The character | mock.player:setMoney(copper) (PLAYER_MONEY), mock.player:equip(slot, item) (PLAYER_EQUIPMENT_CHANGED, UNIT_INVENTORY_CHANGED; GetInventoryItem…), mock.setBag(bag, { slots, items }) (BAG_UPDATE, BAG_UPDATE_DELAYED; C_Container, C_Item.GetItemCount), mock.moveTo({ mapID, zone, subZone, x, y, world }) (ZONE_CHANGED_NEW_AREA, ZONE_CHANGED; GetZoneText and the other zone texts, C_Map.GetBestMapForUnit, C_Map.GetPlayerMapPosition, UnitPosition). An item is its ID or { id, count, link, bound, locked }; links are the test's, the item data's, or made like the client's. The events and their order are the client's (/stdtest engine record); camelot's links have its form (` |
| Vendors | mock.openMerchant({ name, items = { { id, price, stack, available, maxStack, usable } }, canRepair, repairCost }) (MERCHANT_SHOW, the interaction frame, then MERCHANT_UPDATE; the vendor is the npc unit), mock.closeMerchant(), mock.sell(bag, slot) (the user's right-click); buying, selling and buying back through the client's API (BuyMerchantItem, BuybackItem, C_MerchantFrame), answered by the server 0.4 s later with the recorded events |
| Unit tokens | Follow from relationships, as in the client: mock.player:target(unit), :focus(unit), :mouseover(unit), mock.player:softTarget(unit, kind?) (softenemy, softfriend, softinteract), unit:showNameplate() (nameplateN), mock.group:add(unit) (partyN, raidN), bosses (bossN); compound tokens (targettarget) resolve through them. |
| Combat and restrictions | mock.player:enterCombat(), :leaveCombat(); mock.enterInstance(kind) (a restricted map), encounters, challenge modes, PvP matches, chat lockdown. The client's addon restrictions follow (WoW client emulation §17.5): ADDON_RESTRICTION_STATE_CHANGED, secret API results, protected functions refused. |
| Time | GetTime and C_Timer on a virtual clock: mock.advanceTime(sec); OnUpdate per simulated frame: mock.nextFrame(), mock.advanceFrames(n, ms?); waiting in a test: mock.waitFor, mock.waitFrames, mock.waitSeconds, mock.waitUntil (§16.1.3) |
| Lifecycle | mock.login() (ADDON_LOADED, PLAYER_LOGIN, PLAYER_ENTERING_WORLD), mock.loadAddon(name), mock.reloadUI() (/reload: the pass ends; the test runs again from the top in a fresh VM that kept only the TOC's SavedVariables, as the client writes them, logged in as a reload; mock.reloads() counts the passes; the file's other tests stay in the first VM) |
| Communication | mock.receiveAddonMessage(prefix, message, chatType, sender) (another player's), mock.sentAddonMessages(), mock.joinGuild(); the player hears their own group and guild messages and whispers to themselves on the next frame, for registered prefixes; WoW Forever's characters are addressed by first and last name (unit:setName(name, lastName)), as /stdtest engine comm recorded |
| Addons | mock.defineAddon(name, { metadata, loadOnDemand, load }): an addon the client knows (C_AddOns.GetAddOnMetadata, GetAddOnInfo, LoadAddOn) |
| Input | mock.slash(line), mock.defineBinding(name, action, { header, runOnUp }) (from Bindings.xml), mock.pressBinding(name), mock.pressKey(key) (the key's binding, as SetBinding bound it; ENTER, ESCAPE, TAB and SPACE go to the focused edit box first), mock.click(frame, button?) (a visible frame: OnMouseDown, OnMouseUp, then an enabled button's PreClick, OnClick, PostClick; the order unverified), mock.hover(frame?) (OnLeave, OnEnter; GetMouseFoci, IsMouseOver), mock.typeText(editBox, text) (keyboard focus, then OnChar and OnTextChanged(true) per character; HasFocus, GetCurrentKeyBoardFocus), mock.drag(frame, dx, dy) (the frame, or an overlay's parent, moves if it starts moving), mock.sizeBy(grip, dw, dh) (a resize grip's frame), mock.wheel(frame, delta), mock.script(frame, name, ...) (any other user action), mock.setModifiers(...) |
| UI | tooltips (mock.showTooltip(kind, data, tip?) with Blizzard's UI code: an item { id }, { link }, { bag, slot } or equipped { slot }, a spell { id }, a unit { unit }; the engine's C_TooltipInfo gives the data from the tests' game data, Blizzard's tooltip code shows it and runs the addons' hooks; returns the lines), menus and dialogs, Settings, the screen size, locale, CVars, other addons and libraries (mock.provideLibrary); mock.frames() (every frame, in creation order); stand-ins for Blizzard's UI code: mock.defineTemplate(name, fn) (CreateFrame with the template calls fn(frame); an undefined template only warns) and mock.defineFrameType(name, base, fn) (an intrinsic frame type such as EventFrame) |
| Output | mock.printed() (what the chat frame shows: print's lines, and with Blizzard's UI everything DEFAULT_CHAT_FRAME:AddMessage gets), mock.errors() |
World profiles (decision T213). A test file starts in the situation a
profile describes: config({ profile = "stormwind" }) (a literal, read
before anything loads) or test.profile of the manifest names
tests/profiles/<name>.json, applied through the controls above before the
addon loads:
{ "character": { "name": "Thrall", "realm": "Realm", "class": "SHAMAN", "race": "Orc", "level": 60 }, "money": 123456, "items": { "19019": { "name": "Thunderfury, Blessed Blade of the Windseeker", "quality": 5 } }, "spells": { "403": { "name": "Lightning Bolt", "known": true } }, "equipment": { "MainHandSlot": 19019 }, "bags": { "0": { "slots": 16, "items": { "1": { "id": 159, "count": 5 } } } }, "position": { "mapID": 1453, "zone": "Stormwind City", "subZone": "Trade District", "x": 0.5, "y": 0.6 }, "auras": [ { "spellId": 546, "name": "Water Walking", "duration": 600 } ], "units": [ { "name": "Hogger", "level": 11, "reaction": "hostile", "distance": 12, "target": true } ], "cvars": { "nameplateShowEnemies": "1" }, "bindings": { "SHIFT-1": "ACTIONBUTTON1" }, "savedVariables": { "MyAddonDB": { "logins": 41 } }}/stdtest snapshot records the character's world in the client (StdTest,
then /reload), and cargo xtask profile <SavedVariables/StdTest.lua> <out.json> writes it as a profile with the character's name and realm, a
player target's name and identifying CVars scrubbed. character is
mock.setPlayer's data; items and spells are
defineItem and defineSpell data (items cached unless cached is
false); equipment slots are IDs or the client's slot names
(GetInventorySlotInfo); a unit's target, focus, group ("party",
"raid") and nameplate relate it to the player, its auras are
applied; object keys that are whole numbers become number keys. Every part
stays drivable during the test.
Raw events. mock.fireEvent(name, ...) and mock.fireUnitEvent(name, unit, ...) fire any event the flavor documents, the payload checked against
its documentation (count, types, and secrets where the documentation says);
an event the flavor does not document is an error (decision T202).
Game data. Spells, items and other game data come from the tests
(decision T203): mock.defineSpell(id, data) (name, iconID,
description, known, petKnown, cooldown, charges, ranges, cached),
mock.defineItem(id, data, { cached?, delay?, missing? }) (GetItemInfo's
fields by name; RequestLoadItemDataByID fires ITEM_DATA_LOAD_RESULT
after delay, 0.2 s by default), in the test or in tests/mocks/. The data
stays the test's table, so its values may be secrets. An ID the test did not define behaves as data
the client does not have. Optionally, tests can run with the real client's
data: wowlua data pull --wow <path to the WoW installation> reads the
flavor's game data from the user's own client into a local cache (nothing is
distributed with wowlua), and wowlua test --game-data loads it. Off by
default, so tests in CI run with test data only.
Secret values. mock.secret(v) makes a secret; the restrictions above
make the API return secrets where the client does.
Mocking (decision T187):
- Spies, stubs and fakes for any function:
mock.spy(fn?),mock.stub(table, key, fn),mock.fake(name, impl)for an engine function, restored after each test. - Scenario controls set the game's state: combat, units, group, auras, spells, items, time, screen, locale, CVars, other addons and libraries.
- Taint in tests. A protected action refused because of taint fails the
test with its taint chain (WoW client emulation §17.4),
unless the test expected it:
expect(fn).toBeBlocked()asserts that callingfnis refused, and readingmock.refusals()marks the refusals so far as expected.expect("Name").toBeSecure()asserts that a global is untainted,expect(t).toBeSecure(key)a table's field (issecurevariable).
16.1.5 Running
wowlua test [filter] [--flavor <f>] [-u] [--watch] [--coverage] [--reporter <r>,…] [--output <dir>] [--game-data]- Each test file runs once per manifest flavor by default.
--flavorrestricts the run, and@flavorsrestricts individual tests. - Each file and flavor runs in a fresh VM. Files run in parallel.
filterkeeps the test files and tests whose path or name contains it.- The test build of each flavor goes to
<build.out>/.test/<flavor>/<Name>(the AddOn folder as the client would load it, with the test side next to it); error locations in reports are the sources' (thetestprofile keeps lines). - Test reporters:
pretty(the terminal default),junit(XML,junit.xml),tap(results.tap),json(results.json),github(annotations, on standard output). Several may be given (ortest.reportersnames them); files go to<build.out>/test/unless--outputsays otherwise. - Exit code 1 if any test fails, or if coverage is below a threshold (§16.1.6).
16.1.6 Coverage
--coverage measures, mapped to the .wowlua and .lua sources
(decision T188):
| Kind | Counts |
|---|---|
| Lines and statements | Each statement executed |
| Branches | Each arm of if/elseif/else (the implicit "no else" too), loops entered or skipped, both outcomes of and, or, ?? and ?. |
| Functions | Each function, method and closure called |
| Conditions | Each condition of a compound test (a and b, a or b) both true and false |
The VM counts them; the compiled code is not changed. It counts the
instructions that run and the outcomes of conditional jumps, mapped to the
sources (the test profile keeps lines): a line is covered when code on it
ran, statements count as lines, a branch is one outcome of a conditional
jump, a condition one on a line with several (a compound test), and the
compiler's runtime checks and generated functions are left out. Code that
@if removes for the flavor under test counts as not applicable, and
coverage of all flavors is merged unless --coverage-per-flavor is given
(reports then in <flavor>/). -- wowlua: coverage ignore (the next line
with code, or the line it ends) and -- wowlua: coverage ignore-file
exclude code, and test.coverage.exclude excludes files.
Reports, any combination: a terminal summary (always, with each file's lines
that never ran), lcov (lcov.info, with branch and function data),
cobertura (cobertura.xml), coverage-json (coverage.json), and html
(coverage.html): a self-contained report in the wowlua design system (its
fonts and tokens, dark and light), with every file's source, hit counts,
branch markers and totals per file and folder.
Thresholds in test.coverage (Manifest §9.18) set
minimum percentages per kind; a run below one fails.
16.2 Language server (wowlua lsp)
A Language Server Protocol implementation over stdio, shipped in the same binary. It shares the parser, binder and checker with the compiler.
| Feature | Details |
|---|---|
| Diagnostics | All compiler diagnostics, live, per flavor and profile (both switchable from the editor) |
| Hover | Types, documentation comments, class members, wowlua:meta folded values |
| Completion | Locals, imports with auto-import, members, string literal unions (events, anchors, frame types), decorator names, manifest keys in wowlua.json |
| Navigation | Go to definition (including into .d.wowlua and dependencies), find references, document and workspace symbols |
| Refactoring | Rename (scope-aware), organize imports, quick fixes from diagnostic fixes |
| Signature help | Including overloads |
| Inlay hints | Inferred types of locals and parameters, parameter names |
| Semantic tokens | Distinguishes contextual keywords from identifiers |
| Formatting | Via the formatter (§16.3) |
| Code lens | Profiler and test shortcuts (run test, run file) |
Configuration discovery. For each open file, the server uses the nearest
wowlua.json found by walking up from the file. If that manifest belongs to a
workspace, it uses the workspace root manifest. The selected flavor and profile
are stored in the editor's settings. The server never accesses the network
unless the editor setting wowlua.lsp.fetch allows it. Missing dependencies
produce a diagnostic suggesting wowlua fetch.
A VS Code extension (TextMate grammar plus LSP client) is part of the project. Other editors use the LSP directly.
16.3 Formatter (wowlua fmt)
An opinionated formatter for .wowlua and .d.wowlua. It can optionally format
.lua with fmt.lua = true.
- Idempotent, and preserves comments (it is built on the lossless trivia kept by the parser).
- Configuration is deliberately small, in the manifest:
"fmt": { "indent": "tab" | 2 | 4, "line-width": 100, "quote-style": "double" | "single" | "preserve" }. Defaults:indent"tab",line-width100,quote-style"double"(Manifest §9.13). wowlua fmt --checkexits 1 if any file would change (for CI).- Style (decision T65), following StyLua: lines are reflowed at
line-width; call arguments are always parenthesized (f "x"becomesf("x")); tables print as{ a = 1, b = 2 }when they fit, otherwise one field per line with a trailing comma; a single table or function argument, or a function as the last argument, hugs the parentheses; long binary expressions break after the operator with a hanging indent; blocks are never kept on one line, except an empty function body (function() end); at most one blank line is kept;;is written only before a statement that starts with(. Trailing comments stay on their line, comments on their own line stay on their own line. wowlua's syntax (M9 S6): decorators each on their own line; annotations asname: T,name?: Tandname: T = default;function f<T extends X>(a: T, ...: U): R; function types as(a: A, ...B) -> Rand a results list of one...Twithout parentheses; table types like tables; long unions and intersections break before|/&; class and interface members one per line with their blank lines kept; enums like tables;import,export,const,continue, compound assignment, destructuring,?.,?:,??and::with single spaces;declarestatements like the declarations they declare (members one per line); interpolated strings keep their text. - Formatting never changes semantics. Every formatted file is verified to
produce the same AST (types, decorators and declarations included); an
idempotence suite formats every
.wowluafile of the repository twice.
16.4 Trace decoder (wowlua trace)
Maps error messages and stack traces from bundled or minified builds back to source locations, using the source maps of the matching build (Code generation §14.6).
$ wowlua trace < bugsack.txtMyAddon/MyAddon.lua:4821: attempt to index local 'a' (a nil value) → src/ui/window.wowlua:87 in Window:refresh ('a' is 'self.frame')[string "@Interface/AddOns/MyAddon/MyAddon.lua"]:5310: in function <MyAddon/MyAddon.lua:5302> → src/main.wowlua:22 in onLogin- WoW error messages and stacks contain line numbers only, never columns. Every minification level keeps at least one line break per statement (Optimization §15.4), so a line always identifies one source statement.
- Input: pasted text, a file, or
--clipboard. It recognizes WoW error-frame and BugSack formats. - Build matching: the build id (
--build, theX-Wowlua-BuildTOC field, also included in the load errors of bundles) selects the right source maps: a build indist/with that id, else the archive. Without one, the newest build indist/is used. Maps of published builds are archived as described in Code generation §14.6, and can be uploaded as release assets (publish.archive-maps). That way, reports from users of old versions can still be decoded. - Renamed locals are reported with their original names, looked up by line and minified name.
- Line-preserved builds need no map.
wowlua tracepasses their locations through, rewriting the output path (including any flavor directory) to the.wowluasource file, and names the enclosing function found in the source (in Window:refresh). - Locations of other addons are left as they are.
16.5 Profiling reports (wowlua prof report)
wowlua prof report turns recordings exported from the dev window
(Debugging §7.7.9) into a single,
self-contained HTML page and opens it in the browser (--no-open only writes
it). The default output is dist/reports/<Name>-<date>.html; --out sets
another path.
- Finding the data. The command reads
WowluaDevDB_<Addon>.luaof the current package from the SavedVariables folders of the WoW installation, found as forwowlua dev(CLI §12.5):--wow,WOWLUA_WOW_PATH, the global config, then auto-detection. Every client folder and every account underWTF/Account/is searched. When more than one has exported recordings, the command lists them and asks, or takes--clientand--account(decision T179). - What it shows. Every exported recording (
--recordingselects some), with the functions table, the call tree and flame graph when call paths were on, the frame timeline with the frame budget, the slow calls with their stacks, the garbage view and the hints found in that recording, in the style of the wowlua website. Locations are mapped back to source lines aswowlua tracedoes (§16.4), using the build id stored with each recording. - Nothing is uploaded. The report is a local file; the command does not use the network.