gd-tools-cli 0.6.0__tar.gz → 0.7.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.6.0 → gd_tools_cli-0.7.0}/PKG-INFO +57 -16
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/README.md +56 -15
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/pyproject.toml +2 -2
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-coverage/coverage.gd +105 -1
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-test/gd_tools_mock.gd +50 -3
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-test/gd_tools_native_coverage.gd +108 -1
- gd_tools_cli-0.7.0/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_serializer.gd +153 -0
- gd_tools_cli-0.7.0/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_store.gd +132 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test.gd +339 -3
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_runner.gd +68 -2
- gd_tools_cli-0.7.0/src/gd_tools/atomic_io.py +78 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/clean.py +8 -3
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/cli.py +262 -69
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/config.py +17 -70
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/diff_reporter.py +2 -2
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/orchestrator.py +59 -7
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/plan_generator.py +319 -9
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/playtest.py +27 -14
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/reporter.py +194 -16
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/errors.py +2 -6
- gd_tools_cli-0.7.0/src/gd_tools/gh_annotations.py +36 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/godot.py +3 -39
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/init.py +41 -28
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/lint_runner.py +44 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/artifacts.py +2 -1
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/command.py +35 -4
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/orchestrator.py +7 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/preflight.py +3 -3
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/protocol.py +7 -31
- gd_tools_cli-0.7.0/src/gd_tools/pre_commit.py +283 -0
- gd_tools_cli-0.7.0/src/gd_tools/test_results.py +353 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/version.py +4 -12
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/session.py +9 -1
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/PKG-INFO +57 -16
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/SOURCES.txt +6 -3
- gd_tools_cli-0.6.0/src/gd_tools/addons/gd-tools-coverage/post_run_hook.gd +0 -116
- gd_tools_cli-0.6.0/src/gd_tools/addons/gd-tools-coverage/pre_run_hook.gd +0 -10
- gd_tools_cli-0.6.0/src/gd_tools/test_runner.py +0 -175
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/setup.cfg +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/__init__.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/__main__.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addon_check.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/__init__.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-editor/coverage_overlay.gd +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-editor/dock.gd +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-editor/plugin.cfg +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-editor/plugin.gd +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-test/gd_tools_parameter_naming.gd +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_context.gd +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_preflight.gd +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/changes.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/__init__.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/cobertura_reporter.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/html_reporter.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/lcov_reporter.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/omissions.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/templates/file.html +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/templates/index.html +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/terminal_reporter.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/doctor.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/file_discovery.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/format_runner.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/__init__.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/apply.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/gutconfig.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/reporter.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/rewrite.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/scan.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/translate.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/__init__.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/bridge_scan.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/discovery.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/import_cache.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/preflight_cache.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/output.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/schema.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/update_check.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/verbosity.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/__init__.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/coalescer.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/loop.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/mapping.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/observer.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/scope.py +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/dependency_links.txt +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/entry_points.txt +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/requires.txt +0 -0
- {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.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.7.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
|
|
@@ -74,10 +74,11 @@ One install, one config, one mental model.
|
|
|
74
74
|
| Feature | Description |
|
|
75
75
|
|---------|-------------|
|
|
76
76
|
| **Unified workflow** | One install, one config (`gd-tools.toml`), one mental model for test, lint, format, and coverage. Consistent terminal output with colored markers and summary footers across all commands. |
|
|
77
|
-
| **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. |
|
|
77
|
+
| **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
|
+
| **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. |
|
|
78
79
|
| **Zero-friction bootstrap** | `gd-tools init` gets a project fully set up in under a minute -- native test and coverage addons deployed, configs generated. |
|
|
79
|
-
| **Coverage gap-filling** | Production-quality line and branch coverage for GDScript -- HTML, LCOV, and
|
|
80
|
-
| **CI/CD friendly** | Exit codes, `--check` flags, machine-readable output (JSON, JUnit XML, LCOV, Cobertura),
|
|
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
|
+
| **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. |
|
|
81
82
|
| **Standalone compatibility** | gdlint and gdformat continue to work if invoked directly. `gd-tools` is a layer on top, not a lock-in. |
|
|
82
83
|
|
|
83
84
|
## 3. Installation
|
|
@@ -140,13 +141,13 @@ func test_health_starts_at_full() -> void:
|
|
|
140
141
|
|---------|-------------|
|
|
141
142
|
| `gd-tools init` | Bootstrap a Godot project -- deploy the native test and coverage addons, generate configs. |
|
|
142
143
|
| `gd-tools doctor` | Diagnose the development environment -- Godot, native test addon, coverage addon, tooling; reports legacy GUT artifacts as migration advice. |
|
|
143
|
-
| `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. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
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>/`. |
|
|
144
145
|
| `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. |
|
|
145
|
-
| `gd-tools lint` | Lint GDScript files using gdlint with text or
|
|
146
|
+
| `gd-tools lint` | Lint GDScript files using gdlint with text, JSON, or GitHub Actions annotation output. Accepts one or more file or directory paths. |
|
|
146
147
|
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
|
|
147
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). |
|
|
148
149
|
| `gd-tools config` | Configuration management -- `show` (display resolved config), `validate` (check config validity), `schema` (print or write the JSON Schema for `gd-tools.toml`). |
|
|
149
|
-
| `gd-tools version` | Display versions of all gd-tools components (gd-tools, Godot, gdtoolkit, Python
|
|
150
|
+
| `gd-tools version` | Display versions of all gd-tools components (gd-tools, Godot, gdtoolkit, Python) in a table or JSON. |
|
|
150
151
|
| `gd-tools completion` | Generate shell completion scripts for bash, zsh, fish, or PowerShell. |
|
|
151
152
|
|
|
152
153
|
### Selecting and Filtering Tests
|
|
@@ -158,6 +159,7 @@ gd-tools test --test test_takes_damage # one test method
|
|
|
158
159
|
gd-tools test --tag smoke # suites with that class-level tag
|
|
159
160
|
gd-tools test --test-timeout 30 # per-test timeout, in seconds
|
|
160
161
|
gd-tools test --parallel 4 # run suites with 4 concurrent workers
|
|
162
|
+
gd-tools test --durations 10 # report the 10 slowest tests
|
|
161
163
|
gd-tools test --changed # only suites mapped from git-changed files
|
|
162
164
|
gd-tools test --changed --base main # PR/CI mode: diff from merge-base with main
|
|
163
165
|
```
|
|
@@ -168,6 +170,13 @@ worker pool (1-32 workers; bare `--parallel` means 4) while keeping results,
|
|
|
168
170
|
JUnit XML, and merged coverage identical to a sequential run. Persist the
|
|
169
171
|
choice with `parallel = 4` under `[test]` in `gd-tools.toml`.
|
|
170
172
|
|
|
173
|
+
`--durations N` prints a "Slowest Tests" table after the run, sorted
|
|
174
|
+
slowest-first, covering pass, fail, and skip outcomes. Bare `--durations`
|
|
175
|
+
means 10; `--durations 0` lists every executed test (pytest parity). Persist
|
|
176
|
+
the choice with `durations = 10` under `[test]` in `gd-tools.toml`; the flag
|
|
177
|
+
overrides the config for one invocation. The table renders identically under
|
|
178
|
+
`--watch` re-runs and `--parallel` runs.
|
|
179
|
+
|
|
171
180
|
`--changed` selects suites from git instead of running everything: uncommitted
|
|
172
181
|
changes (staged, unstaged, and untracked) map to suites by the same
|
|
173
182
|
file→suite convention as `--watch` — `src/enemy.gd` selects
|
|
@@ -319,7 +328,41 @@ eval "$(gd-tools)"
|
|
|
319
328
|
See the [User Guide](./docs/USER_GUIDE.md) for detailed per-shell
|
|
320
329
|
instructions.
|
|
321
330
|
|
|
322
|
-
## 7.
|
|
331
|
+
## 7. Pre-commit Hooks
|
|
332
|
+
|
|
333
|
+
Wire gd-tools into the [pre-commit](https://pre-commit.com) framework so
|
|
334
|
+
every commit runs your format check, lint, and (optionally) your test suite:
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
gd-tools install-hooks
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The command generates `.pre-commit-hooks.yaml` and merges a `repos: local:`
|
|
341
|
+
entry into `.pre-commit-config.yaml`. Existing hooks — including your own —
|
|
342
|
+
are preserved; re-runs update only the gd-tools entries in place.
|
|
343
|
+
|
|
344
|
+
Interactive by default: confirm each hook (`format`, `lint`, `test`). For
|
|
345
|
+
scripted or CI use, pass flags instead:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
gd-tools install-hooks --non-interactive # format + lint (defaults)
|
|
349
|
+
gd-tools install-hooks --all # format + lint + test
|
|
350
|
+
gd-tools install-hooks --hooks format,lint # explicit selection
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
| Flag | Behavior |
|
|
354
|
+
|---|---|
|
|
355
|
+
| `--all` | Enable every hook (format, lint, test). |
|
|
356
|
+
| `--hooks HOOKS` | Comma-separated selection, e.g. `--hooks format,lint`. |
|
|
357
|
+
| `--non-interactive` | Never prompt; use defaults or explicit flags. |
|
|
358
|
+
|
|
359
|
+
Exit codes follow the gd-tools convention: `0` installed, `1` nothing to do,
|
|
360
|
+
`2` config/environment error.
|
|
361
|
+
|
|
362
|
+
See the [User Guide](./docs/USER_GUIDE.md#8-pre-commit-hooks) for the
|
|
363
|
+
generated file contents and per-hook details.
|
|
364
|
+
|
|
365
|
+
## 8. Configuration
|
|
323
366
|
|
|
324
367
|
`gd-tools` uses a single `gd-tools.toml` file as the source of truth for
|
|
325
368
|
all tool configuration. `gd-tools init` generates this file with sensible
|
|
@@ -333,7 +376,6 @@ binary = "" # Optional -- auto-detected if unset
|
|
|
333
376
|
test_dirs = ["test", "tests"]
|
|
334
377
|
prefix = "test_"
|
|
335
378
|
suffix = ".gd"
|
|
336
|
-
gutconfig = ".gutconfig.json" # legacy; not read by any runtime
|
|
337
379
|
runtime = "native" # native (default); "gut" is no longer a runnable runtime
|
|
338
380
|
timeout_seconds = 5.0 # default per-test timeout for async native tests
|
|
339
381
|
retries = 0 # opt-in retries per native test
|
|
@@ -384,7 +426,7 @@ version's config model.
|
|
|
384
426
|
See the [User Guide](./docs/USER_GUIDE.md) for a full configuration reference
|
|
385
427
|
with all keys, defaults, and examples.
|
|
386
428
|
|
|
387
|
-
##
|
|
429
|
+
## 9. Documentation
|
|
388
430
|
|
|
389
431
|
| Document | Description |
|
|
390
432
|
|----------|-------------|
|
|
@@ -395,7 +437,7 @@ with all keys, defaults, and examples.
|
|
|
395
437
|
| [Roadmap](./docs/ROADMAP.md) | Release phases and milestones. |
|
|
396
438
|
| [Testing Strategy](./docs/TESTING_STRATEGY.md) | Test pyramid, coverage targets, and CI integration. |
|
|
397
439
|
|
|
398
|
-
##
|
|
440
|
+
## 10. Development
|
|
399
441
|
|
|
400
442
|
```bash
|
|
401
443
|
# Clone and install in editable mode with dev dependencies
|
|
@@ -425,15 +467,14 @@ See [Testing Strategy](./docs/TESTING_STRATEGY.md) for the full testing
|
|
|
425
467
|
guide and [Contributing Guide](./docs/CONTRIBUTING.md) for development
|
|
426
468
|
setup details.
|
|
427
469
|
|
|
428
|
-
##
|
|
470
|
+
## 11. License
|
|
429
471
|
|
|
430
472
|
MIT
|
|
431
473
|
|
|
432
|
-
##
|
|
474
|
+
## 12. Acknowledgements
|
|
433
475
|
|
|
434
|
-
`gd-tools` stands on the shoulders of
|
|
435
|
-
|
|
436
|
-
hard parts.
|
|
476
|
+
`gd-tools` stands on the shoulders of excellent community tools, past and
|
|
477
|
+
present. Full credit to their authors for the hard parts.
|
|
437
478
|
|
|
438
479
|
- **[GUT (Godot Unit Test)](https://github.com/bitwes/Gut)** by
|
|
439
480
|
[bitwes](https://github.com/bitwes): the GDScript test framework whose
|
|
@@ -33,10 +33,11 @@ One install, one config, one mental model.
|
|
|
33
33
|
| Feature | Description |
|
|
34
34
|
|---------|-------------|
|
|
35
35
|
| **Unified workflow** | One install, one config (`gd-tools.toml`), one mental model for test, lint, format, and coverage. Consistent terminal output with colored markers and summary footers across all commands. |
|
|
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. |
|
|
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
|
+
| **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. |
|
|
37
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. |
|
|
38
|
-
| **Coverage gap-filling** | Production-quality line and branch coverage for GDScript -- HTML, LCOV, and
|
|
39
|
-
| **CI/CD friendly** | Exit codes, `--check` flags, machine-readable output (JSON, JUnit XML, LCOV, Cobertura),
|
|
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. |
|
|
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. |
|
|
40
41
|
| **Standalone compatibility** | gdlint and gdformat continue to work if invoked directly. `gd-tools` is a layer on top, not a lock-in. |
|
|
41
42
|
|
|
42
43
|
## 3. Installation
|
|
@@ -99,13 +100,13 @@ func test_health_starts_at_full() -> void:
|
|
|
99
100
|
|---------|-------------|
|
|
100
101
|
| `gd-tools init` | Bootstrap a Godot project -- deploy the native test and coverage addons, generate configs. |
|
|
101
102
|
| `gd-tools doctor` | Diagnose the development environment -- Godot, native test addon, coverage addon, tooling; reports legacy GUT artifacts as migration advice. |
|
|
102
|
-
| `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. 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. Every run publishes a machine-readable artifact index under `.gd-tools/artifacts/<run_id>/`. |
|
|
103
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. |
|
|
104
|
-
| `gd-tools lint` | Lint GDScript files using gdlint with text or
|
|
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. |
|
|
105
106
|
| `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
|
|
106
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
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`). |
|
|
108
|
-
| `gd-tools version` | Display versions of all gd-tools components (gd-tools, Godot, gdtoolkit, Python
|
|
109
|
+
| `gd-tools version` | Display versions of all gd-tools components (gd-tools, Godot, gdtoolkit, Python) in a table or JSON. |
|
|
109
110
|
| `gd-tools completion` | Generate shell completion scripts for bash, zsh, fish, or PowerShell. |
|
|
110
111
|
|
|
111
112
|
### Selecting and Filtering Tests
|
|
@@ -117,6 +118,7 @@ gd-tools test --test test_takes_damage # one test method
|
|
|
117
118
|
gd-tools test --tag smoke # suites with that class-level tag
|
|
118
119
|
gd-tools test --test-timeout 30 # per-test timeout, in seconds
|
|
119
120
|
gd-tools test --parallel 4 # run suites with 4 concurrent workers
|
|
121
|
+
gd-tools test --durations 10 # report the 10 slowest tests
|
|
120
122
|
gd-tools test --changed # only suites mapped from git-changed files
|
|
121
123
|
gd-tools test --changed --base main # PR/CI mode: diff from merge-base with main
|
|
122
124
|
```
|
|
@@ -127,6 +129,13 @@ worker pool (1-32 workers; bare `--parallel` means 4) while keeping results,
|
|
|
127
129
|
JUnit XML, and merged coverage identical to a sequential run. Persist the
|
|
128
130
|
choice with `parallel = 4` under `[test]` in `gd-tools.toml`.
|
|
129
131
|
|
|
132
|
+
`--durations N` prints a "Slowest Tests" table after the run, sorted
|
|
133
|
+
slowest-first, covering pass, fail, and skip outcomes. Bare `--durations`
|
|
134
|
+
means 10; `--durations 0` lists every executed test (pytest parity). Persist
|
|
135
|
+
the choice with `durations = 10` under `[test]` in `gd-tools.toml`; the flag
|
|
136
|
+
overrides the config for one invocation. The table renders identically under
|
|
137
|
+
`--watch` re-runs and `--parallel` runs.
|
|
138
|
+
|
|
130
139
|
`--changed` selects suites from git instead of running everything: uncommitted
|
|
131
140
|
changes (staged, unstaged, and untracked) map to suites by the same
|
|
132
141
|
file→suite convention as `--watch` — `src/enemy.gd` selects
|
|
@@ -278,7 +287,41 @@ eval "$(gd-tools)"
|
|
|
278
287
|
See the [User Guide](./docs/USER_GUIDE.md) for detailed per-shell
|
|
279
288
|
instructions.
|
|
280
289
|
|
|
281
|
-
## 7.
|
|
290
|
+
## 7. Pre-commit Hooks
|
|
291
|
+
|
|
292
|
+
Wire gd-tools into the [pre-commit](https://pre-commit.com) framework so
|
|
293
|
+
every commit runs your format check, lint, and (optionally) your test suite:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
gd-tools install-hooks
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The command generates `.pre-commit-hooks.yaml` and merges a `repos: local:`
|
|
300
|
+
entry into `.pre-commit-config.yaml`. Existing hooks — including your own —
|
|
301
|
+
are preserved; re-runs update only the gd-tools entries in place.
|
|
302
|
+
|
|
303
|
+
Interactive by default: confirm each hook (`format`, `lint`, `test`). For
|
|
304
|
+
scripted or CI use, pass flags instead:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
gd-tools install-hooks --non-interactive # format + lint (defaults)
|
|
308
|
+
gd-tools install-hooks --all # format + lint + test
|
|
309
|
+
gd-tools install-hooks --hooks format,lint # explicit selection
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
| Flag | Behavior |
|
|
313
|
+
|---|---|
|
|
314
|
+
| `--all` | Enable every hook (format, lint, test). |
|
|
315
|
+
| `--hooks HOOKS` | Comma-separated selection, e.g. `--hooks format,lint`. |
|
|
316
|
+
| `--non-interactive` | Never prompt; use defaults or explicit flags. |
|
|
317
|
+
|
|
318
|
+
Exit codes follow the gd-tools convention: `0` installed, `1` nothing to do,
|
|
319
|
+
`2` config/environment error.
|
|
320
|
+
|
|
321
|
+
See the [User Guide](./docs/USER_GUIDE.md#8-pre-commit-hooks) for the
|
|
322
|
+
generated file contents and per-hook details.
|
|
323
|
+
|
|
324
|
+
## 8. Configuration
|
|
282
325
|
|
|
283
326
|
`gd-tools` uses a single `gd-tools.toml` file as the source of truth for
|
|
284
327
|
all tool configuration. `gd-tools init` generates this file with sensible
|
|
@@ -292,7 +335,6 @@ binary = "" # Optional -- auto-detected if unset
|
|
|
292
335
|
test_dirs = ["test", "tests"]
|
|
293
336
|
prefix = "test_"
|
|
294
337
|
suffix = ".gd"
|
|
295
|
-
gutconfig = ".gutconfig.json" # legacy; not read by any runtime
|
|
296
338
|
runtime = "native" # native (default); "gut" is no longer a runnable runtime
|
|
297
339
|
timeout_seconds = 5.0 # default per-test timeout for async native tests
|
|
298
340
|
retries = 0 # opt-in retries per native test
|
|
@@ -343,7 +385,7 @@ version's config model.
|
|
|
343
385
|
See the [User Guide](./docs/USER_GUIDE.md) for a full configuration reference
|
|
344
386
|
with all keys, defaults, and examples.
|
|
345
387
|
|
|
346
|
-
##
|
|
388
|
+
## 9. Documentation
|
|
347
389
|
|
|
348
390
|
| Document | Description |
|
|
349
391
|
|----------|-------------|
|
|
@@ -354,7 +396,7 @@ with all keys, defaults, and examples.
|
|
|
354
396
|
| [Roadmap](./docs/ROADMAP.md) | Release phases and milestones. |
|
|
355
397
|
| [Testing Strategy](./docs/TESTING_STRATEGY.md) | Test pyramid, coverage targets, and CI integration. |
|
|
356
398
|
|
|
357
|
-
##
|
|
399
|
+
## 10. Development
|
|
358
400
|
|
|
359
401
|
```bash
|
|
360
402
|
# Clone and install in editable mode with dev dependencies
|
|
@@ -384,15 +426,14 @@ See [Testing Strategy](./docs/TESTING_STRATEGY.md) for the full testing
|
|
|
384
426
|
guide and [Contributing Guide](./docs/CONTRIBUTING.md) for development
|
|
385
427
|
setup details.
|
|
386
428
|
|
|
387
|
-
##
|
|
429
|
+
## 11. License
|
|
388
430
|
|
|
389
431
|
MIT
|
|
390
432
|
|
|
391
|
-
##
|
|
433
|
+
## 12. Acknowledgements
|
|
392
434
|
|
|
393
|
-
`gd-tools` stands on the shoulders of
|
|
394
|
-
|
|
395
|
-
hard parts.
|
|
435
|
+
`gd-tools` stands on the shoulders of excellent community tools, past and
|
|
436
|
+
present. Full credit to their authors for the hard parts.
|
|
396
437
|
|
|
397
438
|
- **[GUT (Godot Unit Test)](https://github.com/bitwes/Gut)** by
|
|
398
439
|
[bitwes](https://github.com/bitwes): the GDScript test framework whose
|
|
@@ -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.7.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"}
|
|
@@ -105,7 +105,7 @@ target-version = ["py310"]
|
|
|
105
105
|
|
|
106
106
|
[tool.commitizen]
|
|
107
107
|
name = "cz_conventional_commits"
|
|
108
|
-
version = "0.
|
|
108
|
+
version = "0.7.0"
|
|
109
109
|
version_files = ["pyproject.toml:version"]
|
|
110
110
|
changelog_file = "CHANGELOG.md"
|
|
111
111
|
tag_format = "v$version"
|
|
@@ -80,6 +80,15 @@ func hit(file_id: int, line_id: int) -> void:
|
|
|
80
80
|
_hits[file_id][line_id] += 1
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
func hit_ret(file_id: int, line_id: int, value: Variant) -> Variant:
|
|
84
|
+
## Record one instrumented hit and pass the tracked value through.
|
|
85
|
+
##
|
|
86
|
+
## Injected around ternary operands so each arm is measured exactly
|
|
87
|
+
## when it evaluates, without re-evaluating the condition.
|
|
88
|
+
hit(file_id, line_id)
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
|
|
83
92
|
func get_hits() -> Dictionary:
|
|
84
93
|
return _hits
|
|
85
94
|
|
|
@@ -336,10 +345,17 @@ static func _detect_body_indent(source_lines: PackedStringArray, pattern_index:
|
|
|
336
345
|
|
|
337
346
|
|
|
338
347
|
static func _inject_trackers(source: String, file_id: int, lines: Array) -> String:
|
|
339
|
-
var
|
|
348
|
+
var wrapped := _wrap_ternary_operands(source, file_id, lines)
|
|
349
|
+
var source_lines: PackedStringArray = wrapped.split("\n")
|
|
340
350
|
var sorted_lines: Array = lines.duplicate(true)
|
|
341
351
|
sorted_lines.sort_custom(func(a, b): return int(a["line"]) > int(b["line"]))
|
|
342
352
|
for entry in sorted_lines:
|
|
353
|
+
# Ternary arms are instrumented by wrapping their operand text
|
|
354
|
+
# (see _wrap_ternary_operands); a line-inserted hit() on the
|
|
355
|
+
# shared anchor line fires both arms in lockstep and can never
|
|
356
|
+
# report an uncovered arm.
|
|
357
|
+
if entry.get("operand_span") != null:
|
|
358
|
+
continue
|
|
343
359
|
var target_line_num: int = int(entry["line"])
|
|
344
360
|
var line_id: int = int(entry["id"])
|
|
345
361
|
var target_index: int = target_line_num - 1
|
|
@@ -368,6 +384,94 @@ static func _inject_trackers(source: String, file_id: int, lines: Array) -> Stri
|
|
|
368
384
|
return "\n".join(source_lines)
|
|
369
385
|
|
|
370
386
|
|
|
387
|
+
static func _offset_of(source: String, line: int, col: int) -> int:
|
|
388
|
+
## Absolute character offset of a 1-based line/column position, or -1.
|
|
389
|
+
var source_lines := source.split("\n")
|
|
390
|
+
if line < 1 or line > source_lines.size() or col < 1:
|
|
391
|
+
return -1
|
|
392
|
+
var offset := 0
|
|
393
|
+
for i in range(line - 1):
|
|
394
|
+
offset += source_lines[i].length() + 1
|
|
395
|
+
var target := offset + (col - 1)
|
|
396
|
+
if target > source.length():
|
|
397
|
+
return -1
|
|
398
|
+
return target
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
static func _wrap_ternary_operands(source: String, file_id: int, lines: Array) -> String:
|
|
402
|
+
## Replace each ternary operand with a value-preserving tracker call.
|
|
403
|
+
##
|
|
404
|
+
## The plan records the source span of every ternary arm operand.
|
|
405
|
+
## Wrapping the operand records the arm's hit exactly when that arm
|
|
406
|
+
## evaluates, keeping values, evaluation order, and single evaluation
|
|
407
|
+
## intact. Line numbers are unchanged: a wrapper never adds or removes
|
|
408
|
+
## a newline, so the line-based insertion that follows stays valid.
|
|
409
|
+
var spans: Array = []
|
|
410
|
+
for entry in lines:
|
|
411
|
+
var span: Variant = entry.get("operand_span")
|
|
412
|
+
if span == null:
|
|
413
|
+
continue
|
|
414
|
+
var start := _offset_of(source, int(span[0]), int(span[1]))
|
|
415
|
+
var end := _offset_of(source, int(span[2]), int(span[3]))
|
|
416
|
+
if start < 0 or end < 0 or start >= end or end > source.length():
|
|
417
|
+
# Fail open: an unusable span leaves the arm uninstrumented
|
|
418
|
+
# rather than corrupting the source.
|
|
419
|
+
continue
|
|
420
|
+
spans.append({"start": start, "end": end, "id": int(entry["id"])})
|
|
421
|
+
if spans.is_empty():
|
|
422
|
+
return source
|
|
423
|
+
spans.sort_custom(func(a, b):
|
|
424
|
+
if int(a["start"]) != int(b["start"]):
|
|
425
|
+
return int(a["start"]) < int(b["start"])
|
|
426
|
+
return int(a["end"]) > int(b["end"])
|
|
427
|
+
)
|
|
428
|
+
return _wrap_spans(source, file_id, spans, 0, 0, source.length())
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
static func _wrap_spans(
|
|
432
|
+
source: String,
|
|
433
|
+
file_id: int,
|
|
434
|
+
spans: Array,
|
|
435
|
+
index: int,
|
|
436
|
+
from: int,
|
|
437
|
+
to: int
|
|
438
|
+
) -> String:
|
|
439
|
+
## Emit source[from:to] with spans[index..] wrapped in place.
|
|
440
|
+
##
|
|
441
|
+
## Spans are sorted ascending by start (outer span before inner on a
|
|
442
|
+
## tie), so a span's children are the following entries fully contained
|
|
443
|
+
## in it; they are wrapped recursively inside the operand text.
|
|
444
|
+
var result := ""
|
|
445
|
+
var cursor := from
|
|
446
|
+
var i := index
|
|
447
|
+
while i < spans.size():
|
|
448
|
+
var span: Dictionary = spans[i]
|
|
449
|
+
var start := int(span["start"])
|
|
450
|
+
var end := int(span["end"])
|
|
451
|
+
if start >= to or end > to:
|
|
452
|
+
break
|
|
453
|
+
if start < cursor:
|
|
454
|
+
i += 1
|
|
455
|
+
continue
|
|
456
|
+
var children: Array = []
|
|
457
|
+
var j := i + 1
|
|
458
|
+
while j < spans.size() and int(spans[j]["end"]) <= end:
|
|
459
|
+
children.append(spans[j])
|
|
460
|
+
j += 1
|
|
461
|
+
result += source.substr(cursor, start - cursor)
|
|
462
|
+
var operand := _wrap_spans(source, file_id, children, 0, start, end)
|
|
463
|
+
result += "%s.hit_ret(%d, %d, %s)" % [
|
|
464
|
+
TRACKER_NAME,
|
|
465
|
+
file_id,
|
|
466
|
+
int(span["id"]),
|
|
467
|
+
operand,
|
|
468
|
+
]
|
|
469
|
+
cursor = end
|
|
470
|
+
i = j
|
|
471
|
+
result += source.substr(cursor, to - cursor)
|
|
472
|
+
return result
|
|
473
|
+
|
|
474
|
+
|
|
371
475
|
func _load_plan(path: String) -> Dictionary:
|
|
372
476
|
if not FileAccess.file_exists(path):
|
|
373
477
|
_log_error(
|
{gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-test/gd_tools_mock.gd
RENAMED
|
@@ -71,14 +71,35 @@ func calls_super(method: String, args: Array) -> bool:
|
|
|
71
71
|
##
|
|
72
72
|
## Unstubbed doubles answer null (GUT semantics); the [method coerce] layer
|
|
73
73
|
## adapts that to typed returns. A matched stub registered with
|
|
74
|
-
## `to_return()` supplies its value here
|
|
74
|
+
## `to_return()` supplies its value here; `to_return_seq()` answers with its
|
|
75
|
+
## values in order and repeats the final one once exhausted; `to_fail()`
|
|
76
|
+
## records a failure through the suite's normal failure path at call time
|
|
77
|
+
## and answers null, which [method coerce] adapts to the return type's zero
|
|
78
|
+
## value so the script under test keeps executing.
|
|
75
79
|
func respond(method: String, args: Array) -> Variant:
|
|
76
80
|
if _suite != null:
|
|
77
81
|
var entry: Dictionary = _suite._gd_tools_stub_find(
|
|
78
82
|
_target_id, method, args
|
|
79
83
|
)
|
|
80
|
-
if not entry.is_empty()
|
|
81
|
-
|
|
84
|
+
if not entry.is_empty():
|
|
85
|
+
if entry["action"] == "return":
|
|
86
|
+
return entry["value"]
|
|
87
|
+
if entry["action"] == "return_seq":
|
|
88
|
+
var values: Array = entry["value"]
|
|
89
|
+
if values.is_empty():
|
|
90
|
+
return null
|
|
91
|
+
var position := int(entry.get("position", 0))
|
|
92
|
+
var answer: Variant = (
|
|
93
|
+
values[position] if position < values.size() else values.back()
|
|
94
|
+
)
|
|
95
|
+
entry["position"] = position + 1
|
|
96
|
+
return answer
|
|
97
|
+
if entry["action"] == "fail":
|
|
98
|
+
_suite._gd_tools_record_failure(
|
|
99
|
+
"stub",
|
|
100
|
+
'Stub for "%s" recorded a failure: %s' % [method, entry["value"]]
|
|
101
|
+
)
|
|
102
|
+
return null
|
|
82
103
|
return null
|
|
83
104
|
|
|
84
105
|
|
|
@@ -214,6 +235,32 @@ class StubBuilder:
|
|
|
214
235
|
)
|
|
215
236
|
return self
|
|
216
237
|
|
|
238
|
+
## Answer successive matching calls with the given values in order.
|
|
239
|
+
##
|
|
240
|
+
## Once the sequence is exhausted, its final value repeats so late calls
|
|
241
|
+
## never hand null to the script under test.
|
|
242
|
+
func to_return_seq(values: Array) -> StubBuilder:
|
|
243
|
+
if _disabled:
|
|
244
|
+
return self
|
|
245
|
+
_suite._gd_tools_stub_register(
|
|
246
|
+
_double_id, _method, _args, "return_seq", values.duplicate()
|
|
247
|
+
)
|
|
248
|
+
return self
|
|
249
|
+
|
|
250
|
+
## Record a test failure through the normal failure path every time a
|
|
251
|
+
## matching call happens.
|
|
252
|
+
##
|
|
253
|
+
## GDScript has no exceptions: the call still answers (the return type's
|
|
254
|
+
## zero value after [method coerce]) so the script under test keeps
|
|
255
|
+
## executing and later calls still reach the recorder.
|
|
256
|
+
func to_fail(message: String) -> StubBuilder:
|
|
257
|
+
if _disabled:
|
|
258
|
+
return self
|
|
259
|
+
_suite._gd_tools_stub_register(
|
|
260
|
+
_double_id, _method, _args, "fail", message
|
|
261
|
+
)
|
|
262
|
+
return self
|
|
263
|
+
|
|
217
264
|
## Forward every matching call to the real implementation.
|
|
218
265
|
func to_call_super() -> StubBuilder:
|
|
219
266
|
if _disabled:
|
|
@@ -57,6 +57,15 @@ static func hit(file_id: int, line_id: int) -> void:
|
|
|
57
57
|
_hits[file_id][line_id] += 1
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
static func hit_ret(file_id: int, line_id: int, value: Variant) -> Variant:
|
|
61
|
+
## Record one instrumented hit and pass the tracked value through.
|
|
62
|
+
##
|
|
63
|
+
## Injected around ternary operands so each arm is measured exactly
|
|
64
|
+
## when it evaluates, without re-evaluating the condition.
|
|
65
|
+
hit(file_id, line_id)
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
60
69
|
static func write() -> bool:
|
|
61
70
|
## Write collected hits using the existing coverage JSON schema.
|
|
62
71
|
if not _active or _output_path.is_empty():
|
|
@@ -178,10 +187,17 @@ static func _inject_trackers(
|
|
|
178
187
|
file_id: int,
|
|
179
188
|
lines: Array
|
|
180
189
|
) -> String:
|
|
181
|
-
var
|
|
190
|
+
var wrapped := _wrap_ternary_operands(source, file_id, lines)
|
|
191
|
+
var source_lines: PackedStringArray = wrapped.split("\n")
|
|
182
192
|
var entries: Array = lines.duplicate(true)
|
|
183
193
|
entries.sort_custom(func(a, b): return int(a["line"]) > int(b["line"]))
|
|
184
194
|
for entry in entries:
|
|
195
|
+
# Ternary arms are instrumented by wrapping their operand text
|
|
196
|
+
# (see _wrap_ternary_operands); a line-inserted hit() on the
|
|
197
|
+
# shared anchor line fires both arms in lockstep and can never
|
|
198
|
+
# report an uncovered arm.
|
|
199
|
+
if entry.get("operand_span") != null:
|
|
200
|
+
continue
|
|
185
201
|
var target_index := int(entry["line"]) - 1
|
|
186
202
|
if target_index < 0 or target_index >= source_lines.size():
|
|
187
203
|
continue
|
|
@@ -201,6 +217,97 @@ static func _inject_trackers(
|
|
|
201
217
|
return "\n".join(source_lines)
|
|
202
218
|
|
|
203
219
|
|
|
220
|
+
static func _offset_of(source: String, line: int, col: int) -> int:
|
|
221
|
+
## Absolute character offset of a 1-based line/column position, or -1.
|
|
222
|
+
var source_lines := source.split("\n")
|
|
223
|
+
if line < 1 or line > source_lines.size() or col < 1:
|
|
224
|
+
return -1
|
|
225
|
+
var offset := 0
|
|
226
|
+
for i in range(line - 1):
|
|
227
|
+
offset += source_lines[i].length() + 1
|
|
228
|
+
var target := offset + (col - 1)
|
|
229
|
+
if target > source.length():
|
|
230
|
+
return -1
|
|
231
|
+
return target
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
static func _wrap_ternary_operands(
|
|
235
|
+
source: String,
|
|
236
|
+
file_id: int,
|
|
237
|
+
lines: Array
|
|
238
|
+
) -> String:
|
|
239
|
+
## Replace each ternary operand with a value-preserving tracker call.
|
|
240
|
+
##
|
|
241
|
+
## The plan records the source span of every ternary arm operand.
|
|
242
|
+
## Wrapping the operand records the arm's hit exactly when that arm
|
|
243
|
+
## evaluates, keeping values, evaluation order, and single evaluation
|
|
244
|
+
## intact. Line numbers are unchanged: a wrapper never adds or removes
|
|
245
|
+
## a newline, so the line-based insertion that follows stays valid.
|
|
246
|
+
var spans: Array = []
|
|
247
|
+
for entry in lines:
|
|
248
|
+
var span: Variant = entry.get("operand_span")
|
|
249
|
+
if span == null:
|
|
250
|
+
continue
|
|
251
|
+
var start := _offset_of(source, int(span[0]), int(span[1]))
|
|
252
|
+
var end := _offset_of(source, int(span[2]), int(span[3]))
|
|
253
|
+
if start < 0 or end < 0 or start >= end or end > source.length():
|
|
254
|
+
# Fail open: an unusable span leaves the arm uninstrumented
|
|
255
|
+
# rather than corrupting the source.
|
|
256
|
+
continue
|
|
257
|
+
spans.append({"start": start, "end": end, "id": int(entry["id"])})
|
|
258
|
+
if spans.is_empty():
|
|
259
|
+
return source
|
|
260
|
+
spans.sort_custom(func(a, b):
|
|
261
|
+
if int(a["start"]) != int(b["start"]):
|
|
262
|
+
return int(a["start"]) < int(b["start"])
|
|
263
|
+
return int(a["end"]) > int(b["end"])
|
|
264
|
+
)
|
|
265
|
+
return _wrap_spans(source, file_id, spans, 0, 0, source.length())
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
static func _wrap_spans(
|
|
269
|
+
source: String,
|
|
270
|
+
file_id: int,
|
|
271
|
+
spans: Array,
|
|
272
|
+
index: int,
|
|
273
|
+
from: int,
|
|
274
|
+
to: int
|
|
275
|
+
) -> String:
|
|
276
|
+
## Emit source[from:to] with spans[index..] wrapped in place.
|
|
277
|
+
##
|
|
278
|
+
## Spans are sorted ascending by start (outer span before inner on a
|
|
279
|
+
## tie), so a span's children are the following entries fully contained
|
|
280
|
+
## in it; they are wrapped recursively inside the operand text.
|
|
281
|
+
var result := ""
|
|
282
|
+
var cursor := from
|
|
283
|
+
var i := index
|
|
284
|
+
while i < spans.size():
|
|
285
|
+
var span: Dictionary = spans[i]
|
|
286
|
+
var start := int(span["start"])
|
|
287
|
+
var end := int(span["end"])
|
|
288
|
+
if start >= to or end > to:
|
|
289
|
+
break
|
|
290
|
+
if start < cursor:
|
|
291
|
+
i += 1
|
|
292
|
+
continue
|
|
293
|
+
var children: Array = []
|
|
294
|
+
var j := i + 1
|
|
295
|
+
while j < spans.size() and int(spans[j]["end"]) <= end:
|
|
296
|
+
children.append(spans[j])
|
|
297
|
+
j += 1
|
|
298
|
+
result += source.substr(cursor, start - cursor)
|
|
299
|
+
var operand := _wrap_spans(source, file_id, children, 0, start, end)
|
|
300
|
+
result += "GdToolsNativeCoverage.hit_ret(%d, %d, %s)" % [
|
|
301
|
+
file_id,
|
|
302
|
+
int(span["id"]),
|
|
303
|
+
operand,
|
|
304
|
+
]
|
|
305
|
+
cursor = end
|
|
306
|
+
i = j
|
|
307
|
+
result += source.substr(cursor, to - cursor)
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
|
|
204
311
|
static func _extract_indent(line: String) -> String:
|
|
205
312
|
var indent := ""
|
|
206
313
|
for character in line:
|