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.
Files changed (88) hide show
  1. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/PKG-INFO +57 -16
  2. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/README.md +56 -15
  3. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/pyproject.toml +2 -2
  4. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-coverage/coverage.gd +105 -1
  5. {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
  6. {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
  7. gd_tools_cli-0.7.0/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_serializer.gd +153 -0
  8. gd_tools_cli-0.7.0/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_store.gd +132 -0
  9. {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
  10. {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
  11. gd_tools_cli-0.7.0/src/gd_tools/atomic_io.py +78 -0
  12. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/clean.py +8 -3
  13. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/cli.py +262 -69
  14. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/config.py +17 -70
  15. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/diff_reporter.py +2 -2
  16. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/orchestrator.py +59 -7
  17. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/plan_generator.py +319 -9
  18. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/playtest.py +27 -14
  19. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/reporter.py +194 -16
  20. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/errors.py +2 -6
  21. gd_tools_cli-0.7.0/src/gd_tools/gh_annotations.py +36 -0
  22. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/godot.py +3 -39
  23. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/init.py +41 -28
  24. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/lint_runner.py +44 -0
  25. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/artifacts.py +2 -1
  26. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/command.py +35 -4
  27. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/orchestrator.py +7 -0
  28. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/preflight.py +3 -3
  29. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/protocol.py +7 -31
  30. gd_tools_cli-0.7.0/src/gd_tools/pre_commit.py +283 -0
  31. gd_tools_cli-0.7.0/src/gd_tools/test_results.py +353 -0
  32. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/version.py +4 -12
  33. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/session.py +9 -1
  34. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/PKG-INFO +57 -16
  35. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/SOURCES.txt +6 -3
  36. gd_tools_cli-0.6.0/src/gd_tools/addons/gd-tools-coverage/post_run_hook.gd +0 -116
  37. gd_tools_cli-0.6.0/src/gd_tools/addons/gd-tools-coverage/pre_run_hook.gd +0 -10
  38. gd_tools_cli-0.6.0/src/gd_tools/test_runner.py +0 -175
  39. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/setup.cfg +0 -0
  40. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/__init__.py +0 -0
  41. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/__main__.py +0 -0
  42. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addon_check.py +0 -0
  43. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/__init__.py +0 -0
  44. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-editor/coverage_overlay.gd +0 -0
  45. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-editor/dock.gd +0 -0
  46. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-editor/plugin.cfg +0 -0
  47. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/addons/gd-tools-editor/plugin.gd +0 -0
  48. {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
  49. {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
  50. {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
  51. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/changes.py +0 -0
  52. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/__init__.py +0 -0
  53. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/cobertura_reporter.py +0 -0
  54. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/html_reporter.py +0 -0
  55. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/lcov_reporter.py +0 -0
  56. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/omissions.py +0 -0
  57. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/templates/file.html +0 -0
  58. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/templates/index.html +0 -0
  59. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/coverage/terminal_reporter.py +0 -0
  60. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/doctor.py +0 -0
  61. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/file_discovery.py +0 -0
  62. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/format_runner.py +0 -0
  63. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/__init__.py +0 -0
  64. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/apply.py +0 -0
  65. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/gutconfig.py +0 -0
  66. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/reporter.py +0 -0
  67. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/rewrite.py +0 -0
  68. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/scan.py +0 -0
  69. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/migration/translate.py +0 -0
  70. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/__init__.py +0 -0
  71. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/bridge_scan.py +0 -0
  72. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/discovery.py +0 -0
  73. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/import_cache.py +0 -0
  74. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/native_test/preflight_cache.py +0 -0
  75. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/output.py +0 -0
  76. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/schema.py +0 -0
  77. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/update_check.py +0 -0
  78. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/verbosity.py +0 -0
  79. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/__init__.py +0 -0
  80. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/coalescer.py +0 -0
  81. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/loop.py +0 -0
  82. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/mapping.py +0 -0
  83. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/observer.py +0 -0
  84. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools/watch/scope.py +0 -0
  85. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/dependency_links.txt +0 -0
  86. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/entry_points.txt +0 -0
  87. {gd_tools_cli-0.6.0 → gd_tools_cli-0.7.0}/src/gd_tools_cli.egg-info/requires.txt +0 -0
  88. {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.6.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 Cobertura reports that integrate with CI and code review tools. `# gd-tools: no cover` annotations exclude debug-only code from the numbers. `coverage run` collects coverage during manual playtest sessions, not just automated tests. |
80
- | **CI/CD friendly** | Exit codes, `--check` flags, machine-readable output (JSON, JUnit XML, LCOV, Cobertura), no interactive prompts in CI mode. |
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 JSON output. Accepts one or more file or directory paths. |
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, and GUT when installed) in a table or JSON. |
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. Configuration
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
- ## 8. Documentation
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
- ## 9. Development
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
- ## 10. License
470
+ ## 11. License
429
471
 
430
472
  MIT
431
473
 
432
- ## 11. Acknowledgements
474
+ ## 12. Acknowledgements
433
475
 
434
- `gd-tools` stands on the shoulders of two excellent community tools. It
435
- wraps them; it does not replace them. Full credit to their authors for the
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 Cobertura reports that integrate with CI and code review tools. `# 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
- | **CI/CD friendly** | Exit codes, `--check` flags, machine-readable output (JSON, JUnit XML, LCOV, Cobertura), no interactive prompts in CI mode. |
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 JSON output. Accepts one or more file or directory paths. |
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, and GUT when installed) in a table or JSON. |
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. Configuration
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
- ## 8. Documentation
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
- ## 9. Development
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
- ## 10. License
429
+ ## 11. License
388
430
 
389
431
  MIT
390
432
 
391
- ## 11. Acknowledgements
433
+ ## 12. Acknowledgements
392
434
 
393
- `gd-tools` stands on the shoulders of two excellent community tools. It
394
- wraps them; it does not replace them. Full credit to their authors for the
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.6.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.6.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 source_lines: PackedStringArray = source.split("\n")
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(
@@ -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() and entry["action"] == "return":
81
- return entry["value"]
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 source_lines: PackedStringArray = source.split("\n")
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: