A Scenario Tree Testing framework for Factorio mods. Define tests as a DAG — child tests inherit their parent's full world state via save/restore snapshots, letting you build up complex game states incrementally.
Platform support: macOS is the primary supported platform. Linux bootstrap and path detection are now supported on a best-effort basis for common installs, but have not been validated as extensively.
- Factorio (headless)
- Lua 5.2.x on PATH (Factorio's runtime is Lua 5.2; other versions are not supported)
- LuaRocks
- bash
Lua 5.2 is not available via Homebrew core. The recommended approach is luaver:
luaver install 5.2.4 && luaver use 5.2.4Or compile from source, or use your system package manager if it provides 5.2.x.
brew install cmtonkinson/tap/factestioThen install the Lua dependencies:
luarocks install --deps-only factestio-*.rockspecFrom your mod project directory, run:
factestio activateThis will:
- Create a
factestio/directory in your mod project (if not present) - Copy
factestio/config.lua.exampletofactestio/config.luafor you to fill in - Copy
factestio/example.luaas a starting point for your tests - Create or update
factestio/.gitignoreto ignore local config and generated results - Symlink your mod project's
factestio/into the factestio scenario - Symlink the factestio repo into Factorio's mods directory
- Symlink the SUT mod into Factorio's mods directory
- Enable both factestio and the SUT in
mod-list.json - By default, disable all other non-base mods for an isolated test session
Edit factestio/config.lua in your mod project:
return {
os_paths = {
binary = '/Applications/factorio.app/Contents/MacOS/factorio',
data = '/Users/<you>/Library/Application Support/factorio',
}
}On Linux, common defaults are:
return {
os_paths = {
binary = '/home/<you>/.factorio/bin/x64/factorio',
data = '/home/<you>/.factorio',
}
}factestio activate also checks FACTESTIO_FACTORIO_BINARY and
FACTESTIO_FACTORIO_DATA before falling back to platform defaults.
To keep other non-base mods enabled during activation:
factestio activate --keep-other-modsfactestioOr with options:
factestio --seed 12345 --debug --timeout 15 /path/to/mod/project
factestio --leaf basic.setup
factestio --branch regressions.setup
factestio list --roots
factestio list --children regressions.setup --jsonAt startup factestio prints:
- version
- mod title
- working directory
- seed
If you omit --seed, factestio generates one and prints it so the run can be reproduced later.
To validate your shell/runtime setup before running tests:
factestio doctorfactestio deactivateThis removes the factestio/SUT symlinks and restores the pre-activation
mod-list.json state captured when the current factestio session began.
| Flag | Description |
|---|---|
-h, --help |
Show command help |
-V, --version |
Print the installed factestio version |
Validate the Lua 5.2 + LuaRocks environment.
factestio doctorScaffold and activate factestio for this mod project.
factestio activate [mod_dir]Flags:
| Flag | Description |
|---|---|
--keep-other-mods |
Keep other non-base mods enabled during activation |
-q, --quiet |
Suppress informational output |
Restore the original mod-list state and remove factestio links.
factestio deactivate [mod_dir]Flags:
| Flag | Description |
|---|---|
-q, --quiet |
Suppress informational output |
Show the compiled scenario DAG without running Factorio.
factestio list [options] [mod_dir]Flags:
| Flag | Description |
|---|---|
--roots |
Show only root scenarios |
--children ID |
Show the named scenario and all descendants |
--json |
Emit machine-readable JSON |
Run scenarios for the target mod project.
factestio [options] [mod_dir]Flags:
| Flag | Description |
|---|---|
-d, --debug |
Run in debug mode |
--leaf ID |
Run only the named scenario and its parent chain |
--branch ID |
Run the named scenario, its parents, and all children |
--seed N |
Seed Lua math.random for reproducible test runs |
-t, --timeout N |
Timeout for each scenario in seconds (default: 8) |
Your mod project's factestio/ directory contains:
| File | Description |
|---|---|
config.lua |
Required. Local Factorio paths (gitignored). |
config.lua.example |
Template for config.lua. |
*.lua |
Your test files, one per suite. |
.gitignore |
Created by factestio activate; ignores config.lua and results/. |
results/ |
Generated artifacts from the most recent run. |
factestio activate creates factestio/.gitignore for you so local config and
generated results stay out of version control.
Tests are defined in factestio/ as Lua files returning a table of named
scenarios. Each scenario is a table with a test function and optional from,
before, and after keys.
-- factestio/my_tests.lua
return {
-- Root test: starts from a fresh world
setup = {
test = function(f, context)
local surface = context.game.surfaces[1]
surface.create_entity({ name = "assembling-machine-2", position = {x=1, y=1} })
f:expect(1, 1)
end,
},
-- Child test: inherits setup's world state (assembler is present)
verify_entity = {
from = 'setup',
test = function(f, context)
local surface = context.game.surfaces[1]
local found = surface.find_entities({{0,0},{2,2}})
f:expect(#found, 1)
f:expect(found[1].name, "assembling-machine-2")
end,
},
}The f test helper also provides with_player_settings for temporarily
overriding runtime player settings inside a single test callback:
paste_to_requester_sets_item_requests = {
from = "setup",
test = function(f, context)
local player = context.player
f:with_player_settings(player, {
["my-mod-request-size-type"] = "items",
["my-mod-request-size"] = 7,
}, function(current_player)
-- settings.get_player_settings(current_player.index) now returns the
-- merged overrides for this callback only.
end)
end,
}Override values can be passed either as raw values or as { value = ... }
tables. The original settings table is restored automatically even if the
callback raises an error.
All factestio/*.lua files are discovered automatically at runtime, except
factestio/config.lua.
| Key | Type | Description |
|---|---|---|
test |
function(f, context) |
Required. Main test body. |
from |
string |
Parent test name. Child starts from parent's saved world state. |
before |
function(f, context) |
Runs before test. |
after |
function(f, context) |
Runs after test. Always runs even if test fails. |
Within a single test file, from is a bare name:
verify = { from = 'setup', ... } -- refers to 'setup' in the same fileTo reference a test in a different file, use a fully-qualified dotted name:
verify = { from = 'other_file.setup', ... } -- cross-file referenceTest names are automatically prefixed with their filename in the registry (e.g.
my_tests.setup), so bare names are relative and dotted names are absolute.
Use the fully-qualified scenario id when targeting part of the DAG:
factestio --leaf my_tests.setup
factestio --branch other_file.root_case--leaf runs just that scenario plus its parent chain. --branch runs that
scenario, its parent chain, and all of its descendants.
Use list to inspect the compiled scenario graph without running Factorio:
factestio list
factestio list --roots
factestio list --children my_tests.setup
factestio list --children my_tests.setup --jsonlist prints the full compiled DAG. --roots limits output to root scenarios.
--children ID prints the named scenario and all descendants. --json works
with any list mode and emits machine-readable output.
f:expect(actual, expected) -- assert actual == expectedcontext.game -- LuaGameScript
context.player -- nil in headless (no player)
context.event -- the on_tick event
context.node -- the test node (metadata)Each test runs Factorio headlessly in a fresh process:
- Root tests (
no from): launched with--start-server-load-scenario, generating a fresh world. - Child tests (
from = 'parent'): the parent's save zip is loaded with--start-server, restoring the full world state including all entities.
At tick +10 the test runs, at tick +20 the world is saved, at tick +30 the
process signals completion. Results and saves are collected under
factestio/results/.
A real mod using factestio can be found at
cmtonkinson/paste-logistic-settings-continued. Sample output looks like:
