gd-tools-cli 0.1.0__py3-none-any.whl
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/__init__.py +3 -0
- gd_tools/__main__.py +31 -0
- gd_tools/addons/__init__.py +0 -0
- gd_tools/addons/gd-tools-coverage/coverage.gd +43 -0
- gd_tools/addons/gd-tools-coverage/post_run_hook.gd +113 -0
- gd_tools/addons/gd-tools-coverage/pre_run_hook.gd +229 -0
- gd_tools/cli.py +329 -0
- gd_tools/config.py +289 -0
- gd_tools/coverage/__init__.py +23 -0
- gd_tools/coverage/cobertura_reporter.py +136 -0
- gd_tools/coverage/html_reporter.py +95 -0
- gd_tools/coverage/lcov_reporter.py +90 -0
- gd_tools/coverage/orchestrator.py +282 -0
- gd_tools/coverage/plan_generator.py +377 -0
- gd_tools/coverage/reporter.py +518 -0
- gd_tools/coverage/templates/file.html +72 -0
- gd_tools/coverage/templates/index.html +90 -0
- gd_tools/coverage/terminal_reporter.py +107 -0
- gd_tools/doctor.py +495 -0
- gd_tools/errors.py +79 -0
- gd_tools/file_discovery.py +41 -0
- gd_tools/format_runner.py +118 -0
- gd_tools/godot.py +346 -0
- gd_tools/init.py +619 -0
- gd_tools/lint_runner.py +223 -0
- gd_tools/test_runner.py +474 -0
- gd_tools_cli-0.1.0.dist-info/METADATA +191 -0
- gd_tools_cli-0.1.0.dist-info/RECORD +31 -0
- gd_tools_cli-0.1.0.dist-info/WHEEL +5 -0
- gd_tools_cli-0.1.0.dist-info/entry_points.txt +2 -0
- gd_tools_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Terminal reporter using Rich for formatted coverage output."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
|
|
5
|
+
from rich import box
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
|
|
9
|
+
from gd_tools.coverage.plan_generator import CoveragePlan
|
|
10
|
+
from gd_tools.coverage.reporter import (
|
|
11
|
+
CoverageData,
|
|
12
|
+
FileCoverage,
|
|
13
|
+
compute_file_summary,
|
|
14
|
+
compute_summary,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
_GREEN_THRESHOLD = 0.80
|
|
18
|
+
_YELLOW_THRESHOLD = 0.50
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _color_for_rate(rate: float) -> str:
|
|
22
|
+
"""Return Rich color name for a coverage rate.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
rate: Coverage rate (0.0-1.0).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Rich color name: ``"green"``, ``"yellow"``, or ``"red"``.
|
|
29
|
+
"""
|
|
30
|
+
if rate >= _GREEN_THRESHOLD:
|
|
31
|
+
return "green"
|
|
32
|
+
elif rate >= _YELLOW_THRESHOLD:
|
|
33
|
+
return "yellow"
|
|
34
|
+
else:
|
|
35
|
+
return "red"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def generate_terminal_report(
|
|
39
|
+
plan: CoveragePlan,
|
|
40
|
+
data: CoverageData,
|
|
41
|
+
) -> str:
|
|
42
|
+
"""Generate a Rich table coverage report as a string.
|
|
43
|
+
|
|
44
|
+
Produces a color-coded table with per-file line and branch
|
|
45
|
+
coverage metrics, followed by an overall summary. Table borders
|
|
46
|
+
use ASCII characters for terminal compatibility.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
plan: The instrumentation plan.
|
|
50
|
+
data: The runtime coverage data.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
A formatted Rich table string with ANSI color codes.
|
|
54
|
+
"""
|
|
55
|
+
buf = io.StringIO()
|
|
56
|
+
console = Console(file=buf, force_terminal=True, width=120)
|
|
57
|
+
|
|
58
|
+
table = Table(title="Coverage Report", box=box.ASCII)
|
|
59
|
+
table.add_column("File")
|
|
60
|
+
table.add_column("Lines Found", justify="right")
|
|
61
|
+
table.add_column("Lines Hit", justify="right")
|
|
62
|
+
table.add_column("Line %", justify="right")
|
|
63
|
+
table.add_column("Branches Found", justify="right")
|
|
64
|
+
table.add_column("Branches Hit", justify="right")
|
|
65
|
+
table.add_column("Branch %", justify="right")
|
|
66
|
+
|
|
67
|
+
summary = compute_summary(plan, data)
|
|
68
|
+
coverage_by_id = {fc.file_id: fc for fc in data.files}
|
|
69
|
+
|
|
70
|
+
for file_plan in plan.files:
|
|
71
|
+
file_data = coverage_by_id.get(
|
|
72
|
+
file_plan.file_id,
|
|
73
|
+
FileCoverage(file_id=file_plan.file_id, hits={}),
|
|
74
|
+
)
|
|
75
|
+
fs = compute_file_summary(file_plan, file_data)
|
|
76
|
+
|
|
77
|
+
line_color = _color_for_rate(fs.line_rate)
|
|
78
|
+
branch_color = _color_for_rate(fs.branch_rate)
|
|
79
|
+
|
|
80
|
+
table.add_row(
|
|
81
|
+
file_plan.path,
|
|
82
|
+
str(fs.total_lines),
|
|
83
|
+
str(fs.covered_lines),
|
|
84
|
+
f"[{line_color}]{fs.line_rate:.1%}[/{line_color}]",
|
|
85
|
+
str(fs.total_branches),
|
|
86
|
+
str(fs.covered_branches),
|
|
87
|
+
f"[{branch_color}]{fs.branch_rate:.1%}[/{branch_color}]",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
console.print(table)
|
|
91
|
+
|
|
92
|
+
# Overall summary
|
|
93
|
+
overall_line_color = _color_for_rate(summary.line_rate)
|
|
94
|
+
overall_branch_color = _color_for_rate(summary.branch_rate)
|
|
95
|
+
console.print()
|
|
96
|
+
console.print(
|
|
97
|
+
f"[bold]Overall Line Coverage:[/bold] "
|
|
98
|
+
f"[{overall_line_color}]{summary.line_rate:.1%}"
|
|
99
|
+
f"[/{overall_line_color}]"
|
|
100
|
+
)
|
|
101
|
+
console.print(
|
|
102
|
+
f"[bold]Overall Branch Coverage:[/bold] "
|
|
103
|
+
f"[{overall_branch_color}]{summary.branch_rate:.1%}"
|
|
104
|
+
f"[/{overall_branch_color}]"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return buf.getvalue()
|
gd_tools/doctor.py
ADDED
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""Diagnostic command for checking gd-tools project health.
|
|
2
|
+
|
|
3
|
+
Implements ``gd-tools doctor``, which runs a series of environment
|
|
4
|
+
and configuration checks and reports pass/fail status with actionable
|
|
5
|
+
fix hints. See TDD \u00a73.6 and PRD \u00a78.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
if sys.version_info >= (3, 11):
|
|
15
|
+
import tomllib
|
|
16
|
+
else: # pragma: no cover
|
|
17
|
+
import tomli as tomllib
|
|
18
|
+
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from .config import GdToolsConfig, find_project_root, load_config
|
|
22
|
+
from .errors import ConfigError
|
|
23
|
+
from .godot import (
|
|
24
|
+
GodotNotFoundError,
|
|
25
|
+
check_version_compatible,
|
|
26
|
+
find_godot,
|
|
27
|
+
get_gut_version_for_godot,
|
|
28
|
+
)
|
|
29
|
+
from .init import COVERAGE_ADDON_FILES, get_installed_gut_version
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class CheckResult:
|
|
34
|
+
"""Result of a single diagnostic check.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
name: Human-readable name of the check.
|
|
38
|
+
passed: Whether the check passed.
|
|
39
|
+
message: Description of what was found.
|
|
40
|
+
fix_hint: Actionable suggestion for failures.
|
|
41
|
+
severity: "critical" or "warning".
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
passed: bool
|
|
46
|
+
message: str
|
|
47
|
+
fix_hint: str = ""
|
|
48
|
+
severity: str = "critical"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class DoctorResult:
|
|
53
|
+
"""Aggregate result of all diagnostic checks.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
checks: List of individual check results.
|
|
57
|
+
all_passed: True if every check passed.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
checks: list[CheckResult]
|
|
61
|
+
all_passed: bool
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --- Godot and External Tool Checks ---
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def check_godot_binary(config: GdToolsConfig) -> CheckResult:
|
|
68
|
+
"""Check that a Godot binary is found via the detection chain.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
config: The gd-tools configuration containing Godot settings.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
CheckResult indicating whether the Godot binary was found.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
info = find_godot(config.godot)
|
|
78
|
+
except GodotNotFoundError:
|
|
79
|
+
return CheckResult(
|
|
80
|
+
name="Godot Binary",
|
|
81
|
+
passed=False,
|
|
82
|
+
message="Godot binary not found",
|
|
83
|
+
fix_hint=(
|
|
84
|
+
"Install Godot 4.5+ from "
|
|
85
|
+
"https://godotengine.org and set GODOT_BIN "
|
|
86
|
+
"or add to PATH."
|
|
87
|
+
),
|
|
88
|
+
severity="critical",
|
|
89
|
+
)
|
|
90
|
+
return CheckResult(
|
|
91
|
+
name="Godot Binary",
|
|
92
|
+
passed=True,
|
|
93
|
+
message=f"Godot {info.version} at {info.path}",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def check_godot_version(config: GdToolsConfig) -> CheckResult:
|
|
98
|
+
"""Check that the detected Godot version is >= 4.5.0.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
config: The gd-tools configuration containing Godot settings.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
CheckResult indicating whether the Godot version is compatible.
|
|
105
|
+
"""
|
|
106
|
+
try:
|
|
107
|
+
info = find_godot(config.godot)
|
|
108
|
+
except GodotNotFoundError:
|
|
109
|
+
return CheckResult(
|
|
110
|
+
name="Godot Version",
|
|
111
|
+
passed=False,
|
|
112
|
+
message="Godot binary not found - cannot check version",
|
|
113
|
+
fix_hint=(
|
|
114
|
+
"Install Godot 4.5+ from "
|
|
115
|
+
"https://godotengine.org and set GODOT_BIN "
|
|
116
|
+
"or add to PATH."
|
|
117
|
+
),
|
|
118
|
+
severity="critical",
|
|
119
|
+
)
|
|
120
|
+
if check_version_compatible(info.version):
|
|
121
|
+
return CheckResult(
|
|
122
|
+
name="Godot Version",
|
|
123
|
+
passed=True,
|
|
124
|
+
message=f"Godot {info.version} is >= 4.5.0",
|
|
125
|
+
)
|
|
126
|
+
return CheckResult(
|
|
127
|
+
name="Godot Version",
|
|
128
|
+
passed=False,
|
|
129
|
+
message=f"Godot {info.version} is below required 4.5.0",
|
|
130
|
+
fix_hint=(
|
|
131
|
+
"Install Godot 4.5+ from "
|
|
132
|
+
"https://godotengine.org and set GODOT_BIN "
|
|
133
|
+
"or add to PATH."
|
|
134
|
+
),
|
|
135
|
+
severity="critical",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def check_gdtoolkit() -> CheckResult:
|
|
140
|
+
"""Check that gdlint and gdformat CLI tools are installed.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
CheckResult indicating whether both tools are available.
|
|
144
|
+
"""
|
|
145
|
+
missing = []
|
|
146
|
+
for tool in ("gdlint", "gdformat"):
|
|
147
|
+
try:
|
|
148
|
+
subprocess.run(
|
|
149
|
+
[tool, "--version"],
|
|
150
|
+
capture_output=True,
|
|
151
|
+
timeout=10,
|
|
152
|
+
)
|
|
153
|
+
except FileNotFoundError:
|
|
154
|
+
missing.append(tool)
|
|
155
|
+
|
|
156
|
+
if missing:
|
|
157
|
+
return CheckResult(
|
|
158
|
+
name="GD Toolkit",
|
|
159
|
+
passed=False,
|
|
160
|
+
message=f"Missing tools: {', '.join(missing)}",
|
|
161
|
+
fix_hint="Install gdtoolkit: pip install gdtoolkit",
|
|
162
|
+
severity="critical",
|
|
163
|
+
)
|
|
164
|
+
return CheckResult(
|
|
165
|
+
name="GD Toolkit",
|
|
166
|
+
passed=True,
|
|
167
|
+
message="gdlint and gdformat are installed",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# --- GUT and Project Configuration Checks ---
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def check_gut_installed(project_root: Path) -> CheckResult:
|
|
175
|
+
"""Check that GUT is installed in the project.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
project_root: Path to the Godot project root.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
CheckResult indicating whether GUT is installed.
|
|
182
|
+
"""
|
|
183
|
+
gut_path = project_root / "addons" / "gut" / "gut.gd"
|
|
184
|
+
if gut_path.exists():
|
|
185
|
+
return CheckResult(
|
|
186
|
+
name="GUT Installed",
|
|
187
|
+
passed=True,
|
|
188
|
+
message="GUT is installed",
|
|
189
|
+
)
|
|
190
|
+
return CheckResult(
|
|
191
|
+
name="GUT Installed",
|
|
192
|
+
passed=False,
|
|
193
|
+
message="GUT is not installed",
|
|
194
|
+
fix_hint=(
|
|
195
|
+
"Run `gd-tools init` to install GUT, "
|
|
196
|
+
"or see https://github.com/bitwes/Gut."
|
|
197
|
+
),
|
|
198
|
+
severity="critical",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def check_gut_version(project_root: Path, godot_version: str) -> CheckResult:
|
|
203
|
+
"""Check that the installed GUT version matches the expected version.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
project_root: Path to the Godot project root.
|
|
207
|
+
godot_version: The detected Godot version string.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
CheckResult indicating whether the GUT version is compatible.
|
|
211
|
+
"""
|
|
212
|
+
installed = get_installed_gut_version(project_root)
|
|
213
|
+
if installed is None:
|
|
214
|
+
return CheckResult(
|
|
215
|
+
name="GUT Version",
|
|
216
|
+
passed=True,
|
|
217
|
+
message="GUT version unknown - cannot verify",
|
|
218
|
+
)
|
|
219
|
+
expected = get_gut_version_for_godot(godot_version)
|
|
220
|
+
if installed == expected:
|
|
221
|
+
return CheckResult(
|
|
222
|
+
name="GUT Version",
|
|
223
|
+
passed=True,
|
|
224
|
+
message=f"GUT version {installed} matches expected {expected}",
|
|
225
|
+
)
|
|
226
|
+
return CheckResult(
|
|
227
|
+
name="GUT Version",
|
|
228
|
+
passed=False,
|
|
229
|
+
message=(
|
|
230
|
+
f"GUT version {installed} does not match " f"expected {expected}"
|
|
231
|
+
),
|
|
232
|
+
fix_hint=(
|
|
233
|
+
f"Install GUT version {expected} " f"for Godot {godot_version}"
|
|
234
|
+
),
|
|
235
|
+
severity="warning",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def check_coverage_addon(project_root: Path) -> CheckResult:
|
|
240
|
+
"""Check that the gd-tools-coverage addon files are present.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
project_root: Path to the Godot project root.
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
CheckResult indicating whether all coverage addon files exist.
|
|
247
|
+
"""
|
|
248
|
+
cov_dir = project_root / "addons" / "gd-tools-coverage"
|
|
249
|
+
missing = [f for f in COVERAGE_ADDON_FILES if not (cov_dir / f).exists()]
|
|
250
|
+
if missing:
|
|
251
|
+
return CheckResult(
|
|
252
|
+
name="Coverage Addon",
|
|
253
|
+
passed=False,
|
|
254
|
+
message=f"Missing coverage files: {', '.join(missing)}",
|
|
255
|
+
fix_hint="Run `gd-tools init` to install the coverage addon.",
|
|
256
|
+
severity="warning",
|
|
257
|
+
)
|
|
258
|
+
return CheckResult(
|
|
259
|
+
name="Coverage Addon",
|
|
260
|
+
passed=True,
|
|
261
|
+
message="Coverage addon is installed",
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def check_gutconfig(project_root: Path) -> CheckResult:
|
|
266
|
+
"""Check that .gutconfig.json is valid JSON with hook script keys.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
project_root: Path to the Godot project root.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
CheckResult indicating whether .gutconfig.json exists, is valid
|
|
273
|
+
JSON, and contains both ``pre_run_script`` and ``post_run_script``
|
|
274
|
+
keys.
|
|
275
|
+
"""
|
|
276
|
+
gutconfig_path = project_root / ".gutconfig.json"
|
|
277
|
+
if not gutconfig_path.exists():
|
|
278
|
+
return CheckResult(
|
|
279
|
+
name="GUT Config",
|
|
280
|
+
passed=False,
|
|
281
|
+
message=".gutconfig.json not found",
|
|
282
|
+
fix_hint="Run `gd-tools init` to generate .gutconfig.json.",
|
|
283
|
+
severity="warning",
|
|
284
|
+
)
|
|
285
|
+
try:
|
|
286
|
+
content = json.loads(gutconfig_path.read_text())
|
|
287
|
+
except ValueError as exc:
|
|
288
|
+
return CheckResult(
|
|
289
|
+
name="GUT Config",
|
|
290
|
+
passed=False,
|
|
291
|
+
message=f".gutconfig.json is invalid JSON: {exc}",
|
|
292
|
+
fix_hint="Fix the JSON syntax in .gutconfig.json or run `gd-tools init`.",
|
|
293
|
+
severity="warning",
|
|
294
|
+
)
|
|
295
|
+
missing_keys = [
|
|
296
|
+
key
|
|
297
|
+
for key in ("pre_run_script", "post_run_script")
|
|
298
|
+
if key not in content
|
|
299
|
+
]
|
|
300
|
+
if missing_keys:
|
|
301
|
+
return CheckResult(
|
|
302
|
+
name="GUT Config",
|
|
303
|
+
passed=False,
|
|
304
|
+
message=f"Missing keys: {', '.join(missing_keys)}",
|
|
305
|
+
fix_hint="Run `gd-tools init` to regenerate .gutconfig.json with hook scripts.",
|
|
306
|
+
severity="warning",
|
|
307
|
+
)
|
|
308
|
+
return CheckResult(
|
|
309
|
+
name="GUT Config",
|
|
310
|
+
passed=True,
|
|
311
|
+
message=".gutconfig.json is valid with hook scripts",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def check_gd_tools_toml(project_root: Path) -> CheckResult:
|
|
316
|
+
"""Check that gd-tools.toml exists and is parseable TOML.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
project_root: Path to the Godot project root.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
CheckResult indicating whether gd-tools.toml exists and
|
|
323
|
+
is valid TOML.
|
|
324
|
+
"""
|
|
325
|
+
toml_path = project_root / "gd-tools.toml"
|
|
326
|
+
if not toml_path.exists():
|
|
327
|
+
return CheckResult(
|
|
328
|
+
name="gd-tools.toml",
|
|
329
|
+
passed=False,
|
|
330
|
+
message="gd-tools.toml not found",
|
|
331
|
+
fix_hint="Run `gd-tools init` to generate gd-tools.toml.",
|
|
332
|
+
severity="critical",
|
|
333
|
+
)
|
|
334
|
+
try:
|
|
335
|
+
with open(toml_path, "rb") as f:
|
|
336
|
+
tomllib.load(f)
|
|
337
|
+
except tomllib.TOMLDecodeError as exc:
|
|
338
|
+
return CheckResult(
|
|
339
|
+
name="gd-tools.toml",
|
|
340
|
+
passed=False,
|
|
341
|
+
message=f"gd-tools.toml is invalid TOML: {exc}",
|
|
342
|
+
fix_hint="Fix the TOML syntax in gd-tools.toml or run `gd-tools init`.",
|
|
343
|
+
severity="critical",
|
|
344
|
+
)
|
|
345
|
+
return CheckResult(
|
|
346
|
+
name="gd-tools.toml",
|
|
347
|
+
passed=True,
|
|
348
|
+
message="gd-tools.toml is valid",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def check_autoload(project_root: Path) -> CheckResult:
|
|
353
|
+
"""Check that _GDTCoverage autoload is registered in project.godot.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
project_root: Path to the Godot project root.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
CheckResult indicating whether the ``_GDTCoverage`` autoload
|
|
360
|
+
is registered in the ``[autoload]`` section of
|
|
361
|
+
``project.godot``.
|
|
362
|
+
"""
|
|
363
|
+
project_godot = project_root / "project.godot"
|
|
364
|
+
if not project_godot.exists():
|
|
365
|
+
return CheckResult(
|
|
366
|
+
name="Autoload",
|
|
367
|
+
passed=False,
|
|
368
|
+
message="project.godot not found",
|
|
369
|
+
fix_hint="Run `gd-tools init` to deploy coverage addon.",
|
|
370
|
+
severity="critical",
|
|
371
|
+
)
|
|
372
|
+
content = project_godot.read_text()
|
|
373
|
+
in_autoload = False
|
|
374
|
+
for line in content.splitlines():
|
|
375
|
+
stripped = line.strip()
|
|
376
|
+
if stripped.startswith("["):
|
|
377
|
+
in_autoload = stripped == "[autoload]"
|
|
378
|
+
continue
|
|
379
|
+
if in_autoload and stripped.startswith("_GDTCoverage="):
|
|
380
|
+
return CheckResult(
|
|
381
|
+
name="Autoload",
|
|
382
|
+
passed=True,
|
|
383
|
+
message="_GDTCoverage autoload is registered",
|
|
384
|
+
)
|
|
385
|
+
return CheckResult(
|
|
386
|
+
name="Autoload",
|
|
387
|
+
passed=False,
|
|
388
|
+
message="_GDTCoverage autoload is not registered",
|
|
389
|
+
fix_hint=(
|
|
390
|
+
"Run `gd-tools init` to deploy coverage addon "
|
|
391
|
+
"(autoload registration in Phase 3)."
|
|
392
|
+
),
|
|
393
|
+
severity="critical",
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
# --- Orchestration ---
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def run_doctor() -> DoctorResult:
|
|
401
|
+
"""Run all diagnostic checks and return aggregated result.
|
|
402
|
+
|
|
403
|
+
Resolves project root, loads config, and runs all 9 checks in
|
|
404
|
+
order. Never raises — all exceptions are caught and converted
|
|
405
|
+
to failed CheckResults.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
DoctorResult with all check results.
|
|
409
|
+
"""
|
|
410
|
+
checks = []
|
|
411
|
+
|
|
412
|
+
try:
|
|
413
|
+
project_root = find_project_root()
|
|
414
|
+
except ConfigError:
|
|
415
|
+
project_root = Path.cwd()
|
|
416
|
+
|
|
417
|
+
try:
|
|
418
|
+
config = load_config()
|
|
419
|
+
except ConfigError:
|
|
420
|
+
config = GdToolsConfig()
|
|
421
|
+
|
|
422
|
+
godot_version = "unknown"
|
|
423
|
+
try:
|
|
424
|
+
info = find_godot(config.godot)
|
|
425
|
+
godot_version = info.version
|
|
426
|
+
except GodotNotFoundError:
|
|
427
|
+
pass
|
|
428
|
+
|
|
429
|
+
check_specs = [
|
|
430
|
+
("Godot Binary", lambda: check_godot_binary(config)),
|
|
431
|
+
("Godot Version", lambda: check_godot_version(config)),
|
|
432
|
+
("GUT Installed", lambda: check_gut_installed(project_root)),
|
|
433
|
+
(
|
|
434
|
+
"GUT Version",
|
|
435
|
+
lambda: check_gut_version(project_root, godot_version),
|
|
436
|
+
),
|
|
437
|
+
("Coverage Addon", lambda: check_coverage_addon(project_root)),
|
|
438
|
+
("GUT Config", lambda: check_gutconfig(project_root)),
|
|
439
|
+
("gd-tools.toml", lambda: check_gd_tools_toml(project_root)),
|
|
440
|
+
("GD Toolkit", lambda: check_gdtoolkit()),
|
|
441
|
+
("Autoload", lambda: check_autoload(project_root)),
|
|
442
|
+
]
|
|
443
|
+
|
|
444
|
+
for name, fn in check_specs:
|
|
445
|
+
try:
|
|
446
|
+
checks.append(fn())
|
|
447
|
+
except Exception as exc:
|
|
448
|
+
checks.append(
|
|
449
|
+
CheckResult(
|
|
450
|
+
name=name,
|
|
451
|
+
passed=False,
|
|
452
|
+
message=f"Unexpected error: {exc}",
|
|
453
|
+
severity="critical",
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
all_passed = all(c.passed for c in checks)
|
|
458
|
+
return DoctorResult(checks=checks, all_passed=all_passed)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def format_doctor_table(result: DoctorResult) -> Table:
|
|
462
|
+
"""Build a rich Table from doctor check results.
|
|
463
|
+
|
|
464
|
+
Creates a color-coded table with Check, Status, Message, and
|
|
465
|
+
Fix Hint columns. Passing checks show a green checkmark, critical
|
|
466
|
+
failures show a red X, and warning failures show a yellow warning
|
|
467
|
+
symbol. A summary line shows the pass count.
|
|
468
|
+
|
|
469
|
+
Args:
|
|
470
|
+
result: The DoctorResult to format.
|
|
471
|
+
|
|
472
|
+
Returns:
|
|
473
|
+
A rich.table.Table ready for console printing.
|
|
474
|
+
"""
|
|
475
|
+
table = Table(title="gd-tools Doctor")
|
|
476
|
+
table.add_column("Check", style="cyan")
|
|
477
|
+
table.add_column("Status")
|
|
478
|
+
table.add_column("Message")
|
|
479
|
+
table.add_column("Fix Hint")
|
|
480
|
+
|
|
481
|
+
passed_count = 0
|
|
482
|
+
for check in result.checks:
|
|
483
|
+
if check.passed:
|
|
484
|
+
status = "[green]\u2713[/green]"
|
|
485
|
+
passed_count += 1
|
|
486
|
+
elif check.severity == "critical":
|
|
487
|
+
status = "[red]\u2717[/red]"
|
|
488
|
+
else:
|
|
489
|
+
status = "[yellow]\u26a0[/yellow]"
|
|
490
|
+
table.add_row(check.name, status, check.message, check.fix_hint)
|
|
491
|
+
|
|
492
|
+
total = len(result.checks)
|
|
493
|
+
table.caption = f"{passed_count}/{total} checks passed"
|
|
494
|
+
|
|
495
|
+
return table
|
gd_tools/errors.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Exception hierarchy for gd-tools.
|
|
2
|
+
|
|
3
|
+
All gd-tools exceptions inherit from :class:`GdToolsError`, which carries an
|
|
4
|
+
``exit_code`` attribute. The CLI entry point uses this code when the error
|
|
5
|
+
propagates to the top level, so every failure mode maps to a deterministic
|
|
6
|
+
process exit code.
|
|
7
|
+
|
|
8
|
+
Exit-code convention:
|
|
9
|
+
- ``2`` — configuration / environment problems (missing Godot, missing
|
|
10
|
+
GUT, bad config). These are "fix your setup" errors.
|
|
11
|
+
- ``1`` — tool failures (tests failed, lint found issues, coverage below
|
|
12
|
+
threshold). These are "your code has a problem" errors.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GdToolsError(Exception):
|
|
17
|
+
"""Base exception for all gd-tools errors.
|
|
18
|
+
|
|
19
|
+
Attributes:
|
|
20
|
+
exit_code: The exit code to use when this error causes the
|
|
21
|
+
program to terminate. Defaults to 2 (config error).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
exit_code: int = 2
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self, message: str = "", *, exit_code: int | None = None
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Initialize the error.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
message: The error message.
|
|
33
|
+
exit_code: Override the class default exit code. If None,
|
|
34
|
+
the class-level ``exit_code`` is used. Defaults to None.
|
|
35
|
+
"""
|
|
36
|
+
super().__init__(message)
|
|
37
|
+
if exit_code is not None:
|
|
38
|
+
self.exit_code = exit_code
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ConfigError(GdToolsError):
|
|
42
|
+
"""Raised when there is a configuration error."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GodotNotFoundError(GdToolsError):
|
|
46
|
+
"""Raised when the Godot binary cannot be found."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class GUTNotInstalledError(GdToolsError):
|
|
50
|
+
"""Raised when GUT is not installed in the project."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CoveragePlanError(GdToolsError):
|
|
54
|
+
"""Raised when there is an error generating the coverage plan."""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CoverageThresholdError(GdToolsError):
|
|
58
|
+
"""Raised when coverage falls below the required threshold."""
|
|
59
|
+
|
|
60
|
+
exit_code: int = 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TestFailureError(GdToolsError):
|
|
64
|
+
"""Raised when tests fail."""
|
|
65
|
+
|
|
66
|
+
__test__ = False
|
|
67
|
+
exit_code: int = 1
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class LintError(GdToolsError):
|
|
71
|
+
"""Raised when linting finds issues."""
|
|
72
|
+
|
|
73
|
+
exit_code: int = 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class FormatError(GdToolsError):
|
|
77
|
+
"""Raised when formatting issues are found."""
|
|
78
|
+
|
|
79
|
+
exit_code: int = 1
|