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.
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/PKG-INFO +26 -2
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/README.md +25 -1
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/pyproject.toml +2 -2
- {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
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/atomic_io.py +35 -0
- gd_tools_cli-0.9.0/src/gd_tools/cli.py +219 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/__init__.py +6 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/clean.py +171 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/completion.py +61 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/config.py +255 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/coverage.py +378 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/doctor.py +24 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/format.py +73 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/init.py +25 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/install_hooks.py +118 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/lint.py +64 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/migrate.py +137 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/test.py +359 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/test_validation.py +210 -0
- gd_tools_cli-0.9.0/src/gd_tools/commands/version.py +36 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/config.py +6 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/plan_generator.py +28 -3
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/artifacts.py +30 -30
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/command.py +78 -2
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/orchestrator.py +25 -20
- gd_tools_cli-0.9.0/src/gd_tools/native_test/ordering.py +110 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/protocol.py +2 -7
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/output.py +14 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/session.py +16 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/PKG-INFO +26 -2
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/SOURCES.txt +15 -0
- gd_tools_cli-0.8.0/src/gd_tools/cli.py +0 -1790
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/setup.cfg +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/__main__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addon_check.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-coverage/coverage.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/coverage_overlay.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/dock.gd +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/plugin.cfg +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/addons/gd-tools-editor/plugin.gd +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/changes.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/clean.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/cobertura_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/diff_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/html_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/lcov_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/omissions.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/orchestrator.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/patch.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/playtest.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/templates/file.html +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/templates/index.html +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/coverage/terminal_reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/doctor.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/errors.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/file_discovery.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/format_runner.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/gh_annotations.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/godot.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/init.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/lint_runner.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/apply.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/gutconfig.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/reporter.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/rewrite.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/scan.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/migration/translate.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/bridge_scan.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/discovery.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/flaky.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/import_cache.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/preflight.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/native_test/preflight_cache.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/pre_commit.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/schema.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/test_results.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/update_check.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/verbosity.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/version.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/__init__.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/coalescer.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/loop.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/mapping.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/observer.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools/watch/scope.py +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/dependency_links.txt +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/entry_points.txt +0 -0
- {gd_tools_cli-0.8.0 → gd_tools_cli-0.9.0}/src/gd_tools_cli.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
362
|
-
|
|
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))
|