gd-tools-cli 0.7.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/PKG-INFO +55 -5
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/README.md +53 -4
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/pyproject.toml +7 -2
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-coverage/coverage.gd +185 -22
- gd_tools_cli-0.9.0/src/gd_tools/addons/gd-tools-test/gd_tools_native_coverage.gd +622 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_preflight.gd +2 -2
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_runner.gd +12 -3
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/atomic_io.py +35 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/changes.py +82 -0
- gd_tools_cli-0.9.0/src/gd_tools/cli.py +219 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/__init__.py +6 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/clean.py +171 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/completion.py +61 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/config.py +255 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/coverage.py +378 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/doctor.py +24 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/format.py +73 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/init.py +25 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/install_hooks.py +118 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/lint.py +64 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/migrate.py +137 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/test.py +359 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/test_validation.py +210 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/version.py +36 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/config.py +6 -0
- gd_tools_cli-0.9.0/src/gd_tools/coverage/html_reporter.py +283 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/orchestrator.py +125 -1
- gd_tools_cli-0.9.0/src/gd_tools/coverage/patch.py +350 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/plan_generator.py +286 -45
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/reporter.py +21 -8
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/templates/file.html +40 -3
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/templates/index.html +58 -6
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/lint_runner.py +98 -3
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/artifacts.py +39 -31
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/command.py +125 -3
- gd_tools_cli-0.9.0/src/gd_tools/native_test/flaky.py +63 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/orchestrator.py +167 -35
- gd_tools_cli-0.9.0/src/gd_tools/native_test/ordering.py +110 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/protocol.py +8 -11
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/output.py +14 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/test_results.py +67 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/session.py +18 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/PKG-INFO +55 -5
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/SOURCES.txt +17 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/requires.txt +1 -0
- gd_tools_cli-0.7.0/src/gd_tools/addons/gd-tools-test/gd_tools_native_coverage.gd +0 -318
- gd_tools_cli-0.7.0/src/gd_tools/cli.py +0 -1629
- gd_tools_cli-0.7.0/src/gd_tools/coverage/html_reporter.py +0 -142
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/setup.cfg +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/__init__.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/__main__.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addon_check.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/__init__.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/coverage_overlay.gd +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/dock.gd +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/plugin.cfg +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/plugin.gd +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_mock.gd +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_parameter_naming.gd +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_serializer.gd +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_store.gd +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test.gd +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_context.gd +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/clean.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/__init__.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/cobertura_reporter.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/diff_reporter.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/lcov_reporter.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/omissions.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/playtest.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/terminal_reporter.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/doctor.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/errors.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/file_discovery.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/format_runner.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/gh_annotations.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/godot.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/init.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/__init__.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/apply.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/gutconfig.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/reporter.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/rewrite.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/scan.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/translate.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/__init__.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/bridge_scan.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/discovery.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/import_cache.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/preflight.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/preflight_cache.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/pre_commit.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/schema.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/update_check.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/verbosity.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/version.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/__init__.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/coalescer.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/loop.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/mapping.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/observer.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/scope.py +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/dependency_links.txt +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/entry_points.txt +0 -0
- {gd_tools_cli-0.7.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gd-tools-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A modern development workflow CLI for GDScript projects in Godot 4.5+
|
|
5
5
|
Author-email: mansyar <m.ansyarafi@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -35,6 +35,7 @@ Requires-Dist: watchdog
|
|
|
35
35
|
Provides-Extra: dev
|
|
36
36
|
Requires-Dist: pytest; extra == "dev"
|
|
37
37
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
38
|
+
Requires-Dist: pytest-xdist; extra == "dev"
|
|
38
39
|
Requires-Dist: ruff==0.15.21; extra == "dev"
|
|
39
40
|
Requires-Dist: black; extra == "dev"
|
|
40
41
|
Requires-Dist: commitizen; extra == "dev"
|
|
@@ -77,7 +78,7 @@ One install, one config, one mental model.
|
|
|
77
78
|
| **Native test runtime** | Tests extend `GdToolsTest` and run inside Godot itself. Scene and resource integration tests reach the real scene tree through an explicit context object rather than a proxy. Doubles, partial doubles, and stubbing isolate collaborators; spy assertions cover calls, argument wildcards, call order, and property values. |
|
|
78
79
|
| **Snapshot testing** | `assert_snapshot(value)` pins rendered values -- including object property dumps and node trees -- against versioned, diffable `.snap` files. First run writes and passes; mismatches fail with a unified diff, and `--snapshot-update` accepts the new output. |
|
|
79
80
|
| **Zero-friction bootstrap** | `gd-tools init` gets a project fully set up in under a minute -- native test and coverage addons deployed, configs generated. |
|
|
80
|
-
| **Coverage gap-filling** | Production-quality line and branch coverage for GDScript -- HTML, LCOV, Cobertura, and JSON reports, plus GitHub Actions annotations that surface failures inline on pull requests. Independent line (`--min`) and branch (`--min-branch`) gates. `# gd-tools: no cover` annotations exclude debug-only code from the numbers. `coverage run` collects coverage during manual playtest sessions, not just automated tests. |
|
|
81
|
+
| **Coverage gap-filling** | Production-quality line and branch coverage for GDScript -- HTML, LCOV, Cobertura, and JSON reports, plus GitHub Actions annotations that surface failures inline on pull requests. Independent line (`--min`) and branch (`--min-branch`) gates. `# gd-tools: no cover` annotations exclude debug-only code from the numbers. `coverage run` collects coverage during manual playtest sessions, not just automated tests. Patch coverage (`coverage diff --patch`) reports coverage over only the lines a pull request changes, with its own gate and inline annotations. |
|
|
81
82
|
| **CI/CD friendly** | Exit codes, `--check` flags, machine-readable output (JSON, JUnit XML, LCOV, Cobertura), and GitHub Actions annotations for lint violations and coverage gates. No interactive prompts in CI mode. |
|
|
82
83
|
| **Standalone compatibility** | gdlint and gdformat continue to work if invoked directly. `gd-tools` is a layer on top, not a lock-in. |
|
|
83
84
|
|
|
@@ -141,11 +142,11 @@ func test_health_starts_at_full() -> void:
|
|
|
141
142
|
|---------|-------------|
|
|
142
143
|
| `gd-tools init` | Bootstrap a Godot project -- deploy the native test and coverage addons, generate configs. |
|
|
143
144
|
| `gd-tools doctor` | Diagnose the development environment -- Godot, native test addon, coverage addon, tooling; reports legacy GUT artifacts as migration advice. |
|
|
144
|
-
| `gd-tools test` | Run tests with optional coverage, thresholds, and JUnit XML output. Suites must extend `GdToolsTest`; `GutTest` suites are rejected with exit 2 and migration guidance. Accepts optional path arguments to override configured test directories. `--parallel N` runs suites through a bounded worker pool. `--changed` runs only the suites mapped from git-changed files. `--watch` re-runs affected suites on `.gd` file changes. `--durations N` reports the N slowest tests after the run. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
145
|
+
| `gd-tools test` | Run tests with optional coverage, thresholds, and JUnit XML output. Suites must extend `GdToolsTest`; `GutTest` suites are rejected with exit 2 and migration guidance. Accepts optional path arguments to override configured test directories. `--parallel N` runs suites through a bounded worker pool. `--changed` runs only the suites mapped from git-changed files. `--watch` re-runs affected suites on `.gd` file changes. `--durations N` reports the N slowest tests after the run. `--exitfirst` (`-x`) stops dispatching new suites after the first failing suite; `--shard K/N` runs one round-robin shard for CI matrix splitting; `--random-order` (with `--no-random-order` to turn a configured shuffle off) shuffles suite and test-method execution order, and `--seed N` pins the shuffle so a run can be replayed. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
145
146
|
| `gd-tools migrate` | Guided GUT-to-native migration. Default: read-only report with unsupported-construct inventory and proposed base-class rewrites. `--apply` renames clean suites to `GdToolsTest` and translates `.gutconfig.json` into `gd-tools.toml` (merge, never clobber). `--config-only` translates config only. |
|
|
146
147
|
| `gd-tools lint` | Lint GDScript files using gdlint with text, JSON, or GitHub Actions annotation output. Accepts one or more file or directory paths. |
|
|
147
148
|
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
|
|
148
|
-
| `gd-tools coverage` | Coverage subcommands -- `report`, `merge`, `show`, `save-baseline`, `diff` (baseline comparison for CI regression gates), and `run` (collect coverage during a manual playtest session). |
|
|
149
|
+
| `gd-tools coverage` | Coverage subcommands -- `report`, `merge`, `show`, `save-baseline`, `diff` (baseline comparison for CI regression gates; with `--patch`, PR-scoped coverage over changed lines), and `run` (collect coverage during a manual playtest session). |
|
|
149
150
|
| `gd-tools config` | Configuration management -- `show` (display resolved config), `validate` (check config validity), `schema` (print or write the JSON Schema for `gd-tools.toml`). |
|
|
150
151
|
| `gd-tools version` | Display versions of all gd-tools components (gd-tools, Godot, gdtoolkit, Python) in a table or JSON. |
|
|
151
152
|
| `gd-tools completion` | Generate shell completion scripts for bash, zsh, fish, or PowerShell. |
|
|
@@ -162,6 +163,11 @@ gd-tools test --parallel 4 # run suites with 4 concurrent worker
|
|
|
162
163
|
gd-tools test --durations 10 # report the 10 slowest tests
|
|
163
164
|
gd-tools test --changed # only suites mapped from git-changed files
|
|
164
165
|
gd-tools test --changed --base main # PR/CI mode: diff from merge-base with main
|
|
166
|
+
gd-tools test --exitfirst # stop dispatching after the first failing suite
|
|
167
|
+
gd-tools test --shard 2/4 # CI sharding: run suite shard 2 of 4
|
|
168
|
+
gd-tools test --random-order # shuffle suite and test method order
|
|
169
|
+
gd-tools test --seed 42 # --seed implies --random-order; replayable
|
|
170
|
+
gd-tools test --no-random-order # discovery order for one run, config regardless
|
|
165
171
|
```
|
|
166
172
|
|
|
167
173
|
`--tag` is repeatable, and `--test-timeout` (per test) is separate from
|
|
@@ -190,6 +196,48 @@ pull-request CI. `--changed` composes with `--parallel`, `--coverage`, and
|
|
|
190
196
|
the `--suite`/`--test`/`--tag` filters (the active filters bound the
|
|
191
197
|
mapping domain); an empty change set exits 0 without launching Godot.
|
|
192
198
|
|
|
199
|
+
`--exitfirst` (short form `-x`) stops dispatching **new** suites after the
|
|
200
|
+
first suite whose final result is a failure — a test failure or an
|
|
201
|
+
infrastructure error. In-flight suites finish and are collected; suites that
|
|
202
|
+
were never started are reported as skipped (`not_run`) and summarized in a
|
|
203
|
+
`Stopped early: fail-fast after suite <id> (K of M suites skipped)` line.
|
|
204
|
+
Passes and skips never trigger the gate, and a suite that passes after a
|
|
205
|
+
retry does not either — only final suite results decide. The exit code stays
|
|
206
|
+
1 for test failures. `--exitfirst` composes with `--parallel` (the pool
|
|
207
|
+
drains instead of killing workers), with `--coverage` (the report covers the
|
|
208
|
+
suites that ran, labeled partial when plan targets went unmeasured), and with
|
|
209
|
+
`--watch` (the gate resets for every re-run).
|
|
210
|
+
|
|
211
|
+
`--shard K/N` runs only the suites assigned to shard `K` of `N` — suite *i*
|
|
212
|
+
of the deterministic plan order belongs to shard `(i mod N) + 1` — for
|
|
213
|
+
splitting one test run across CI jobs. Selection happens after `--changed`
|
|
214
|
+
filtering and before `--parallel`, so each flag keeps exactly one job.
|
|
215
|
+
`--shard 1/1` is valid and equivalent to no sharding; malformed values
|
|
216
|
+
(`--shard 4/3`, `--shard 3`) exit 2 before any work. Each shard produces its
|
|
217
|
+
own coverage report; merge the shards with `gd-tools coverage merge`.
|
|
218
|
+
`--shard` and `--watch` are rejected together (exit 2). See the
|
|
219
|
+
[sharding recipe](./docs/USER_GUIDE.md#sharding-a-test-run-across-ci-jobs)
|
|
220
|
+
for a GitHub Actions matrix example.
|
|
221
|
+
|
|
222
|
+
`--random-order` shuffles the execution order of the selected suites, and of
|
|
223
|
+
the test methods within each suite, to surface tests that depend on a
|
|
224
|
+
neighbour's side effects. The order is a pure function of the seed and the
|
|
225
|
+
selected suite/test set: the run summary reports
|
|
226
|
+
`Random order: seed <N> (replay with --seed <N>)` — exempt from `--quiet`,
|
|
227
|
+
since a shuffled failure cannot be reproduced without its seed — and
|
|
228
|
+
re-running with that
|
|
229
|
+
seed reproduces the order exactly. `--seed N` pins the shuffle and implies
|
|
230
|
+
`--random-order`; without it a fresh 31-bit seed is generated per run. Persist
|
|
231
|
+
the choice with `random_order = true` and `seed = 42` under `[test]`, and undo
|
|
232
|
+
it for one run with the paired `--no-random-order`. Shuffling
|
|
233
|
+
never changes shard membership — `--shard` assigns suites from the
|
|
234
|
+
deterministic plan order, so CI matrix jobs still cover every suite exactly
|
|
235
|
+
once — and `--watch` resolves one seed for the whole session so successive
|
|
236
|
+
re-runs stay comparable. `--verbose` lists the resolved order suite-by-suite,
|
|
237
|
+
and the seed is recorded in `artifacts.json`. A seed reproduces an order for
|
|
238
|
+
the same Python version, not across versions: `random.Random` is what draws
|
|
239
|
+
the order and CPython may change its shuffle algorithm between releases.
|
|
240
|
+
|
|
193
241
|
Repeat runs are faster: the `godot --headless --import` step is cached under
|
|
194
242
|
`.gd-tools/native/import-cache/` and skipped when `project.godot`, any
|
|
195
243
|
project source/resource file (scripts, scenes, import sidecars, addons), or
|
|
@@ -378,8 +426,10 @@ prefix = "test_"
|
|
|
378
426
|
suffix = ".gd"
|
|
379
427
|
runtime = "native" # native (default); "gut" is no longer a runnable runtime
|
|
380
428
|
timeout_seconds = 5.0 # default per-test timeout for async native tests
|
|
381
|
-
retries = 0 # opt-in retries per native test
|
|
429
|
+
retries = 0 # opt-in retries per native test; retried passes are flagged as flaky
|
|
382
430
|
# parallel = 4 # opt-in parallel suite execution (1-32; unset runs sequentially)
|
|
431
|
+
# random_order = true # opt-in shuffled suite and test-method order
|
|
432
|
+
# seed = 42 # pin the shuffle seed so a shuffled run is replayable
|
|
383
433
|
tags = [] # native suite tag filters
|
|
384
434
|
|
|
385
435
|
[lint]
|
|
@@ -36,7 +36,7 @@ One install, one config, one mental model.
|
|
|
36
36
|
| **Native test runtime** | Tests extend `GdToolsTest` and run inside Godot itself. Scene and resource integration tests reach the real scene tree through an explicit context object rather than a proxy. Doubles, partial doubles, and stubbing isolate collaborators; spy assertions cover calls, argument wildcards, call order, and property values. |
|
|
37
37
|
| **Snapshot testing** | `assert_snapshot(value)` pins rendered values -- including object property dumps and node trees -- against versioned, diffable `.snap` files. First run writes and passes; mismatches fail with a unified diff, and `--snapshot-update` accepts the new output. |
|
|
38
38
|
| **Zero-friction bootstrap** | `gd-tools init` gets a project fully set up in under a minute -- native test and coverage addons deployed, configs generated. |
|
|
39
|
-
| **Coverage gap-filling** | Production-quality line and branch coverage for GDScript -- HTML, LCOV, Cobertura, and JSON reports, plus GitHub Actions annotations that surface failures inline on pull requests. Independent line (`--min`) and branch (`--min-branch`) gates. `# gd-tools: no cover` annotations exclude debug-only code from the numbers. `coverage run` collects coverage during manual playtest sessions, not just automated tests. |
|
|
39
|
+
| **Coverage gap-filling** | Production-quality line and branch coverage for GDScript -- HTML, LCOV, Cobertura, and JSON reports, plus GitHub Actions annotations that surface failures inline on pull requests. Independent line (`--min`) and branch (`--min-branch`) gates. `# gd-tools: no cover` annotations exclude debug-only code from the numbers. `coverage run` collects coverage during manual playtest sessions, not just automated tests. Patch coverage (`coverage diff --patch`) reports coverage over only the lines a pull request changes, with its own gate and inline annotations. |
|
|
40
40
|
| **CI/CD friendly** | Exit codes, `--check` flags, machine-readable output (JSON, JUnit XML, LCOV, Cobertura), and GitHub Actions annotations for lint violations and coverage gates. No interactive prompts in CI mode. |
|
|
41
41
|
| **Standalone compatibility** | gdlint and gdformat continue to work if invoked directly. `gd-tools` is a layer on top, not a lock-in. |
|
|
42
42
|
|
|
@@ -100,11 +100,11 @@ func test_health_starts_at_full() -> void:
|
|
|
100
100
|
|---------|-------------|
|
|
101
101
|
| `gd-tools init` | Bootstrap a Godot project -- deploy the native test and coverage addons, generate configs. |
|
|
102
102
|
| `gd-tools doctor` | Diagnose the development environment -- Godot, native test addon, coverage addon, tooling; reports legacy GUT artifacts as migration advice. |
|
|
103
|
-
| `gd-tools test` | Run tests with optional coverage, thresholds, and JUnit XML output. Suites must extend `GdToolsTest`; `GutTest` suites are rejected with exit 2 and migration guidance. Accepts optional path arguments to override configured test directories. `--parallel N` runs suites through a bounded worker pool. `--changed` runs only the suites mapped from git-changed files. `--watch` re-runs affected suites on `.gd` file changes. `--durations N` reports the N slowest tests after the run. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
103
|
+
| `gd-tools test` | Run tests with optional coverage, thresholds, and JUnit XML output. Suites must extend `GdToolsTest`; `GutTest` suites are rejected with exit 2 and migration guidance. Accepts optional path arguments to override configured test directories. `--parallel N` runs suites through a bounded worker pool. `--changed` runs only the suites mapped from git-changed files. `--watch` re-runs affected suites on `.gd` file changes. `--durations N` reports the N slowest tests after the run. `--exitfirst` (`-x`) stops dispatching new suites after the first failing suite; `--shard K/N` runs one round-robin shard for CI matrix splitting; `--random-order` (with `--no-random-order` to turn a configured shuffle off) shuffles suite and test-method execution order, and `--seed N` pins the shuffle so a run can be replayed. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
104
104
|
| `gd-tools migrate` | Guided GUT-to-native migration. Default: read-only report with unsupported-construct inventory and proposed base-class rewrites. `--apply` renames clean suites to `GdToolsTest` and translates `.gutconfig.json` into `gd-tools.toml` (merge, never clobber). `--config-only` translates config only. |
|
|
105
105
|
| `gd-tools lint` | Lint GDScript files using gdlint with text, JSON, or GitHub Actions annotation output. Accepts one or more file or directory paths. |
|
|
106
106
|
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
|
|
107
|
-
| `gd-tools coverage` | Coverage subcommands -- `report`, `merge`, `show`, `save-baseline`, `diff` (baseline comparison for CI regression gates), and `run` (collect coverage during a manual playtest session). |
|
|
107
|
+
| `gd-tools coverage` | Coverage subcommands -- `report`, `merge`, `show`, `save-baseline`, `diff` (baseline comparison for CI regression gates; with `--patch`, PR-scoped coverage over changed lines), and `run` (collect coverage during a manual playtest session). |
|
|
108
108
|
| `gd-tools config` | Configuration management -- `show` (display resolved config), `validate` (check config validity), `schema` (print or write the JSON Schema for `gd-tools.toml`). |
|
|
109
109
|
| `gd-tools version` | Display versions of all gd-tools components (gd-tools, Godot, gdtoolkit, Python) in a table or JSON. |
|
|
110
110
|
| `gd-tools completion` | Generate shell completion scripts for bash, zsh, fish, or PowerShell. |
|
|
@@ -121,6 +121,11 @@ gd-tools test --parallel 4 # run suites with 4 concurrent worker
|
|
|
121
121
|
gd-tools test --durations 10 # report the 10 slowest tests
|
|
122
122
|
gd-tools test --changed # only suites mapped from git-changed files
|
|
123
123
|
gd-tools test --changed --base main # PR/CI mode: diff from merge-base with main
|
|
124
|
+
gd-tools test --exitfirst # stop dispatching after the first failing suite
|
|
125
|
+
gd-tools test --shard 2/4 # CI sharding: run suite shard 2 of 4
|
|
126
|
+
gd-tools test --random-order # shuffle suite and test method order
|
|
127
|
+
gd-tools test --seed 42 # --seed implies --random-order; replayable
|
|
128
|
+
gd-tools test --no-random-order # discovery order for one run, config regardless
|
|
124
129
|
```
|
|
125
130
|
|
|
126
131
|
`--tag` is repeatable, and `--test-timeout` (per test) is separate from
|
|
@@ -149,6 +154,48 @@ pull-request CI. `--changed` composes with `--parallel`, `--coverage`, and
|
|
|
149
154
|
the `--suite`/`--test`/`--tag` filters (the active filters bound the
|
|
150
155
|
mapping domain); an empty change set exits 0 without launching Godot.
|
|
151
156
|
|
|
157
|
+
`--exitfirst` (short form `-x`) stops dispatching **new** suites after the
|
|
158
|
+
first suite whose final result is a failure — a test failure or an
|
|
159
|
+
infrastructure error. In-flight suites finish and are collected; suites that
|
|
160
|
+
were never started are reported as skipped (`not_run`) and summarized in a
|
|
161
|
+
`Stopped early: fail-fast after suite <id> (K of M suites skipped)` line.
|
|
162
|
+
Passes and skips never trigger the gate, and a suite that passes after a
|
|
163
|
+
retry does not either — only final suite results decide. The exit code stays
|
|
164
|
+
1 for test failures. `--exitfirst` composes with `--parallel` (the pool
|
|
165
|
+
drains instead of killing workers), with `--coverage` (the report covers the
|
|
166
|
+
suites that ran, labeled partial when plan targets went unmeasured), and with
|
|
167
|
+
`--watch` (the gate resets for every re-run).
|
|
168
|
+
|
|
169
|
+
`--shard K/N` runs only the suites assigned to shard `K` of `N` — suite *i*
|
|
170
|
+
of the deterministic plan order belongs to shard `(i mod N) + 1` — for
|
|
171
|
+
splitting one test run across CI jobs. Selection happens after `--changed`
|
|
172
|
+
filtering and before `--parallel`, so each flag keeps exactly one job.
|
|
173
|
+
`--shard 1/1` is valid and equivalent to no sharding; malformed values
|
|
174
|
+
(`--shard 4/3`, `--shard 3`) exit 2 before any work. Each shard produces its
|
|
175
|
+
own coverage report; merge the shards with `gd-tools coverage merge`.
|
|
176
|
+
`--shard` and `--watch` are rejected together (exit 2). See the
|
|
177
|
+
[sharding recipe](./docs/USER_GUIDE.md#sharding-a-test-run-across-ci-jobs)
|
|
178
|
+
for a GitHub Actions matrix example.
|
|
179
|
+
|
|
180
|
+
`--random-order` shuffles the execution order of the selected suites, and of
|
|
181
|
+
the test methods within each suite, to surface tests that depend on a
|
|
182
|
+
neighbour's side effects. The order is a pure function of the seed and the
|
|
183
|
+
selected suite/test set: the run summary reports
|
|
184
|
+
`Random order: seed <N> (replay with --seed <N>)` — exempt from `--quiet`,
|
|
185
|
+
since a shuffled failure cannot be reproduced without its seed — and
|
|
186
|
+
re-running with that
|
|
187
|
+
seed reproduces the order exactly. `--seed N` pins the shuffle and implies
|
|
188
|
+
`--random-order`; without it a fresh 31-bit seed is generated per run. Persist
|
|
189
|
+
the choice with `random_order = true` and `seed = 42` under `[test]`, and undo
|
|
190
|
+
it for one run with the paired `--no-random-order`. Shuffling
|
|
191
|
+
never changes shard membership — `--shard` assigns suites from the
|
|
192
|
+
deterministic plan order, so CI matrix jobs still cover every suite exactly
|
|
193
|
+
once — and `--watch` resolves one seed for the whole session so successive
|
|
194
|
+
re-runs stay comparable. `--verbose` lists the resolved order suite-by-suite,
|
|
195
|
+
and the seed is recorded in `artifacts.json`. A seed reproduces an order for
|
|
196
|
+
the same Python version, not across versions: `random.Random` is what draws
|
|
197
|
+
the order and CPython may change its shuffle algorithm between releases.
|
|
198
|
+
|
|
152
199
|
Repeat runs are faster: the `godot --headless --import` step is cached under
|
|
153
200
|
`.gd-tools/native/import-cache/` and skipped when `project.godot`, any
|
|
154
201
|
project source/resource file (scripts, scenes, import sidecars, addons), or
|
|
@@ -337,8 +384,10 @@ prefix = "test_"
|
|
|
337
384
|
suffix = ".gd"
|
|
338
385
|
runtime = "native" # native (default); "gut" is no longer a runnable runtime
|
|
339
386
|
timeout_seconds = 5.0 # default per-test timeout for async native tests
|
|
340
|
-
retries = 0 # opt-in retries per native test
|
|
387
|
+
retries = 0 # opt-in retries per native test; retried passes are flagged as flaky
|
|
341
388
|
# parallel = 4 # opt-in parallel suite execution (1-32; unset runs sequentially)
|
|
389
|
+
# random_order = true # opt-in shuffled suite and test-method order
|
|
390
|
+
# seed = 42 # pin the shuffle seed so a shuffled run is replayable
|
|
342
391
|
tags = [] # native suite tag filters
|
|
343
392
|
|
|
344
393
|
[lint]
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "gd-tools-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "A modern development workflow CLI for GDScript projects in Godot 4.5+"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -49,6 +49,7 @@ Documentation = "https://github.com/mansyar/gd-tools#readme"
|
|
|
49
49
|
dev = [
|
|
50
50
|
"pytest",
|
|
51
51
|
"pytest-cov",
|
|
52
|
+
"pytest-xdist",
|
|
52
53
|
"ruff==0.15.21",
|
|
53
54
|
"black",
|
|
54
55
|
"commitizen",
|
|
@@ -71,6 +72,10 @@ where = ["src"]
|
|
|
71
72
|
|
|
72
73
|
[tool.pytest.ini_options]
|
|
73
74
|
testpaths = ["tests"]
|
|
75
|
+
# Import gd_tools from this checkout's src/ rather than any globally
|
|
76
|
+
# installed (editable) copy, so worktrees and CI checkouts always test
|
|
77
|
+
# their own tree.
|
|
78
|
+
pythonpath = ["src"]
|
|
74
79
|
markers = [
|
|
75
80
|
"unit: Unit tests for individual modules.",
|
|
76
81
|
"integration: Integration tests for command flows.",
|
|
@@ -105,7 +110,7 @@ target-version = ["py310"]
|
|
|
105
110
|
|
|
106
111
|
[tool.commitizen]
|
|
107
112
|
name = "cz_conventional_commits"
|
|
108
|
-
version = "0.
|
|
113
|
+
version = "0.9.0"
|
|
109
114
|
version_files = ["pyproject.toml:version"]
|
|
110
115
|
changelog_file = "CHANGELOG.md"
|
|
111
116
|
tag_format = "v$version"
|
|
@@ -17,6 +17,28 @@ extends Node
|
|
|
17
17
|
|
|
18
18
|
const TRACKER_NAME = "_GDTCoverage"
|
|
19
19
|
|
|
20
|
+
## Every branch_type this collector knows how to measure. A plan that
|
|
21
|
+
## carries anything else was produced by a newer plan generator than
|
|
22
|
+
## this collector implements; instrumenting it would silently
|
|
23
|
+
## mis-measure the unknown arms, so the plan is rejected instead.
|
|
24
|
+
const KNOWN_BRANCH_TYPES: Array[String] = [
|
|
25
|
+
"if_true",
|
|
26
|
+
"elif_true",
|
|
27
|
+
"if_false",
|
|
28
|
+
"loop_body",
|
|
29
|
+
"match_case",
|
|
30
|
+
"ternary_true",
|
|
31
|
+
"ternary_false",
|
|
32
|
+
"and_site",
|
|
33
|
+
"and_right",
|
|
34
|
+
"and_short",
|
|
35
|
+
"or_site",
|
|
36
|
+
"or_right",
|
|
37
|
+
"or_short",
|
|
38
|
+
"assert_true",
|
|
39
|
+
"assert_false",
|
|
40
|
+
]
|
|
41
|
+
|
|
20
42
|
const PLAYTEST_ENV = "GD_TOOLS_COVERAGE_PLAYTEST"
|
|
21
43
|
|
|
22
44
|
const PLAYTEST_INTERVAL_ENV = "GD_TOOLS_COVERAGE_PLAYTEST_INTERVAL"
|
|
@@ -24,6 +46,7 @@ const PLAYTEST_INTERVAL_ENV = "GD_TOOLS_COVERAGE_PLAYTEST_INTERVAL"
|
|
|
24
46
|
const DEFAULT_PLAYTEST_INTERVAL: float = 5.0
|
|
25
47
|
|
|
26
48
|
var _hits: Dictionary = {}
|
|
49
|
+
var _derived: Dictionary = {}
|
|
27
50
|
|
|
28
51
|
var _active: bool = false
|
|
29
52
|
|
|
@@ -83,12 +106,24 @@ func hit(file_id: int, line_id: int) -> void:
|
|
|
83
106
|
func hit_ret(file_id: int, line_id: int, value: Variant) -> Variant:
|
|
84
107
|
## Record one instrumented hit and pass the tracked value through.
|
|
85
108
|
##
|
|
86
|
-
## Injected around ternary operands
|
|
87
|
-
## when it evaluates, without
|
|
109
|
+
## Injected around ternary operands and boolean-operator sites/right
|
|
110
|
+
## operands so each arm is measured exactly when it evaluates, without
|
|
111
|
+
## re-evaluating the condition.
|
|
88
112
|
hit(file_id, line_id)
|
|
89
113
|
return value
|
|
90
114
|
|
|
91
115
|
|
|
116
|
+
func hit_bool(file_id: int, true_id: int, false_id: int, value: Variant) -> Variant:
|
|
117
|
+
## Record exactly one boolean-arm hit and pass the value through.
|
|
118
|
+
##
|
|
119
|
+
## Injected around assert conditions: the condition's truth decides
|
|
120
|
+
## which of the two arms is recorded, and the value continues into
|
|
121
|
+
## the assert itself. One wrapper call therefore records exactly one
|
|
122
|
+
## of assert_true / assert_false per evaluation.
|
|
123
|
+
hit(file_id, true_id if value else false_id)
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
92
127
|
func get_hits() -> Dictionary:
|
|
93
128
|
return _hits
|
|
94
129
|
|
|
@@ -100,6 +135,7 @@ func get_omitted() -> Array:
|
|
|
100
135
|
|
|
101
136
|
func reset() -> void:
|
|
102
137
|
_hits.clear()
|
|
138
|
+
_derived.clear()
|
|
103
139
|
_omitted.clear()
|
|
104
140
|
|
|
105
141
|
|
|
@@ -194,6 +230,7 @@ func _build_coverage_json(hits: Dictionary, omitted: Array = []) -> Dictionary:
|
|
|
194
230
|
# Matches the post-run hook output contract exactly so every
|
|
195
231
|
# reporter consumes playtest sessions unchanged.
|
|
196
232
|
var files: Array = []
|
|
233
|
+
_derive_short_hits(hits)
|
|
197
234
|
for file_id in hits:
|
|
198
235
|
var file_hits: Dictionary = hits[file_id]
|
|
199
236
|
var hits_dict: Dictionary = {}
|
|
@@ -318,9 +355,57 @@ func _instrument_file(file_entry: Dictionary) -> bool:
|
|
|
318
355
|
# change to any percentage.
|
|
319
356
|
if not _hits.has(file_id):
|
|
320
357
|
_hits[file_id] = {}
|
|
358
|
+
_derive_short_arms(file_id, lines)
|
|
321
359
|
return true
|
|
322
360
|
|
|
323
361
|
|
|
362
|
+
func _derive_short_arms(file_id: int, lines: Array) -> void:
|
|
363
|
+
## Pair each ``<op>_short`` point with its operator's site and right
|
|
364
|
+
## point ids so coverage aggregation can derive short-circuit hits.
|
|
365
|
+
##
|
|
366
|
+
## Short-circuit arms have no span of their own: the collector records
|
|
367
|
+
## the site whenever the whole expression evaluates and the right arm
|
|
368
|
+
## whenever the right operand evaluates, so ``short = site - right``
|
|
369
|
+
## counts the evaluations the operator skipped. The plan emits each
|
|
370
|
+
## right operand immediately followed by its short arm, so pairing by
|
|
371
|
+
## most recent same-operator right is exact, including for chains.
|
|
372
|
+
var site_by_operator: Dictionary = {}
|
|
373
|
+
var last_right: Dictionary = {}
|
|
374
|
+
var pairs: Dictionary = {}
|
|
375
|
+
for entry in lines:
|
|
376
|
+
var branch_type := str(entry.get("branch_type", ""))
|
|
377
|
+
if branch_type.ends_with("_site"):
|
|
378
|
+
site_by_operator[branch_type.trim_suffix("_site")] = int(entry["id"])
|
|
379
|
+
elif branch_type.ends_with("_right"):
|
|
380
|
+
last_right[branch_type.trim_suffix("_right")] = int(entry["id"])
|
|
381
|
+
elif branch_type.ends_with("_short"):
|
|
382
|
+
var operator := branch_type.trim_suffix("_short")
|
|
383
|
+
if site_by_operator.has(operator) and last_right.has(operator):
|
|
384
|
+
pairs[int(entry["id"])] = [
|
|
385
|
+
site_by_operator[operator],
|
|
386
|
+
last_right[operator],
|
|
387
|
+
]
|
|
388
|
+
if not pairs.is_empty():
|
|
389
|
+
_derived[file_id] = pairs
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
func _derive_short_hits(hits: Dictionary) -> void:
|
|
393
|
+
## Fold derived short-circuit arm hits into the recorded hit counts.
|
|
394
|
+
##
|
|
395
|
+
## ``short = site - right`` clamped at zero: a negative result would
|
|
396
|
+
## mean the wrap double-fired, which is a defect, not a measurement.
|
|
397
|
+
for file_id in _derived:
|
|
398
|
+
if not hits.has(file_id):
|
|
399
|
+
continue
|
|
400
|
+
var pairs: Dictionary = _derived[file_id]
|
|
401
|
+
for short_id in pairs:
|
|
402
|
+
var site_hits := int(hits[file_id].get(pairs[short_id][0], 0))
|
|
403
|
+
var right_hits := int(hits[file_id].get(pairs[short_id][1], 0))
|
|
404
|
+
var short_hits := maxi(0, site_hits - right_hits)
|
|
405
|
+
if short_hits > 0:
|
|
406
|
+
hits[file_id][short_id] = short_hits
|
|
407
|
+
|
|
408
|
+
|
|
324
409
|
static func _extract_indent(line: String) -> String:
|
|
325
410
|
var indent: String = ""
|
|
326
411
|
for c in line:
|
|
@@ -345,17 +430,22 @@ static func _detect_body_indent(source_lines: PackedStringArray, pattern_index:
|
|
|
345
430
|
|
|
346
431
|
|
|
347
432
|
static func _inject_trackers(source: String, file_id: int, lines: Array) -> String:
|
|
348
|
-
var wrapped :=
|
|
433
|
+
var wrapped := _wrap_expression_operands(source, file_id, lines)
|
|
349
434
|
var source_lines: PackedStringArray = wrapped.split("\n")
|
|
350
435
|
var sorted_lines: Array = lines.duplicate(true)
|
|
351
436
|
sorted_lines.sort_custom(func(a, b): return int(a["line"]) > int(b["line"]))
|
|
352
437
|
for entry in sorted_lines:
|
|
353
|
-
# Ternary arms are instrumented by wrapping
|
|
354
|
-
# (see
|
|
355
|
-
# shared anchor line fires both arms
|
|
356
|
-
# report an uncovered arm.
|
|
438
|
+
# Ternary and boolean-operator arms are instrumented by wrapping
|
|
439
|
+
# their operand text (see _wrap_expression_operands); a
|
|
440
|
+
# line-inserted hit() on the shared anchor line fires both arms
|
|
441
|
+
# in lockstep and can never report an uncovered arm.
|
|
357
442
|
if entry.get("operand_span") != null:
|
|
358
443
|
continue
|
|
444
|
+
# Short-circuit arms are derived at aggregation as site - right; a
|
|
445
|
+
# line-inserted hit() would fire whenever the statement runs
|
|
446
|
+
# rather than when the operator skipped its right operand.
|
|
447
|
+
if str(entry.get("branch_type", "")).ends_with("_short"):
|
|
448
|
+
continue
|
|
359
449
|
var target_line_num: int = int(entry["line"])
|
|
360
450
|
var line_id: int = int(entry["id"])
|
|
361
451
|
var target_index: int = target_line_num - 1
|
|
@@ -398,26 +488,60 @@ static func _offset_of(source: String, line: int, col: int) -> int:
|
|
|
398
488
|
return target
|
|
399
489
|
|
|
400
490
|
|
|
401
|
-
static func
|
|
402
|
-
## Replace each
|
|
491
|
+
static func _wrap_expression_operands(source: String, file_id: int, lines: Array) -> String:
|
|
492
|
+
## Replace each tracked operand with a value-preserving tracker call.
|
|
493
|
+
##
|
|
494
|
+
## The plan records the source span of every ternary arm operand and
|
|
495
|
+
## boolean-operator site/right operand. Wrapping the operand records
|
|
496
|
+
## the arm's hit exactly when that operand evaluates, keeping values,
|
|
497
|
+
## evaluation order, and single evaluation intact. Line numbers are
|
|
498
|
+
## unchanged: a wrapper never adds or removes a newline, so the
|
|
499
|
+
## line-based insertion that follows stays valid.
|
|
403
500
|
##
|
|
404
|
-
##
|
|
405
|
-
##
|
|
406
|
-
##
|
|
407
|
-
## intact. Line numbers are unchanged: a wrapper never adds or removes
|
|
408
|
-
## a newline, so the line-based insertion that follows stays valid.
|
|
501
|
+
## Assert conditions carry two arm ids (assert_true/assert_false)
|
|
502
|
+
## over one shared span: the pair becomes a single value-aware
|
|
503
|
+
## hit_bool wrap that records exactly one arm per evaluation.
|
|
409
504
|
var spans: Array = []
|
|
505
|
+
var assert_pairs: Dictionary = {}
|
|
410
506
|
for entry in lines:
|
|
507
|
+
var branch_type := str(entry.get("branch_type", ""))
|
|
411
508
|
var span: Variant = entry.get("operand_span")
|
|
412
509
|
if span == null:
|
|
413
510
|
continue
|
|
511
|
+
if branch_type == "assert_true" or branch_type == "assert_false":
|
|
512
|
+
var key := str(span)
|
|
513
|
+
if not assert_pairs.has(key):
|
|
514
|
+
assert_pairs[key] = {
|
|
515
|
+
"span": span,
|
|
516
|
+
"assert_true": -1,
|
|
517
|
+
"assert_false": -1,
|
|
518
|
+
}
|
|
519
|
+
assert_pairs[key][branch_type] = int(entry["id"])
|
|
520
|
+
continue
|
|
414
521
|
var start := _offset_of(source, int(span[0]), int(span[1]))
|
|
415
522
|
var end := _offset_of(source, int(span[2]), int(span[3]))
|
|
416
523
|
if start < 0 or end < 0 or start >= end or end > source.length():
|
|
417
524
|
# Fail open: an unusable span leaves the arm uninstrumented
|
|
418
525
|
# rather than corrupting the source.
|
|
419
526
|
continue
|
|
420
|
-
spans.append({"start": start, "end": end, "id": int(entry["id"])})
|
|
527
|
+
spans.append({"start": start, "end": end, "kind": "ret", "id": int(entry["id"])})
|
|
528
|
+
for key in assert_pairs:
|
|
529
|
+
var pair: Dictionary = assert_pairs[key]
|
|
530
|
+
if int(pair["assert_true"]) < 0 or int(pair["assert_false"]) < 0:
|
|
531
|
+
# Fail open: an incomplete pair leaves the assert uninstrumented.
|
|
532
|
+
continue
|
|
533
|
+
var span: Array = pair["span"]
|
|
534
|
+
var start := _offset_of(source, int(span[0]), int(span[1]))
|
|
535
|
+
var end := _offset_of(source, int(span[2]), int(span[3]))
|
|
536
|
+
if start < 0 or end < 0 or start >= end or end > source.length():
|
|
537
|
+
continue
|
|
538
|
+
spans.append({
|
|
539
|
+
"start": start,
|
|
540
|
+
"end": end,
|
|
541
|
+
"kind": "bool",
|
|
542
|
+
"true_id": int(pair["assert_true"]),
|
|
543
|
+
"false_id": int(pair["assert_false"]),
|
|
544
|
+
})
|
|
421
545
|
if spans.is_empty():
|
|
422
546
|
return source
|
|
423
547
|
spans.sort_custom(func(a, b):
|
|
@@ -460,12 +584,21 @@ static func _wrap_spans(
|
|
|
460
584
|
j += 1
|
|
461
585
|
result += source.substr(cursor, start - cursor)
|
|
462
586
|
var operand := _wrap_spans(source, file_id, children, 0, start, end)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
587
|
+
if str(span.get("kind", "ret")) == "bool":
|
|
588
|
+
result += "%s.hit_bool(%d, %d, %d, %s)" % [
|
|
589
|
+
TRACKER_NAME,
|
|
590
|
+
file_id,
|
|
591
|
+
int(span["true_id"]),
|
|
592
|
+
int(span["false_id"]),
|
|
593
|
+
operand,
|
|
594
|
+
]
|
|
595
|
+
else:
|
|
596
|
+
result += "%s.hit_ret(%d, %d, %s)" % [
|
|
597
|
+
TRACKER_NAME,
|
|
598
|
+
file_id,
|
|
599
|
+
int(span["id"]),
|
|
600
|
+
operand,
|
|
601
|
+
]
|
|
469
602
|
cursor = end
|
|
470
603
|
i = j
|
|
471
604
|
result += source.substr(cursor, to - cursor)
|
|
@@ -562,7 +695,11 @@ func _validate_file_entry(file_entry: Variant) -> bool:
|
|
|
562
695
|
)
|
|
563
696
|
return false
|
|
564
697
|
|
|
565
|
-
|
|
698
|
+
return _validate_line_entries(file_entry["lines"])
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
func _validate_line_entries(lines: Array) -> bool:
|
|
702
|
+
for line_entry in lines:
|
|
566
703
|
if not (line_entry is Dictionary) or not line_entry.has("line") or not line_entry.has("id"):
|
|
567
704
|
_log_error(
|
|
568
705
|
"Invalid coverage plan structure.",
|
|
@@ -570,10 +707,36 @@ func _validate_file_entry(file_entry: Variant) -> bool:
|
|
|
570
707
|
"Ensure each line entry has 'line' and 'id' fields."
|
|
571
708
|
)
|
|
572
709
|
return false
|
|
710
|
+
if not _validate_branch_type(line_entry):
|
|
711
|
+
return false
|
|
573
712
|
|
|
574
713
|
return true
|
|
575
714
|
|
|
576
715
|
|
|
716
|
+
func _validate_branch_type(line_entry: Dictionary) -> bool:
|
|
717
|
+
## Reject plans carrying branch types this collector cannot measure.
|
|
718
|
+
##
|
|
719
|
+
## Loud-failure handshake: instrumenting an unknown branch type would
|
|
720
|
+
## silently mis-measure the unknown arms, so the plan is rejected
|
|
721
|
+
## instead of reporting numbers that look trustworthy but are not.
|
|
722
|
+
var branch_type: Variant = line_entry.get("branch_type")
|
|
723
|
+
if branch_type == null:
|
|
724
|
+
return true
|
|
725
|
+
var type_name := str(branch_type)
|
|
726
|
+
if type_name.is_empty() or type_name in KNOWN_BRANCH_TYPES:
|
|
727
|
+
return true
|
|
728
|
+
_log_error(
|
|
729
|
+
"Unsupported coverage plan.",
|
|
730
|
+
(
|
|
731
|
+
"Branch type '%s' (point id %s) is not implemented by this "
|
|
732
|
+
+ "collector."
|
|
733
|
+
) % [type_name, str(line_entry.get("id", "?"))],
|
|
734
|
+
"Update the gd-tools addons so the collector matches the plan "
|
|
735
|
+
+ "version."
|
|
736
|
+
)
|
|
737
|
+
return false
|
|
738
|
+
|
|
739
|
+
|
|
577
740
|
func _log_error(what: String, cause: String, fix: String) -> void:
|
|
578
741
|
push_error(
|
|
579
742
|
"[gd-tools] [Error] " + what + "\n\n" + " Cause: " + cause + "\n" + " Fix: " + fix
|