Version: 0.1.0 (draft) Date: 2026-07-08 Status: Post-v1.0 — Shell Completion delivered (Track 26) Target Godot Version: 4.5+
gd-tools is a Python CLI that brings a modern development workflow to GDScript
projects in Godot 4.5+. It provides a project-owned native Godot test runtime
and wraps mature, community-trusted tools for linting and formatting. It also
provides line and branch coverage with a custom hybrid instrumentation
system.
The tool is designed to feel familiar to developers coming from JavaScript
(Jest), Python (pytest + coverage.py), or Go (go test -cover), while
respecting the realities of the Godot/GDScript ecosystem.
- Native-first testing. The bundled
GdToolsTestruntime is the default; legacy GUT support was removed in v0.6.0. - Build only what's missing. No production-quality GDScript line/branch
coverage tool exists for Godot 4. This is the unique value of
gd-tools. - Convention over configuration. Sensible defaults out of the box; config for when conventions don't fit.
- Single source of truth. One
gd-tools.tomlconfig drives all tools.gd-tools initgenerates per-tool config files (gdlintrc,gdformatrc) from it, so individual tools still work standalone.
- Unified CLI for test, lint, format, and coverage — one install, one config, one mental model.
- Zero-friction bootstrap —
gd-tools initdeploys the native test and coverage addons and generates project configuration in under a minute; GUT is not supported since v0.6.0 — legacy suites are migrated withgd-tools migrate. - Production-quality coverage — line and branch coverage for GDScript, with HTML and LCOV/Cobertura reports that integrate with CI and code review tools.
- CI/CD friendly — exit codes,
--checkflags,--quietfor minimal CI output, machine-readable output, no interactive prompts when run non-interactively. - Standalone tool compatibility — gdlint, gdformat, and GUT continue to
work if invoked directly (GUT is not supported by gd-tools since v0.6.0;
migrate legacy suites with
gd-tools migrate).gd-toolsis a layer on top, not a lock-in.
- Not a general-purpose mocking or editor test framework. The native
runtime provides focused assertions, async waits, lifecycle hooks,
declarative scene/resource integration, mocking/stubbing, and parameterized
tests; it is not a full IDE test framework. The bundled editor plugin
(test/coverage dock with a coverage heatmap overlay) covers run-and-inspect
workflows only. The GUT compatibility bridge shipped in v0.5.0 and was
removed in v0.6.0;
gd-tools migrateis the migration path for legacy GUT suites. - Not a linter/formatter engine. We use gdtoolkit. We do not implement our own static analysis or code formatting rules.
- Not a Godot plugin manager. We bootstrap our own native and coverage addons; we do not manage arbitrary addons.
- No C# support. GDScript only. C# projects should use coverlet + GoDotTest.
- No Godot < 4.5 support. The coverage instrumentation relies on Godot 4.x Script APIs. Older versions are out of scope.
- No deep IDE integration. The workflow is CLI-first. The editor plugin
deployed by
gd-tools initprovides a test/coverage dock and a coverage heatmap overlay, but the tool is not an IDE — there is no in-editor test authoring, debugging, or source navigation.
- GDScript developers on Godot 4.5+ who want professional tooling parity with other languages.
- Teams needing CI/CD pipelines with test results, lint gates, and coverage thresholds.
- Open-source Godot project maintainers who want contributor-friendly
setup (
gd-tools init+gd-tools doctor).
┌─────────────────────────────────────────────────────────┐
│ gd-tools CLI (Python) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────────┐ │
│ │ test │ │ lint │ │ format │ │ coverage │ │
│ │ (native)│ │(gdlint │ │(gdformat│ │ (custom │ │
│ │ │ │ wrapper)│ │ wrapper)│ │ Arch. C) │ │
│ └────┬────┘ └─────────┘ └─────────┘ └─────┬──────┘ │
│ │ │ │
│ │ ┌──────────────────────┐ │ │
│ └────►│ Godot binary (CLI) │◄─────────┘ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌──────────▼──────────┐ │
│ │ Native runner and │ │
│ │ coverage addons │ │
│ │ addons (GDScript) │ │
│ └─────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ gdtoolkit (Python — Lark parser) │ │
│ │ Used by: lint, format, coverage (plan gen) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
| Feature | Underlying Tool | Our Role |
|---|---|---|
| Test | Native GdToolsTest |
Discover suites, isolate Godot processes, normalize results, and write JUnit XML |
| Lint | gdlint (Python) | Wrap CLI, manage config, apply excludes |
| Format | gdformat (Python) | Wrap CLI, manage config, apply excludes |
| Coverage | Custom (Arch. C) | Full implementation — plan gen + report gen |
gd-tools init Bootstrap the native runtime
gd-tools doctor Diagnose environment and configuration
gd-tools test [options] [paths] Run tests via the native runtime (GutTest suites are rejected with migration guidance)
gd-tools lint [paths]... Lint GDScript files via gdlint
gd-tools format [options] [paths]... Format GDScript files via gdformat
gd-tools coverage report Generate report from last coverage run
gd-tools coverage merge Merge multiple coverage data files
gd-tools coverage show Print coverage summary to terminal
gd-tools config show Display resolved configuration (Rich table, TOML, or JSON)
gd-tools config validate Validate gd-tools.toml — schema, deprecated settings, paths
gd-tools version Print all component versions (gd-tools, Godot, gdtoolkit, Python)
gd-tools completion [shell] Generate shell completion script (bash, zsh, fish, powershell)
gd-tools test [paths]... [--coverage] [--min N] [--suite NAME]
[--test NAME] [--tag TAG] [--test-timeout SECONDS] [--timeout SECONDS]
[--junit-xml PATH] [--no-exit-code] [--show-uncovered]
| Flag/Arg | Description |
|---|---|
paths |
One or more test files/directories to run (files are exact) |
--coverage |
Enable coverage instrumentation during test run |
--min N |
Fail if coverage falls below N% (requires --coverage) |
--suite NAME |
Run only the named test suite |
--test NAME |
Run only tests matching the name substring |
--tag TAG |
Run native suites with a matching class-level tag (repeatable) |
--test-timeout |
Per-test timeout override for native tests |
--timeout |
Godot import and per-suite process timeout |
--junit-xml P |
Write JUnit XML to path (default: .gd-tools/results.xml) |
--no-exit-code |
Always exit 0 regardless of test failures |
--show-uncovered |
Show uncovered lines and branches when coverage < 100% (requires --coverage) |
When paths are provided, they override test_dirs from config. The native
runtime accepts exact files or recursively scanned directories.
Exit codes: 0 = pass, 1 = test failures, 2 = environment/config error.
Native suites are GDScript classes extending the bundled GdToolsTest node.
Each test_* method runs in a fresh suite instance. Python discovers suites,
writes a versioned manifest, starts one headless Godot process per suite, and
normalizes the result into the same CLI table and JUnit format as the legacy
runner. Async tests may await process frames, physics frames, timers, and
signals; each test has a configurable timeout (five seconds by default).
Lifecycle hook failures and suite state are preserved, cleanup runs after
ordinary failures and timeouts, and native JSON/JUnit output carries structured
assertion diagnostics plus Godot engine errors/warnings.
Native coverage is activated transiently for the run and merged from per-suite
shards into the existing plan-v1 coverage data. It does not require a permanent
autoload. Legacy GutTest suites are rejected with exit 2 and migration
guidance — the compatibility bridge was removed in v0.6.0 — and no separate
runtime is selectable.
Suites may declare const INTEGRATION with a res:// scene, a map of logical
names to res:// resources, a suite-level headless/windowed mode, and
per-test overrides. One headless preflight per command reads the declaration
through Godot metadata — Python never parses GDScript — validates paths, modes,
and override targets, and publishes an enriched protocol-v2 manifest. Overrides
merge field by field; omitted fields are inherited, resource maps merge by
logical name, and null removes an inherited entry. Unknown fields, invalid
modes, and overrides for non-test methods are configuration errors and exit 2.
The runtime exposes an explicit GdToolsTestContext from get_test_context()
for the scene root, relative node lookup, recursive name-substring node lookup,
named resources, bounded signal waits, effective metadata, and screenshot
capture. Resources are never assigned to nodes automatically. The context is
live in before_each, the test, and after_each; every attempt rebuilds the
scene, context, and a private copy of each resource so retries are isolated even
when a previous attempt kept and mutated a reference. Project autoloads run
exactly as in production.
Windowed suites require a real display and fail with exit 2 instead of falling
back to headless. Failed, timed-out, and errored attempts in a windowed suite
capture a screenshot after after_each and before teardown, and the path appears
in the test diagnostics and JUnit XML. Each run publishes a machine-readable
index under .gd-tools/artifacts/<run_id>/ covering preflight and per-suite
artifacts, and retains only the latest run.
Current runtime limits: broad mocking, multiple primary scenes, automatic resource assignment, editor UI, and automatic GUT migration are not included.
gd-tools lint [paths]... [--fix] [--report-format text|json]
paths— one or more directories/files to lint (default:.). Files are discovered across all paths and deduplicated.--fixis a no-op placeholder (gdlint is read-only; reserved for future).- Exits non-zero if any lint errors (not warnings).
gd-tools format [paths]... [--check] [--diff]
paths— one or more directories/files to format (default:.). Files are discovered across all paths and deduplicated.--check— report unformatted files without modifying; exit 1 if any need formatting (CI mode).--diff— show a diff of what would change (does not modify).
gd-tools coverage report [--format html|lcov|cobertura|text]
[--output-dir PATH]
gd-tools coverage merge <files...> [--output PATH]
gd-tools coverage show [--min N]
report— regenerate reports from saved.gd-tools/coverage/coverage.jsonwithout re-running tests.merge— combine multiple coverage data files (e.g., from parallel CI shards or multiple test runs).show— print a terminal summary table (file -> line %, branch %), followed by per-file uncovered detail panels (lines and branches).
gd-tools version [--json]
Prints version information for all gd-tools components in a Rich table. Useful for environment diagnostics and bug reports.
- Components: gd-tools (
__version__), Godot (find_godot()), gdtoolkit (importlib.metadata), Python (sys.version). - Missing components: Godot shows "not detected"; gdtoolkit shows
"not installed". In
--jsonoutput, missing components arenull. --json: Flat JSON object keyed by component name (machine-readable).- Exit code: Always 0 — version detection never fails.
- No network calls: All detection is local.
gd-tools completion [bash|zsh|fish|powershell]
Generates a shell completion script for the specified shell. The script is printed to stdout and can be sourced directly or saved to a file.
- Shell argument: Restricted to
bash,zsh,fish,powershellviaclick.Choice. Invalid shells exit with code 2. - PowerShell: Click 8.2.x does not include a native PowerShell
completion class.
gd-toolsprovides a customPowerShellCompleteclass (subclass ofBashComplete) with aRegister-ArgumentCompletertemplate. - Alternative: Click's built-in env-var approach
(
_GD_TOOLS_COMPLETE=<shell>_source gd-tools) also works without sourcing a script.
On every CLI invocation, gd-tools checks PyPI for a newer version of
gd-tools-cli. If an update is available, a notification is printed to
stderr (does not interfere with stdout output or scripts):
A new version of gd-tools is available: 0.2.0 (you have 0.1.0).
Run `pip install --upgrade gd-tools-cli` to update.
Features:
- Cached: The PyPI check is cached for 24 hours in
~/.gd-tools/update-check.jsonto avoid network delays on every run. - Non-blocking: The check fails silently on any error (network issues, PyPI downtime, parse errors). It never prevents command execution.
- Skips dev installs: No check is performed when running from an
editable/development install (
__version__ == "0.0.0"). - Disabling: Set the
GD_TOOLS_NO_UPDATE_CHECK=1environment variable to disable the check entirely. This also disables the addon version staleness check (see below).
On every CLI invocation, gd-tools also checks whether the deployed
coverage addon files are up-to-date with the installed package version.
A _version.txt file in addons/gd-tools-coverage/ records the version
at deploy time (written by gd-tools init). If the addon version is
older than the package version (or the version file is missing), a
warning is printed to stderr (does not interfere with command
execution):
WARNING: Coverage addon is outdated (v0.2.0 deployed, v0.3.0 available).
Run `gd-tools init` to update.
- Non-blocking: The warning never prevents command execution.
- Suppressed by
GD_TOOLS_NO_UPDATE_CHECK=1: The same environment variable that disables the PyPI update check also disables this check. - Doctor integration: The
gd-tools doctorcommand reports addon staleness in the Coverage Addon check.
gd-tools config show [--format toml] [--json]
gd-tools config validate
-
show— Print the resolved configuration (including defaults applied):- Default: Rich table (Section, Key, Value) — one row per setting across all 5 sections (godot, test, lint, format, coverage).
--format toml— Output as TOML (usestomli_w.dumps, stripsNonevalues).--json— Output as JSON (json.dumpswithindent=2).--formatand--jsonare mutually exclusive (exit code 2 if both given).- Works with no config file — shows all defaults.
-
validate— Check config validity without running a command:- Schema errors (✗): Unknown keys, type mismatches, constraint violations. Includes "did you mean" suggestions for misspelled keys.
- Deprecated settings (✗): Warns on deprecated config keys (future deprecation infrastructure, currently empty).
- Path warnings (!): Advisory, non-fatal —
test_dirs,godot.binary,coverage.output_dirparent, andexcludedirectories checked for existence. - Summary: file path, sections validated (5: godot, test, lint, format, coverage), counts, and "✓ Configuration is valid." on success.
Exit codes: show — 0 on success, 2 on config error or conflicting
flags. validate — 0 on valid config, 1 on schema errors or deprecated
settings, 2 on config read error.
Single source of truth, located at project root.
[gd-tools]
# Currently no top-level keys; reserved for future.
[godot]
# Path to Godot binary. If omitted, auto-detection chain is used.
# binary = "/usr/local/bin/godot"
[test]
# Test runtime: native (the only supported value since v0.6.0).
runtime = "native"
# Directories containing test files.
test_dirs = ["test", "tests"]
# Native async test timeout and retry defaults.
timeout_seconds = 5.0
retries = 0
# Test file prefix/suffix (GUT convention retained).
prefix = "test_"
suffix = ".gd"
[lint]
exclude = ["addons", ".godot", ".gd-tools", ".git"]
# Additional gdlint rule overrides go in gdlintrc (generated by init).
[format]
exclude = ["addons", ".godot", ".gd-tools", ".git"]
[coverage]
enabled = false
# Minimum coverage percentage. `gd-tools test --coverage --min 80` fails below this.
min_percent = 0
# Report format for `gd-tools test --coverage` (auto-generates after run).
format = "html"
# Where to store coverage data and reports.
output_dir = ".gd-tools/coverage"
# Directories excluded from coverage measurement.
exclude = ["addons", ".godot", ".gd-tools", ".git"]
# Directories containing test files (excluded from coverage targets, but
# their execution is still tracked for test-side coverage).
test_dirs = ["test", "tests"]- CLI flags override config file values.
gd-tools.tomlin project root (found by walking up from CWD to nearestproject.godot).- If no config file exists, built-in defaults apply.
- Detect project root — walk up from CWD to find
project.godot. - Detect Godot version — run
godot --version, parse output.- Require 4.5+. Error with instructions if older.
- Deploy native test addon — copy the bundled
addons/gd-tools-test/files; no editor plugin or permanent autoload is required. - Install coverage addon — copy bundled GDScript files to
addons/gd-tools-coverage/(always, idempotent — overwrites if stale). Before overwriting, each existing file is compared byte-for-byte to the bundled version; if they differ (indicating user modification), the existing file is backed up toaddons/gd-tools-coverage/.backups/<filename>.bakand a yellow warning is printed. Unchanged files are overwritten silently. Writes a_version.txtfile recording the package version for staleness detection. - Create/update configuration — write
gd-tools.toml. - Generate
gdlintrcandgdformatrc— from[lint]/[format]exclude lists, so gdlint/gdformat work standalone. - Create
.gd-tools/directory — add to.gitignoreif not already present. - Print summary — what was installed/configured, next steps.
The GUT compatibility bridge shipped in v0.5.0 and was removed in v0.6.0:
suites extending GutTest no longer run — gd-tools test rejects them with
exit 2 and migration guidance (run gd-tools migrate; see
docs/gut-migration.md). The native runtime does not
register a GUT editor plugin, .gutconfig.json, or the _GDTCoverage
autoload.
The GDScript coverage addon files ship as package data inside the Python distribution:
gd_tools/
addons/
gd-tools-test/
gd_tools_test.gd # GdToolsTest base class
gd_tools_test_runner.gd # transient native runner
gd_tools_native_coverage.gd # transient native coverage
gd-tools-coverage/
coverage.gd # legacy GUT instrumentation + tracking
pre_run_hook.gd # legacy GUT pre-run hook
post_run_hook.gd # legacy GUT post-run hook
- On
gd-tools init, these are copied to the project'saddons/gd-tools-coverage/. - Always version-matched with the CLI — no separate download.
- Smart backup before overwrite: When re-running
initon a project where addon files have been modified by the user, the modified file is backed up toaddons/gd-tools-coverage/.backups/<filename>.bakbefore being overwritten with the bundled version. A yellow warning names the file and its backup location. Unmodified files are overwritten silently (no backup, no warning). The.backups/directory is auto-created on first backup. - A
_version.txtfile is written alongside the addon files, recording the gd-tools package version at deploy time. This enables stale-addon detection on subsequent CLI invocations (see §5 Addon Version Staleness Check). - After upgrading
gd-toolsvia pip, re-runinitto refresh addon files.
Runs a series of checks and reports status:
| Check | Pass Condition |
|---|---|
| Godot binary accessible | Binary found via detection chain, runs without error |
| Godot version | 4.5 or higher |
| Native Test Addon present | addons/gd-tools-test/*.gd exists; version not stale |
| Coverage Addon files present | addons/gd-tools-coverage/*.gd all exist; version not stale |
| Editor Plugin present | addons/gd-tools-editor/ deployed |
gd-tools.toml exists & valid |
File present, parseable TOML |
| GD Toolkit installed | gdlint --version and gdformat --version succeed |
| Autoload (legacy) | _GDTCoverage autoload; optional for the native runtime |
| Legacy GUT (advisory) | Informational; detects addons/gut, .gutconfig.json, extends GutTest suites; never fails |
Output: table with ✓/✗ per check, plus actionable fix suggestions for failures.
Resolution chain (first match wins):
- Config:
gd-tools.toml→[godot]→binary(user-specified, highest priority) - Environment variables:
GODOT_BIN→GODOT4_BIN→GODOT_PATH - PATH lookup:
shutil.which("godot")→shutil.which("godot4") - Common install locations (platform-specific):
- Windows:
C:\Program Files\Godot\,%LOCALAPPDATA%\Godot\, Scoop, Chocolatey, Steam install paths - macOS:
/Applications/Godot.app/Contents/MacOS/Godot,/opt/homebrew/bin/godot - Linux:
~/.local/bin/godot,/usr/bin/godot,/usr/local/bin/godot
- Windows:
- Not found → error with platform-specific install instructions
Note: gdtoolkit (lint/format/coverage parser) does not need the Godot
binary — it's pure Python. Only gd-tools test and gd-tools init (version
detection) require finding Godot.
Coverage is the core differentiator. No production-quality GDScript coverage
tool exists for Godot 4. gd-tools implements a hybrid architecture:
- Python side (gdtoolkit/Lark): Parses GDScript, identifies executable lines and branch points, generates an instrumentation plan (JSON).
- GDScript side (coverage addon): Reads the plan at runtime, instruments scripts via Godot's Script API, tracks execution, writes coverage data (JSON).
- Python side (reporter): Reads coverage data, generates reports (HTML, LCOV, Cobertura, text).
| Architecture | Pros | Cons |
|---|---|---|
| A (Pure Python) | No runtime component | Lark Reconstructor is experimental; code generation from modified tree is unreliable |
| B (Fork jamie-pate) | Fastest to MVP | Depends on unmaintained code; GDScript-only parsing is less robust than Lark |
| C (Hybrid) ✅ | Best parsing (Lark) + safest instrumentation (Godot Script API) | Most moving parts; requires GDScript addon |
┌──────────────────────────────────────────────────────────────┐
│ PYTHON │
│ │
│ 1. Parse project with gdtoolkit (Lark parser) │
│ - Identify executable lines (expr_stmt, return_stmt, │
│ if_stmt, while_stmt, for_stmt, match_stmt, etc.) │
│ - Identify branch points (if/elif/else, match cases, │
│ for/while loops, ternary) │
│ │
│ 2. Generate instrumentation plan (JSON) │
│ { │
│ "version": 1, │
│ "files": [ │
│ { │
│ "path": "res://scripts/player.gd", │
│ "lines": [ │
│ {"line": 5, "id": 0, "type": "statement"}, │
│ {"line": 8, "id": 1, "type": "branch_true"}, │
│ {"line": 10, "id": 2, "type": "branch_false"} │
│ ] │
│ } │
│ ] │
│ } │
│ │
│ 3. Write plan to .gd-tools/coverage/plan.json │
│ 4. Set env vars: │
│ GD_TOOLS_COVERAGE_PLAN=res://path/to/plan.json │
│ GD_TOOLS_COVERAGE_OUTPUT=res://path/to/output.json │
│ │
│ 5. Invoke Godot headless with the native runner: │
│ godot --headless -s --path . \ │
│ addons/gd-tools-test/gd_tools_test_runner.gd │
│ │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ GODOT (GDScript) │
│ │
│ 6. Pre-run hook (coverage addon): │
│ - Read instrumentation plan from env var path │
│ - For each file in plan: │
│ - Load script via ResourceLoader │
│ - Use Godot Script API to instrument: │
│ inject tracker calls at identified line/branch IDs │
│ - Reload instrumented scripts │
│ - Initialize coverage tracker (visited IDs set) │
│ │
│ 7. The native runner executes the tests │
│ - Instrumented code fires trackers on execution │
│ - Tracker records: {file, line_id, hit_count} │
│ │
│ 8. Post-run hook (coverage addon): │
│ - Serialize coverage data to JSON │
│ - Write to path from GD_TOOLS_COVERAGE_OUTPUT env var │
│ │
│ 9. The native runner writes JUnit XML (test results) │
│ │
└────────────────────────────┬─────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ PYTHON │
│ │
│ 10. Read JUnit XML (test results) │
│ 11. Read coverage JSON (coverage data) │
│ 12. Cross-reference plan + data → compute: │
│ - Line coverage % per file │
│ - Branch coverage % per file │
│ - Overall coverage % │
│ 13. Generate reports: │
│ - HTML (detailed, syntax-highlighted source view) │
│ - LCOV (for CI integration, codecov.io, coveralls) │
│ - Cobertura (Jenkins, GitLab CI) │
│ - Terminal summary table │
│ ✅ Implemented — Track 12 (see below) │
│ 14. Apply --min threshold (exit 1 if below) │
│ ✅ Implemented — Track 13 (coverage/orchestrator.py) │
│ 15. Print coverage summary table on success AND before │
│ threshold error (Lines/Branches: Found/Hit/Rate) │
│ ✅ Track: coverage_success_display_20260714 │
│ │
└──────────────────────────────────────────────────────────────┘
Implemented: Track 9 (
coverage-plan-generator_20260711, archived). Seesrc/gd_tools/coverage/plan_generator.py. All 12 acceptance criteria passed.plan_generator.pyat 100% coverage.Tracker implemented: Track 10 (
coverage_tracker_20260711, archived). Seesrc/gd_tools/addons/gd-tools-coverage/coverage.gd. All 7 success criteria passed. Tracker deployed viaregister_coverage_autoload()ininit.py. 6 GUT tests intest_coverage_tracker.gd.Hooks implemented: Track 11 (
coverage_hooks_20260711, archived). Seesrc/gd_tools/addons/gd-tools-coverage/pre_run_hook.gd(228 lines) andpost_run_hook.gd(113 lines). All 12 acceptance criteria passed. Key deviations from spec: added plan schema validation (_validate_plan,_validate_file_entry), Cause/Fix error format (_log_error), tracker activation guard (is_activecheck in post-run). 28+13 GUT tests + 11 integration tests intest_coverage_hooks.py.Reporter implemented: Track 12 (
coverage_reporter_20260711, archived). Seesrc/gd_tools/coverage/reporter.py(~510 lines, orchestrator),html_reporter.py,lcov_reporter.py,cobertura_reporter.py,terminal_reporter.py. All 8 success criteria passed. Generates reports in 4 formats: HTML (Jinja2 templates), LCOV, Cobertura XML, Text (Rich table). 73 unit tests across 5 test files; coverage modules at 96-100%. Key functions:read_coverage_json(),merge_coverage_data(),compute_summary(),generate_report(plan, data, output_dir, format, min_threshold). Review fixes (commite9457ec): error messages updated to Cause/Fix format, HTML templates added to package-data, dead code removed. Known limitation: HTML source view not populated (spec FR-4.3 partially met — deferred). Resolved (commit8e48360): HTML report now reads and displays source code lines.
{
"version": 1,
"generated_by": "gd-tools 0.1.0",
"files": [
{
"file_id": 0,
"path": "res://scripts/player.gd",
"source_hash": "sha256:...",
"lines": [
{
"line": 5,
"id": 0,
"type": "statement"
},
{
"line": 8,
"id": 1,
"type": "branch",
"branch_type": "if_true"
},
{
"line": 10,
"id": 2,
"type": "branch",
"branch_type": "if_false"
}
]
}
]
}{
"version": 1,
"generated_at": "2026-07-08T12:00:00Z",
"files": [
{
"path": "res://scripts/player.gd",
"hits": {
"0": 15,
"1": 12,
"2": 3
}
}
]
}Using parser.parse(code, gather_metadata=True), the following Lark tree node
types are identified as executable (have line coverage):
| Statement Type | Lark Node | Coverage Type |
|---|---|---|
| Expression | expr_stmt |
statement |
| Return | return_stmt |
statement |
| Assignment | func_var_assigned |
statement |
| Typed assignment | func_var_typed_assgnd |
statement |
| Inferred assign | func_var_inf |
statement |
| If branch | if_branch |
branch (if_true) |
| Elif branch | elif_branch |
branch (elif_true) |
| Else branch | else_branch |
branch (if_false) |
| While loop | while_stmt |
branch (loop_body) |
| For loop | for_stmt |
branch (loop_body) |
| For loop (typed) | for_stmt_typed |
branch (loop_body) |
| Match | match_branch |
branch (match_case) |
| Ternary | test_expr |
branch (ternary_true, ternary_false) |
| Break | break_stmt |
statement |
| Continue | continue_stmt |
statement |
Declarative (not executable, excluded from coverage):
class_var_stmt, const_stmt, signal_stmt, enum_stmt, func_def,
static_func_def, extends_stmt, classname_stmt.
Skipped (no-op, not tracked): pass_stmt, breakpoint_stmt.
Inspired by felix-hellman/godot-code-coverage: inject a tracker call at the start of each executable block. The coverage addon maintains a registry of visited line IDs.
# Before instrumentation:
func take_damage(amount: int) -> void:
if amount > 0:
health -= amount
if health <= 0:
die()
return
# After instrumentation (Conceptual):
func take_damage(amount: int) -> void:
_gd_tools_cov.hit(0) # line 2: expr entry
if amount > 0:
_gd_tools_cov.hit(1) # line 3: if_true branch
health -= amount
if health <= 0:
_gd_tools_cov.hit(2) # line 5: nested if_true
die()
_gd_tools_cov.hit(3) # line 7: return
returnImplementation note: The exact instrumentation mechanism (source text manipulation vs. Script API bytecode injection) is an implementation detail to be resolved during development. The plan JSON provides line numbers and IDs; the GDScript addon decides how to inject trackers.
Branch injection strategy (Track 21): Two injection strategies are used depending on branch type:
- Before the line (statements,
if_true,loop_body): tracker injected at the target line's indentation, before it. - After the keyword line, inside the body (
match_case,if_false,elif_true): tracker injected on the line afterpattern:,else:, orelif:at the body indentation level. Injecting before these keyword lines would insert a statement between theif/elif/elsekeywords, breaking the GDScript block structure (orphanedelse/elif= syntax error). The_detect_body_indent()helper scans forward from the keyword line to find the body indentation.
Null guard (Track 20/21): The plan JSON uses null for non-branch
entries. GDScript's Dictionary.get(key, default) returns null when the
key exists with a null value. An explicit null check prevents assigning
null to a String-typed variable, which would crash instrumentation for
all files.
Autoload-based instrumentation (Track 24.5): Coverage instrumentation now
happens in _GDTCoverage._ready(), which runs as the first autoload (position
0) before any other autoload's _ready(). This eliminates ERR_ALREADY_IN_USE
errors: by instrumenting before autoloads create instances, all subsequent
load() calls return the instrumented source. The _GDTCoverage autoload is
prepended to the [autoload] section by register_coverage_autoload() in
init.py, with an auto-fix that moves it to position 0 if needed. Autoload
scripts (including scripts they instantiate) are now included in coverage.
Applied to lint, format, and coverage:
DEFAULT_EXCLUDES = ["addons", ".godot", ".gd-tools", ".git"]Rationale:
addons/— third-party code (other plugins). Not user code; shouldn't be linted, formatted, or measured for coverage..godot/— Godot's generated cache directory..gd-tools/— our own output directory (coverage data, reports, results)..git/— version control metadata.
Exclude entries support both bare names and path prefixes:
- Bare names (no
/or\): Matched against the basename of each discovered file's path. This is the original behavior and remains backward-compatible (e.g.,"addons"excludes any directory namedaddonsat any depth). - Path prefixes (containing
/or\): Normalized to the OS separator and matched as a path prefix viaos.path.relpath. This allows excluding specific subdirectories while keeping same-named directories elsewhere (e.g.,"src/vendor/addons"excludes only that specific path).
Autoload scripts are now included in coverage instrumentation. The
_GDTCoverage autoload (registered as the first autoload, position 0)
instruments all files in its _ready() before any other autoload
initializes. This ensures that when autoloads create instances of other
scripts during their _ready(), those instances use the instrumented
source. The previous resolve_autoload_paths() exclusion was removed ---
autoloads and the scripts they instantiate are fully covered.
gd-tools init generates gdlintrc and gdformatrc from the [lint] and
[format] sections of gd-tools.toml:
# gdlintrc (generated)
excluded_directories: !!set { .git: null, addons: null, .godot: null, .gd-tools: null }This ensures gdlint and gdformat work correctly when invoked standalone
(without gd-tools).
Coverage additionally excludes test directories from coverage targets (test files are not measured for coverage — they ARE the measurement tool):
[coverage]
exclude = ["addons", ".godot", ".gd-tools", ".git"]
test_dirs = ["test", "tests"]Test files are still linted and formatted (they are user code).
| Package | Purpose |
|---|---|
gdtoolkit |
Lark-based GDScript parser (lint, format, coverage plan gen) |
junitparser |
Parse the test run's JUnit XML output |
jinja2 |
HTML coverage report generation |
rich |
Terminal output (tables, colors, progress) |
tomli |
TOML config parsing (Python < 3.11 backport) |
requests |
Query PyPI for update notifications |
click |
CLI framework (or typer — TBD) |
packaging |
Version comparison for PyPI update notification |
| Package | Purpose |
|---|---|
pytest |
Test gd-tools itself |
ruff |
Lint gd-tools Python code |
black |
Format gd-tools Python code |
commitizen |
Conventional commit enforcement, automated semantic versioning, changelog generation |
| Component | Purpose |
|---|---|
| Coverage addon (bundled) | Runtime instrumentation + tracking |
| Component | Purpose |
|---|---|
| Godot 4.5+ | Runs the native test runner + instrumented tests |
gd-tools/
├── docs/
│ ├── PRD.md # This document
│ ├── USER_GUIDE.md # Complete CLI reference and user guide
│ ├── CONTRIBUTING.md # Contributor onboarding guide
│ ├── ARCHITECTURE.md # Coverage system architecture document
│ ├── ROADMAP.md # Development roadmap and track status
│ ├── TDD.md # Technical design document
│ ├── TESTING_STRATEGY.md # Test pyramid and coverage targets
│ └── SPIKE_coverage_instrumentation.md # Coverage spike results
├── src/
│ └── gd_tools/
│ ├── __init__.py
│ ├── __main__.py # Entry point: python -m gd_tools
│ ├── cli.py # Click/Typer CLI definitions
│ ├── update_check.py # PyPI update notification on CLI invocation
│ ├── addon_check.py # Stale addon version detection on CLI invocation
│ ├── config.py # gd-tools.toml loading & validation
│ ├── godot.py # Godot binary detection + invocation
│ ├── init.py # `gd-tools init` logic
│ ├── doctor.py # `gd-tools doctor` logic
│ ├── test_runner.py # Test result models shared by runtimes
│ ├── lint_runner.py # `gd-tools lint` — gdlint wrapper
│ ├── output.py # Shared terminal output module (Rich-based)
│ ├── verbosity.py # Global verbosity context (Verbosity enum + accessors)
│ ├── format_runner.py # `gd-tools format` — gdformat wrapper
│ ├── version.py # `gd-tools version` — component version detection
│ ├── coverage/
│ │ ├── __init__.py
│ │ ├── orchestrator.py # Coverage CLI orchestration (test --coverage, report, merge, show)
│ │ ├── plan_generator.py # Parse GDScript, build instrumentation plan
│ │ ├── reporter.py # Read coverage data, generate reports
│ │ ├── html_reporter.py # Jinja2 HTML report
│ │ ├── lcov_reporter.py # LCOV format
│ │ ├── cobertura_reporter.py # Cobertura XML
│ │ ├── terminal_reporter.py # Rich terminal table
│ │ └── templates/
│ │ ├── index.html # HTML report index page
│ │ └── file.html # HTML per-file page
│ └── addons/
│ └── gd-tools-coverage/
│ ├── coverage.gd # Core instrumentation + tracker
│ ├── pre_run_hook.gd # GUT pre-run hook
│ └── post_run_hook.gd # GUT post-run hook
├── tests/ # Python tests for gd-tools itself
├── pyproject.toml
└── README.md
.gd-tools/
├── coverage/
│ ├── plan.json # Instrumentation plan (cached when source unchanged)
│ ├── coverage.json # Raw coverage data from last run
│ ├── results.xml # JUnit XML from last test run
│ └── html/ # HTML report output
│ ├── index.html
│ └── ...
└── results.xml # JUnit XML (if --junit-xml not specified)
Not generated by gd-tools init and not read by any runtime. The migration
tooling (gd-tools migrate --config-only / --apply) translates supported
settings into gd-tools.toml; the file itself is preserved on disk.
- name: Install gd-tools
run: pip install gd-tools-cli
- name: Bootstrap
run: gd-tools init --non-interactive
- name: Lint
run: gd-tools lint
- name: Format check
run: gd-tools format --check
- name: Test with coverage
run: gd-tools test --coverage --min 80 --junit-xml .gd-tools/results.xml
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: test-results
path: .gd-tools/results.xml
- name: Upload coverage
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: .gd-tools/coverage/html/| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Test/lint/format failure or coverage below threshold |
| 2 | Environment/config error, or --verbose and --quiet used together |
gd-tools' own CI/CD pipeline is implemented in .github/workflows/:
ci.yml— Staged gating:lint-format-unit→integration→e2e, plus a cross-platform matrix (Ubuntu + Windows, Python 3.10/3.11/3.12). Coverage uploaded to codecov.io. JUnit XML results uploaded as artifacts.commit-check.yml— Conventional commit message validation on pull requests viacz check(commitizen).release.yml— Tag push (v*) triggers build + TestPyPI upload, then production PyPI upload (requiresPYPI_API_TOKENsecret)..github/SECRETS.md— Required GitHub secrets documentation.
These were raised during design discussion but not yet resolved. They should be addressed during implementation, with the simplest reasonable default chosen first:
-
gdlintrc/gdformatrcgeneration policy — Shouldinitalways overwrite these, or only generate if they don't exist? Default: generate if missing, warn if existing and differs from expected. -
Coverage of addon files — Should coverage reports include addon files as "0% covered" (informational) or exclude them entirely? Default: exclude entirely (clean reports).
-
Additional default excludes — Beyond
addons,.godot,.gd-tools,.git, are there other directories to exclude by default? Default: no; users can add via config. -
CLI framework choice —
clickvstyper? Both are mature. Default: decide during implementation;clickhas broader ecosystem,typerhas better type hints. -
Instrumentation mechanism — Source text manipulation vs. Godot Script API bytecode injection? This is the core implementation risk and should be prototyped early.
-
Coverage for manual playtesting — Future:
gd-tools coverage runto instrument and track coverage during manual game playtesting (not just tests). Out of scope for v1.
Delivered since v1 (see the roadmap
and the CHANGELOG for details): gd-tools coverage run (playtest coverage),
the editor plugin (test/coverage dock with a coverage heatmap overlay), watch
mode (gd-tools test --watch), --changed suite selection, and coverage diff
(coverage save-baseline / coverage diff).
Still future:
- GdUnit4 support — Alternative test framework support (legacy GUT
support was removed in v0.6.0; see
gd-tools migrate). - Pre-commit hooks —
gd-tools install-hooksfor pre-commit framework integration.
| Term | Definition |
|---|---|
| GUT | Godot Unit Test — the dominant GDScript test framework |
| gdtoolkit | Python package (Scony/godot-gdscript-toolkit) providing gdlint + gdformat + Lark-based parser |
| Lark | Python parsing library used by gdtoolkit; supports LALR and Earley parsers |
| Architecture C | Hybrid coverage architecture: Python plan generation + GDScript runtime instrumentation + Python reporting |
| Instrumentation plan | JSON file describing which lines/branches to track, generated by Python, consumed by GDScript addon |
| LCOV | Line coverage data format (GNU toolchain standard, used by codecov.io, coveralls) |
| Cobertura | XML-based coverage report format (used by Jenkins, GitLab CI) |
| JUnit XML | Standard XML format for test results (Jenkins, GitHub Actions, GitLab CI) |