Skip to content

Testing Guide

Documentation for the VEAF Lua unit test suite and CI/CD pipeline.

Table of Contents


Overview

The project covers the runtime modules with one Lua test suite per module — the "Test Suite" section lists them all, and ends with what is deliberately left uncovered. Tests run with plain Lua 5.1 using the luaunit framework. No DCS installation is required — the DCS API is stubbed out by dcs_mocks.lua.


Running Tests

All Tests

poetry run test-lua

Exit code 0 if all suites pass, 1 if any fail.

Filtered Run

poetry run test-lua --filter spawn
poetry run test-lua --filter combat

Single File

lua test/lua/test_veafSpawn.lua

Requirements

  • poetry install must have been run once
  • Lua 5.1 on PATH (lua5.1, lua51 or lua, or C:\Program Files (x86)\Lua\5.1\lua.exe on Windows)
  • or, failing that, DCS installed: on Windows, test-lua reads its install folder in the registry (HKCU\Software\Eagle Dynamics\DCS World, then DCS World OpenBeta) and uses the Lua 5.1.5 it ships, bin\luae.exe. Coverage (--coverage) also needs luacov to be found by that interpreter (LUA_PATH).
  • No other dependencies (luaunit is bundled in test/lua/luaunit.lua)

Version 5.1 is verified: every candidate is queried with lua -v, and a 5.2+ interpreter is refused rather than used. That is deliberate — Lua 5.2 removed unpack and made string.format('%d', ...) reject a fractional number, so a run under 5.4 produces dozens of failures that look like regressions and are not.

On Windows, scoop install lua51 provides Lua 5.1.5. Note that its lua shim replaces the one of any other Lua already installed through scoop; the lua51 shim lets you keep both.


Coverage

Generate a per-file line coverage report using luacov:

poetry run test-lua --coverage
# or short form:
poetry run test-lua -c

After the test run a rich table is printed showing hits, missed lines, and coverage percentage per module. The report file is also written to luacov.report.out in the repository root for manual inspection.

Requirements (coverage)

luacov must be installed via luarocks:

# Linux / DevContainer (luarocks is pre-installed)
luarocks install luacov

# Windows (may need an elevated shell)
luarocks install luacov

In the DevContainer, luacov is installed automatically — no extra steps needed.

Configuration

Coverage is collected only for modules under src/scripts/veaf/ (test helpers and luaunit are excluded). The .luacov file at the repository root controls this.


Infrastructure

File Layout

test/lua/
├── luaunit.lua         # Test framework (bundled)
├── dcs_mocks.lua       # DCS API stubs
├── veaf_loader.lua     # Module loader for src/scripts/veaf/
└── test_*.lua          # One file per module

dcs_mocks.lua

Provides minimal stubs for the DCS global API so modules can be required without a running DCS instance. Stubbed namespaces include:

  • env, timer, world
  • Unit, Group, StaticObject, Airbase
  • coalition, country, radio
  • trigger (including trigger.smokeColor, trigger.action.*)
  • Math helpers (math.isnan, math.inf)

If a new module fails to load because a DCS API call is missing, add the stub to dcs_mocks.lua.

veaf_loader.lua

Patches package.path so that require("veaf"), require("veafSpawn"), etc. resolve to src/scripts/veaf/. Each test file starts with:

dofile("test/lua/dcs_mocks.lua")
dofile("test/lua/veaf_loader.lua")
local luaunit = require("luaunit")
-- load the module under test:
require("veafSpawn")

luaunit

Standard luaunit API applies. Notable: assertNoError does not exist in this version; use pcall instead:

local ok, err = pcall(function() veafSpawn.someFunction() end)
luaunit.assertIsTrue(ok, err)

Test Suite

Suite What it covers
test_veaf.lua Core utilities, string/table/vector helpers, logging
test_veafCacheManager.lua Cache get/set/invalidate
test_veafScheduler.lua Native-timer scheduler: repetition, stop time, a failing task
test_skynetIadsUtils.lua Vendored Skynet's own scheduler: an overdue first run is armed for the next tick, and repetition, stop time and cancellation are unchanged
test_veafMath.lua Unit conversions, vectors, coordinate shapes, deep copy
test_veafGeo.lua Coordinate text output, zones, average positions, polygons
test_veafGeo_ported.lua Geometry ported off MiST: random point in a circle, units in a circular zone, heading, zone drawing
test_veafMissionDb.lua Mission snapshot, player roster, name registry, unit ids
test_veafMissionDb_scenery.lua Destroyed-scenery register: recording, zone lookup, event subscription
test_veafDcsSpawner.lua Runtime static creation: country, ids, shapes, random heading
test_csar_init.lua CSAR initialisation, in a real mission's load order (CSAR before VEAF): one event handler whatever happens
test_community_scripts_load.lua Every vendored community script loads without raising, against the DCS mocks alone — the gate that was missing when CTLD rc8 shipped a file that did not load (#957). TUM is excluded and says so: it auto-initializes and requires territory zones
test_aienGroupSA.lua AIEN reads a group's situational awareness through groupSA, built on demand: an artillery group hit from the start has none, and the counter-battery raised on it (2026-10-29)
test_stts.lua SRS text-to-speech validates its arguments before the system command
test_veafInterpreter.lua Mark text tokenizer
test_veafTime.lua Time parsing, formatting, DCS time helpers
test_veafSecurity.lua Security levels, admin management
test_veafServerHook.lua Server hook: chat-command parsing and dispatch
test_veafNamedPoints.lua Point registration, lookup, ATC helpers
test_veafOpposition.lua Opposition level: counted from the players connected or airborne, hysteresis (an immediate rise, a delayed drop), the _opposition marker and its security level
test_veafShortcuts.lua Shortcut registration and resolution
test_veafShortcuts_bypass.lua What a player may type after a security-bypassing shortcut
test_veafWeather.lua Weather parsing, QNH/wind calculations
test_dcsDataExport.lua Unit data export utilities
test_veafCombatMission.lua Base combat mission lifecycle
test_veafCombatLog.lua Combat log: one JSON line per shot, hit, kill and burst (rounds counted), a vanished shooter breaks nothing, without io nothing is written
test_veafCombatMission_menu.lua MISSIONS radio menu: missions declared after initialize(), deferred rebuild
test_veafAirbases.lua Airbase data lookup
test_veafCombatZone.lua Zone activation, scoring, state machine
test_veafCombatZone_displacement.lua Spawn anchor of a widely spread group: it keeps its shape
test_veafDiagnostics.lua The veaf.Diagnostics switch and the combat zones' diagnostic lines
test_veafUnits.lua Unit template lookup, category filtering
test_veafAssets.lua Asset registration, state tracking
test_veafAssist.lua Pilot assistance: checklists, progression, menus
test_veafRemote.lua Remote command parsing
test_veafMarkers.lua Marker event handling
test_veafEventHandler.lua Event dispatch, handler registration
test_veafSkynetIadsHelper.lua Skynet IADS integration helpers
test_veafSkynetIadsHelper_spotter.lua Spotter network: detection, radio graph, propagation, hand-over to Skynet
test_veafSkynetIadsMonitor.lua Skynet monitor state
test_veafGroundAI.lua Ground AI behavior flags
test_veafGroundAI_convoy.lua Convoy under fire: the watch, the split, fight or fall back, the call for help, the _gc orders
test_veafRadio.lua Radio menu tree construction
test_veafRadio_pilots.lua F10 menu in multiplayer: commands bound to pilots present, group-reserved menus, per-pilot menus
test_veafQraManager.lua QRA state machine, zone management
test_veafAirWaves.lua Wave scheduling, group assignment
test_veafReactiveZone.lua Base shared by QRA and AirWaves: zone, mobile zone, spawn, links, deferred spawns
test_veafAircraftSpawn.lua Aircraft spawned with a role: the -cap contract, reading a route, zone defense (zone_defense)
test_veafAwacsEscort.lua -awacs (race-track, Skynet, datalink) and -escort (the Escort task, the airplane nearest the marker, the F10 "Escort me" entry)
test_veafSanctuary.lua Sanctuary zone detection
test_veafMissileGuardian.lua Missile intercept logic
test_veafCampaign.lua Multi-mission campaign: garrisons drawn, recorded, and their losses
test_veafCasMission.lua CAS threat package generation
test_veafTransportMission.lua Transport mission setup
test_veafCarrierOperations.lua Carrier recovery sequence
test_veafMove.lua Move/teleport command parsing
test_veafMove_escort.lua Escort-task recovery after the escorted group is recreated
test_veafGrass.lua Grass runway initialization
test_veafHelicopterSpawn.lua Helicopter spawned from a marker: landed by the ground path, HELICOPTER category, the alias's loadout, orbit and transport tasks
test_veafSpawn.lua Spawn commands, mark text analysis, laser freq conversion
test_veafSpawnParser.lua Deterministic spawn mark-text parsing (markTextAnalysis)
test_veafCommands.lua Command registry: priority ordering and dispatch
test_veafI18n.lua Lua runtime i18n layer (veaf.t, veafI18n catalog)

Module not covered:

Module Reason
dcsUnits.lua Pure data file; exercised indirectly through test_veafUnits.lua

Writing Tests

Minimal Template

-- test_veafMyModule.lua
package.path = package.path .. ";test/lua/?.lua;src/scripts/veaf/?.lua"
dofile("test/lua/dcs_mocks.lua")
dofile("test/lua/veaf_loader.lua")
local luaunit = require("luaunit")
require("veafMyModule")

TestVeafMyModuleConstants = {}

function TestVeafMyModuleConstants:test_Id()
  luaunit.assertEquals(veafMyModule.Id, "MYMODULE")
end

TestVeafMyModuleLogic = {}

function TestVeafMyModuleLogic:test_someFunction()
  local result = veafMyModule.someFunction("input")
  luaunit.assertEquals(result, "expected")
end

-- Entry point
os.exit(luaunit.LuaUnit.run())

Conventions

  • One file per module: test_veaf<ModuleName>.lua
  • Group tests into classes: TestVeaf<ModuleName>Constants, TestVeaf<ModuleName>Logic, etc.
  • Always end with os.exit(luaunit.LuaUnit.run())
  • Use pcall instead of assertNoError (not available in this luaunit version)
  • Don't test internal/private functions — only what the Lua module exports on its global table

Adding DCS API Stubs

If your test fails with a "nil value" or "attempt to call a nil value" on a DCS function, add the stub to dcs_mocks.lua:

-- In dcs_mocks.lua:
trigger.action.myNewFunction = function(...) end
trigger.myNewConstant = 42

CI/CD Pipeline

The GitHub Actions workflow (.github/workflows/lua-ci.yml) runs on every push and pull request:

Jobs

lua-unit-tests — Ubuntu latest 1. Checkout repository 2. Install lua5.1 via apt 3. Run all test/lua/test_*.lua files 4. Fail the job if any suite exits non-zero

luacheck — Ubuntu latest 1. Checkout repository 2. Install lua5.1 + luacheck via LuaRocks 3. Run static analysis on src/scripts/veaf/ with luacheck --config .luacheckrc (undefined globals, unused variables, shadowing) 4. Fail if any violation is found

stylua-check — Ubuntu latest 1. Checkout repository 2. Run JohnnyMorganz/stylua-action@v5 with version 2.4.0 3. Check src/scripts/veaf/ and test/lua/ against .stylua.toml 4. Fail if any file is not formatted

lua-coverage — Ubuntu latest 1. Checkout repository 2. Install lua5.1 + luacov via LuaRocks, then Poetry and dependencies 3. Run poetry run test-lua --cov-fail-under 83.0 (luacov line coverage) 4. Fail if coverage drops below the ratchet floor (the number only ever goes up)

Running StyLua Locally

# Check only (no writes)
stylua --check src/scripts/veaf/ test/lua/

# Auto-format
stylua src/scripts/veaf/ test/lua/

Running Luacheck Locally

luacheck --config .luacheckrc src/scripts/veaf/

StyLua configuration (.stylua.toml):

column_width = 140
line_endings = "Windows"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
collapse_simple_statement = "Never"