gd-tools-cli 0.8.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.
Files changed (103) hide show
  1. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/PKG-INFO +26 -2
  2. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/README.md +25 -1
  3. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/pyproject.toml +2 -2
  4. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_native_coverage.gd +110 -2
  5. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/atomic_io.py +35 -0
  6. gd_tools_cli-0.9.0/src/gd_tools/cli.py +219 -0
  7. gd_tools_cli-0.9.0/src/gd_tools/commands/__init__.py +6 -0
  8. gd_tools_cli-0.9.0/src/gd_tools/commands/clean.py +171 -0
  9. gd_tools_cli-0.9.0/src/gd_tools/commands/completion.py +61 -0
  10. gd_tools_cli-0.9.0/src/gd_tools/commands/config.py +255 -0
  11. gd_tools_cli-0.9.0/src/gd_tools/commands/coverage.py +378 -0
  12. gd_tools_cli-0.9.0/src/gd_tools/commands/doctor.py +24 -0
  13. gd_tools_cli-0.9.0/src/gd_tools/commands/format.py +73 -0
  14. gd_tools_cli-0.9.0/src/gd_tools/commands/init.py +25 -0
  15. gd_tools_cli-0.9.0/src/gd_tools/commands/install_hooks.py +118 -0
  16. gd_tools_cli-0.9.0/src/gd_tools/commands/lint.py +64 -0
  17. gd_tools_cli-0.9.0/src/gd_tools/commands/migrate.py +137 -0
  18. gd_tools_cli-0.9.0/src/gd_tools/commands/test.py +359 -0
  19. gd_tools_cli-0.9.0/src/gd_tools/commands/test_validation.py +210 -0
  20. gd_tools_cli-0.9.0/src/gd_tools/commands/version.py +36 -0
  21. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/config.py +6 -0
  22. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/plan_generator.py +28 -3
  23. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/artifacts.py +30 -30
  24. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/command.py +78 -2
  25. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/orchestrator.py +25 -20
  26. gd_tools_cli-0.9.0/src/gd_tools/native_test/ordering.py +110 -0
  27. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/protocol.py +2 -7
  28. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/output.py +14 -0
  29. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/session.py +16 -0
  30. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/PKG-INFO +26 -2
  31. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/SOURCES.txt +15 -0
  32. gd_tools_cli-0.8.0/src/gd_tools/cli.py +0 -1790
  33. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/setup.cfg +0 -0
  34. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/__init__.py +0 -0
  35. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/__main__.py +0 -0
  36. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addon_check.py +0 -0
  37. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/__init__.py +0 -0
  38. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-coverage/coverage.gd +0 -0
  39. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/coverage_overlay.gd +0 -0
  40. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/dock.gd +0 -0
  41. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/plugin.cfg +0 -0
  42. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/plugin.gd +0 -0
  43. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_mock.gd +0 -0
  44. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_parameter_naming.gd +0 -0
  45. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_serializer.gd +0 -0
  46. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_snapshot_store.gd +0 -0
  47. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test.gd +0 -0
  48. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_context.gd +0 -0
  49. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_preflight.gd +0 -0
  50. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-test/gd_tools_test_runner.gd +0 -0
  51. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/changes.py +0 -0
  52. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/clean.py +0 -0
  53. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/__init__.py +0 -0
  54. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/cobertura_reporter.py +0 -0
  55. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/diff_reporter.py +0 -0
  56. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/html_reporter.py +0 -0
  57. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/lcov_reporter.py +0 -0
  58. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/omissions.py +0 -0
  59. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/orchestrator.py +0 -0
  60. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/patch.py +0 -0
  61. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/playtest.py +0 -0
  62. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/reporter.py +0 -0
  63. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/templates/file.html +0 -0
  64. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/templates/index.html +0 -0
  65. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/terminal_reporter.py +0 -0
  66. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/doctor.py +0 -0
  67. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/errors.py +0 -0
  68. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/file_discovery.py +0 -0
  69. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/format_runner.py +0 -0
  70. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/gh_annotations.py +0 -0
  71. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/godot.py +0 -0
  72. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/init.py +0 -0
  73. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/lint_runner.py +0 -0
  74. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/__init__.py +0 -0
  75. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/apply.py +0 -0
  76. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/gutconfig.py +0 -0
  77. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/reporter.py +0 -0
  78. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/rewrite.py +0 -0
  79. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/scan.py +0 -0
  80. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/translate.py +0 -0
  81. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/__init__.py +0 -0
  82. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/bridge_scan.py +0 -0
  83. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/discovery.py +0 -0
  84. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/flaky.py +0 -0
  85. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/import_cache.py +0 -0
  86. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/preflight.py +0 -0
  87. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/preflight_cache.py +0 -0
  88. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/pre_commit.py +0 -0
  89. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/schema.py +0 -0
  90. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/test_results.py +0 -0
  91. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/update_check.py +0 -0
  92. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/verbosity.py +0 -0
  93. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/version.py +0 -0
  94. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/__init__.py +0 -0
  95. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/coalescer.py +0 -0
  96. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/loop.py +0 -0
  97. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/mapping.py +0 -0
  98. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/observer.py +0 -0
  99. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/scope.py +0 -0
  100. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/dependency_links.txt +0 -0
  101. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/entry_points.txt +0 -0
  102. {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/requires.txt +0 -0
  103. {gd_tools_cli-0.8.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.8.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
@@ -142,7 +142,7 @@ func test_health_starts_at_full() -> void:
142
142
  |---------|-------------|
143
143
  | `gd-tools init` | Bootstrap a Godot project -- deploy the native test and coverage addons, generate configs. |
144
144
  | `gd-tools doctor` | Diagnose the development environment -- Godot, native test addon, coverage addon, tooling; reports legacy GUT artifacts as migration advice. |
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. 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>/`. |
146
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. |
147
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. |
148
148
  | `gd-tools format` | Format GDScript files using gdformat with check and diff modes. Accepts one or more file or directory paths. |
@@ -165,6 +165,9 @@ gd-tools test --changed # only suites mapped from git-changed
165
165
  gd-tools test --changed --base main # PR/CI mode: diff from merge-base with main
166
166
  gd-tools test --exitfirst # stop dispatching after the first failing suite
167
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
168
171
  ```
169
172
 
170
173
  `--tag` is repeatable, and `--test-timeout` (per test) is separate from
@@ -216,6 +219,25 @@ own coverage report; merge the shards with `gd-tools coverage merge`.
216
219
  [sharding recipe](./docs/USER_GUIDE.md#sharding-a-test-run-across-ci-jobs)
217
220
  for a GitHub Actions matrix example.
218
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
+
219
241
  Repeat runs are faster: the `godot --headless --import` step is cached under
220
242
  `.gd-tools/native/import-cache/` and skipped when `project.godot`, any
221
243
  project source/resource file (scripts, scenes, import sidecars, addons), or
@@ -406,6 +428,8 @@ runtime = "native" # native (default); "gut" is no longer a runnable runt
406
428
  timeout_seconds = 5.0 # default per-test timeout for async native tests
407
429
  retries = 0 # opt-in retries per native test; retried passes are flagged as flaky
408
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
409
433
  tags = [] # native suite tag filters
410
434
 
411
435
  [lint]
@@ -100,7 +100,7 @@ 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. `--exitfirst` (`-x`) stops dispatching new suites after the first failing suite; `--shard K/N` runs one round-robin shard for CI matrix splitting. 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. |
@@ -123,6 +123,9 @@ gd-tools test --changed # only suites mapped from git-changed
123
123
  gd-tools test --changed --base main # PR/CI mode: diff from merge-base with main
124
124
  gd-tools test --exitfirst # stop dispatching after the first failing suite
125
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
126
129
  ```
127
130
 
128
131
  `--tag` is repeatable, and `--test-timeout` (per test) is separate from
@@ -174,6 +177,25 @@ own coverage report; merge the shards with `gd-tools coverage merge`.
174
177
  [sharding recipe](./docs/USER_GUIDE.md#sharding-a-test-run-across-ci-jobs)
175
178
  for a GitHub Actions matrix example.
176
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
+
177
199
  Repeat runs are faster: the `godot --headless --import` step is cached under
178
200
  `.gd-tools/native/import-cache/` and skipped when `project.godot`, any
179
201
  project source/resource file (scripts, scenes, import sidecars, addons), or
@@ -364,6 +386,8 @@ runtime = "native" # native (default); "gut" is no longer a runnable runt
364
386
  timeout_seconds = 5.0 # default per-test timeout for async native tests
365
387
  retries = 0 # opt-in retries per native test; retried passes are flagged as flaky
366
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
367
391
  tags = [] # native suite tag filters
368
392
 
369
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.8.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"}
@@ -110,7 +110,7 @@ target-version = ["py310"]
110
110
 
111
111
  [tool.commitizen]
112
112
  name = "cz_conventional_commits"
113
- version = "0.8.0"
113
+ version = "0.9.0"
114
114
  version_files = ["pyproject.toml:version"]
115
115
  changelog_file = "CHANGELOG.md"
116
116
  tag_format = "v$version"
@@ -358,8 +358,14 @@ static func _inject_trackers(
358
358
  var branch_type = str(entry.get("branch_type", ""))
359
359
  var insert_index := target_index
360
360
  if branch_type in ["match_case", "if_false", "elif_true"]:
361
- insert_index = target_index + 1
362
- var indent := _extract_indent(source_lines[insert_index])
361
+ var header_end := _find_header_end(source_lines, target_index)
362
+ if header_end >= 0:
363
+ insert_index = header_end + 1
364
+ else:
365
+ insert_index = target_index + 1
366
+ var indent := ""
367
+ if insert_index >= 0 and insert_index < source_lines.size():
368
+ indent = _extract_indent(source_lines[insert_index])
363
369
  if indent.is_empty():
364
370
  indent = _extract_indent(source_lines[target_index])
365
371
  var tracker := "%sGdToolsNativeCoverage.hit(%d, %d)" % [
@@ -512,3 +518,105 @@ static func _extract_indent(line: String) -> String:
512
518
  else:
513
519
  break
514
520
  return indent
521
+
522
+
523
+ static func _find_header_end(source_lines: PackedStringArray, start_index: int) -> int:
524
+ ## Scan forward from a branch header start line to its terminating colon.
525
+ ##
526
+ ## Returns the 0-based line index holding the first `:` at bracket-depth 0
527
+ ## outside strings and comments, or -1 when it cannot be resolved. The
528
+ ## caller fails open on -1 by keeping the historical insertion position
529
+ ## rather than corrupting the source.
530
+ var size := source_lines.size()
531
+ if start_index < 0 or start_index >= size:
532
+ return -1
533
+ var depth := 0
534
+ var in_single := ""
535
+ var in_triple := ""
536
+ var escaped := false
537
+ var i := start_index
538
+ while i < size:
539
+ var line := source_lines[i]
540
+ var last_code := ""
541
+ var j := 0
542
+ var length := line.length()
543
+ while j < length:
544
+ if not in_triple.is_empty():
545
+ if escaped:
546
+ escaped = false
547
+ j += 1
548
+ continue
549
+ if line.substr(j, 1) == "\\":
550
+ escaped = true
551
+ j += 1
552
+ continue
553
+ if line.substr(j, 3) == in_triple:
554
+ in_triple = ""
555
+ j += 3
556
+ continue
557
+ j += 1
558
+ continue
559
+ if not in_single.is_empty():
560
+ if escaped:
561
+ escaped = false
562
+ j += 1
563
+ continue
564
+ if line.substr(j, 1) == "\\":
565
+ escaped = true
566
+ j += 1
567
+ continue
568
+ if line.substr(j, 1) == in_single:
569
+ in_single = ""
570
+ j += 1
571
+ continue
572
+ j += 1
573
+ continue
574
+ var ch := line.substr(j, 1)
575
+ if ch == "#":
576
+ break
577
+ if ch == "\"" or ch == "'":
578
+ var triple := ch + ch + ch
579
+ if line.substr(j, 3) == triple:
580
+ in_triple = triple
581
+ escaped = false
582
+ j += 3
583
+ continue
584
+ in_single = ch
585
+ escaped = false
586
+ j += 1
587
+ continue
588
+ if ch == "(" or ch == "[" or ch == "{":
589
+ depth += 1
590
+ last_code = ch
591
+ j += 1
592
+ continue
593
+ if ch == ")" or ch == "]" or ch == "}":
594
+ depth = maxi(0, depth - 1)
595
+ last_code = ch
596
+ j += 1
597
+ continue
598
+ if ch == ":":
599
+ if depth == 0:
600
+ return i
601
+ last_code = ch
602
+ j += 1
603
+ continue
604
+ if ch == " " or ch == "\t" or ch == "\r":
605
+ j += 1
606
+ continue
607
+ if ch == "\\":
608
+ last_code = ch
609
+ j += 1
610
+ continue
611
+ last_code = ch
612
+ j += 1
613
+ if not in_single.is_empty():
614
+ in_single = ""
615
+ escaped = false
616
+ else:
617
+ escaped = false
618
+ if depth > 0 or last_code == "\\":
619
+ i += 1
620
+ continue
621
+ return -1
622
+ return -1
@@ -8,9 +8,11 @@ this module: the content is staged in a temporary file beside the destination, f
8
8
  and fsynced, and moved into place with a single ``os.replace``.
9
9
  """
10
10
 
11
+ import json
11
12
  import os
12
13
  import tempfile
13
14
  from pathlib import Path
15
+ from typing import Any
14
16
 
15
17
 
16
18
  def atomic_write_bytes(path: Path | str, payload: bytes) -> Path:
@@ -76,3 +78,36 @@ def atomic_write_text(path: Path | str, text: str) -> Path:
76
78
  written.
77
79
  """
78
80
  return atomic_write_bytes(path, text.encode("utf-8"))
81
+
82
+
83
+ def atomic_write_json(
84
+ path: Path | str,
85
+ payload: Any,
86
+ *,
87
+ sort_keys: bool = True,
88
+ indent: int | None = 2,
89
+ ) -> Path:
90
+ """Serialize ``payload`` as JSON and write it atomically as UTF-8.
91
+
92
+ Convenience wrapper around :func:`atomic_write_text` for JSON
93
+ documents. A trailing newline is appended so the file ends cleanly
94
+ for diffing and line-oriented tooling.
95
+
96
+ Args:
97
+ path: Destination file path (a :class:`~pathlib.Path` or a
98
+ string).
99
+ payload: JSON-serializable value (dicts, lists, scalars).
100
+ sort_keys: Sort dictionary keys; ``True`` matches the writers
101
+ this helper consolidates.
102
+ indent: JSON indentation depth, or ``None`` for compact output.
103
+
104
+ Returns:
105
+ The destination path.
106
+
107
+ Raises:
108
+ TypeError, ValueError: If ``payload`` is not JSON-serializable.
109
+ OSError: If the temporary file or final replacement cannot be
110
+ written.
111
+ """
112
+ text = json.dumps(payload, indent=indent, sort_keys=sort_keys) + "\n"
113
+ return atomic_write_text(path, text)
@@ -0,0 +1,219 @@
1
+ """CLI entry point for gd-tools."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import click
8
+ from click.shell_completion import (
9
+ BashComplete,
10
+ add_completion_class,
11
+ )
12
+
13
+ from . import __version__
14
+ from .addon_check import check_addon_version
15
+ from .commands.clean import clean
16
+ from .commands.completion import create_completion_command
17
+ from .commands.config import config
18
+ from .commands.coverage import coverage
19
+ from .commands.doctor import doctor
20
+ from .commands.format import format
21
+ from .commands.init import init
22
+ from .commands.install_hooks import install_hooks
23
+ from .commands.lint import lint
24
+ from .commands.migrate import migrate
25
+ from .commands.test import test
26
+ from .commands.version import version
27
+ from .config import set_explicit_project_root
28
+ from .update_check import check_for_update
29
+ from .verbosity import Verbosity, set_verbosity
30
+
31
+
32
+ def _configure_windows_utf8() -> None:
33
+ """Reconfigure stdout/stderr to UTF-8 on Windows.
34
+
35
+ Windows defaults to the system codepage (e.g. cp1252) for console
36
+ output, which cannot encode Unicode characters used by Rich (✓, ✗,
37
+ ⚠). This reconfigures the standard streams to UTF-8, matching the
38
+ effect of setting PYTHONUTF8=1.
39
+ """
40
+ if sys.platform != "win32":
41
+ return
42
+ for stream in (sys.stdout, sys.stderr):
43
+ reconfigure = getattr(stream, "reconfigure", None)
44
+ if reconfigure is not None:
45
+ try:
46
+ reconfigure(encoding="utf-8")
47
+ except (ValueError, OSError):
48
+ pass
49
+
50
+
51
+ _configure_windows_utf8()
52
+
53
+
54
+ _SOURCE_POWERSHELL = """Register-ArgumentCompleter -Native -CommandName %(prog_name)s -ScriptBlock {
55
+ param($wordToComplete, $commandAst, $cursorPosition)
56
+
57
+ $words = $commandAst.CommandElements | ForEach-Object { $_.ToString() }
58
+ $env:COMP_WORDS = $words -join ' '
59
+ $commandElements = $commandAst.CommandElements
60
+ $env:COMP_CWORD = $commandElements.Count
61
+ for ($i = 0; $i -lt $commandElements.Count; $i++) {
62
+ if ($cursorPosition -le $commandElements[$i].Extent.StartOffset) {
63
+ $env:COMP_CWORD = $i
64
+ break
65
+ }
66
+ }
67
+
68
+ $env:%(complete_var)s = "powershell_complete"
69
+ %(prog_name)s | ForEach-Object {
70
+ $values = $_.Split(",")
71
+ if ($values[0] -eq "dir") {
72
+ [System.Management.Automation.CompletionResult]::new($values[1], $values[1], "ParameterValue", $values[1])
73
+ } elseif ($values[0] -eq "file") {
74
+ [System.Management.Automation.CompletionResult]::new($values[1], $values[1], "ParameterValue", $values[1])
75
+ } else {
76
+ $help = if ($values[2] -eq "_") { "" } else { $values[2] }
77
+ [System.Management.Automation.CompletionResult]::new($values[1], $values[1], "ParameterValue", $help)
78
+ }
79
+ }
80
+
81
+ $env:COMP_WORDS = $null
82
+ $env:COMP_CWORD = 0
83
+ $env:%(complete_var)s = $null
84
+ }
85
+ """
86
+
87
+
88
+ class PowerShellComplete(BashComplete):
89
+ """Shell completion for PowerShell.
90
+
91
+ Click 8.2.x does not include a PowerShell completion class.
92
+ This subclass reuses BashComplete's completion args and
93
+ format_completion, providing a PowerShell-compatible source
94
+ template using Register-ArgumentCompleter.
95
+ """
96
+
97
+ name = "powershell"
98
+ source_template = _SOURCE_POWERSHELL
99
+
100
+
101
+ add_completion_class(PowerShellComplete)
102
+
103
+
104
+ class GdToolsGroup(click.Group):
105
+ """Custom Click group that converts NotImplementedError to exit code 2.
106
+
107
+ Stub commands raise ``NotImplementedError`` to indicate they are not
108
+ yet implemented. This class catches that exception and exits with
109
+ code 2 (configuration/usage error), consistent with the gd-tools
110
+ error convention.
111
+ """
112
+
113
+ def invoke(self, ctx) -> Any:
114
+ """Invoke the group, catching NotImplementedError as exit code 2.
115
+
116
+ Performs an update check before dispatching to the subcommand.
117
+ If a newer version is available, a notification is printed to
118
+ stderr. The check fails silently and never blocks execution.
119
+
120
+ In quiet mode (``--quiet``/``-q``), both the update check and
121
+ the addon version check are skipped entirely.
122
+
123
+ Args:
124
+ ctx: The Click context for this invocation.
125
+
126
+ Returns:
127
+ The result of the underlying command invocation.
128
+
129
+ Raises:
130
+ SystemExit: With code 2 if a command raises
131
+ NotImplementedError.
132
+ """
133
+ # Use raw ctx.params here — set_verbosity() runs inside the group
134
+ # callback (invoked by super().invoke below), so get_verbosity()
135
+ # is not yet set at this point.
136
+ quiet = ctx.params.get("quiet", False)
137
+ if not quiet:
138
+ latest = check_for_update()
139
+ if latest is not None:
140
+ click.echo(
141
+ f"A new version of gd-tools is available: {latest} "
142
+ f"(you have {__version__}).\n"
143
+ f"Run `pip install --upgrade gd-tools-cli` to update.",
144
+ err=True,
145
+ )
146
+ check_addon_version()
147
+ try:
148
+ return super().invoke(ctx)
149
+ except NotImplementedError:
150
+ click.echo(
151
+ "Error: This command is not yet implemented.",
152
+ err=True,
153
+ )
154
+ ctx.exit(2)
155
+
156
+
157
+ @click.group(cls=GdToolsGroup)
158
+ @click.version_option(
159
+ version=__version__,
160
+ prog_name="gd-tools",
161
+ message="%(prog)s %(version)s",
162
+ )
163
+ @click.option(
164
+ "--verbose",
165
+ "-v",
166
+ is_flag=True,
167
+ default=False,
168
+ help="Show verbose output including underlying commands and timing.",
169
+ )
170
+ @click.option(
171
+ "--quiet",
172
+ "-q",
173
+ is_flag=True,
174
+ default=False,
175
+ help="Suppress non-essential output (update checks, progress info).",
176
+ )
177
+ @click.option(
178
+ "--project",
179
+ "-p",
180
+ "project_path",
181
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
182
+ default=None,
183
+ help="Path to the Godot project root. Defaults to discovery from "
184
+ "the current working directory.",
185
+ )
186
+ def cli(verbose: bool, quiet: bool, project_path: Path | None):
187
+ """gd-tools: A modern development workflow CLI for GDScript."""
188
+ set_explicit_project_root(project_path)
189
+ if verbose and quiet:
190
+ click.echo(
191
+ "Error: --verbose and --quiet are mutually exclusive.", err=True
192
+ )
193
+ ctx = click.get_current_context()
194
+ ctx.exit(2)
195
+ if verbose:
196
+ set_verbosity(Verbosity.VERBOSE)
197
+ elif quiet:
198
+ set_verbosity(Verbosity.QUIET)
199
+ else:
200
+ set_verbosity(Verbosity.DEFAULT)
201
+
202
+
203
+ # Register the extracted commands, preserving the original help order:
204
+ # init, doctor, version, clean, install-hooks, test, migrate, lint,
205
+ # format, coverage, config, completion.
206
+ cli.add_command(init)
207
+ cli.add_command(doctor)
208
+ cli.add_command(version)
209
+ cli.add_command(clean)
210
+ cli.add_command(install_hooks)
211
+
212
+
213
+ cli.add_command(test)
214
+ cli.add_command(migrate)
215
+ cli.add_command(lint)
216
+ cli.add_command(format)
217
+ cli.add_command(coverage)
218
+ cli.add_command(config)
219
+ cli.add_command(create_completion_command(cli))
@@ -0,0 +1,6 @@
1
+ """
2
+ Command modules for the gd-tools CLI.
3
+
4
+ Commands live in this package; `cli.py` remains the thin dispatcher
5
+ that registers them.
6
+ """