godot-input-map-auditor 0.1.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.
- godot_input_map_auditor-0.1.0/LICENSE +21 -0
- godot_input_map_auditor-0.1.0/PKG-INFO +81 -0
- godot_input_map_auditor-0.1.0/README.md +59 -0
- godot_input_map_auditor-0.1.0/pyproject.toml +41 -0
- godot_input_map_auditor-0.1.0/setup.cfg +4 -0
- godot_input_map_auditor-0.1.0/src/godot_input_auditor/__init__.py +3 -0
- godot_input_map_auditor-0.1.0/src/godot_input_auditor/__main__.py +5 -0
- godot_input_map_auditor-0.1.0/src/godot_input_auditor/audit.py +67 -0
- godot_input_map_auditor-0.1.0/src/godot_input_auditor/cli.py +103 -0
- godot_input_map_auditor-0.1.0/src/godot_input_auditor/input_parser.py +100 -0
- godot_input_map_auditor-0.1.0/src/godot_input_auditor/models.py +46 -0
- godot_input_map_auditor-0.1.0/src/godot_input_auditor/reporting.py +125 -0
- godot_input_map_auditor-0.1.0/src/godot_input_map_auditor.egg-info/PKG-INFO +81 -0
- godot_input_map_auditor-0.1.0/src/godot_input_map_auditor.egg-info/SOURCES.txt +19 -0
- godot_input_map_auditor-0.1.0/src/godot_input_map_auditor.egg-info/dependency_links.txt +1 -0
- godot_input_map_auditor-0.1.0/src/godot_input_map_auditor.egg-info/entry_points.txt +2 -0
- godot_input_map_auditor-0.1.0/src/godot_input_map_auditor.egg-info/top_level.txt +1 -0
- godot_input_map_auditor-0.1.0/tests/test_audit.py +40 -0
- godot_input_map_auditor-0.1.0/tests/test_cli.py +80 -0
- godot_input_map_auditor-0.1.0/tests/test_parser.py +43 -0
- godot_input_map_auditor-0.1.0/tests/test_reporting.py +42 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Godot Input Map Auditor contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: godot-input-map-auditor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Audit Godot input maps for device coverage, duplicate bindings, and generated references.
|
|
5
|
+
Author: Godot Input Map Auditor contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NonniGB/godot-production-toolkit/tree/main/godot-input-map-auditor
|
|
8
|
+
Project-URL: Issues, https://github.com/NonniGB/godot-production-toolkit/issues
|
|
9
|
+
Keywords: godot,input,touch,gamepad,ci,gamedev
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Godot Input Map Auditor
|
|
24
|
+
|
|
25
|
+
Audit Godot input actions for keyboard, mouse, gamepad, and touch readiness, then generate input reference docs and strongly named GDScript constants.
|
|
26
|
+
|
|
27
|
+
This is a small CI-friendly tool for projects where input coverage can drift across desktop, controller, and mobile targets.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
python -m pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
When published:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
python -m pip install godot-input-map-auditor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
godot-input-audit C:\Projects\MyGame --require keyboard,touch
|
|
45
|
+
godot-input-audit . --write-docs docs\INPUT_REFERENCE.md
|
|
46
|
+
godot-input-audit . --generate-gd scripts\generated\input_actions.gd
|
|
47
|
+
godot-input-audit . --format json --output input-report.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run against the sample project:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
godot-input-audit examples\tiny-godot-project --fail-on none
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## What It Checks
|
|
57
|
+
|
|
58
|
+
- Parses the `[input]` section in `project.godot`.
|
|
59
|
+
- Classifies `InputEventKey`, mouse, joypad, and screen-touch event classes.
|
|
60
|
+
- Reports actions missing required device families.
|
|
61
|
+
- Reports duplicate normalized bindings across actions.
|
|
62
|
+
- Generates `INPUT_REFERENCE.md`.
|
|
63
|
+
- Generates optional `InputActions` GDScript constants.
|
|
64
|
+
|
|
65
|
+
## Documentation
|
|
66
|
+
|
|
67
|
+
- [Device classification](docs/DEVICE_RULES.md)
|
|
68
|
+
- [Rule reference](docs/RULE_REFERENCE.md)
|
|
69
|
+
- [Generated output](docs/GENERATED_OUTPUT.md)
|
|
70
|
+
- [Touch readiness](docs/TOUCH_READINESS.md)
|
|
71
|
+
- [CI usage](docs/CI.md)
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```powershell
|
|
76
|
+
python -m pip install -e .
|
|
77
|
+
python -m unittest discover -s tests -v
|
|
78
|
+
godot-input-audit examples\tiny-godot-project --require keyboard --fail-on none
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Examples and generated docs use generic action names so the repository can be published safely.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Godot Input Map Auditor
|
|
2
|
+
|
|
3
|
+
Audit Godot input actions for keyboard, mouse, gamepad, and touch readiness, then generate input reference docs and strongly named GDScript constants.
|
|
4
|
+
|
|
5
|
+
This is a small CI-friendly tool for projects where input coverage can drift across desktop, controller, and mobile targets.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
python -m pip install -e .
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
When published:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
python -m pip install godot-input-map-auditor
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
godot-input-audit C:\Projects\MyGame --require keyboard,touch
|
|
23
|
+
godot-input-audit . --write-docs docs\INPUT_REFERENCE.md
|
|
24
|
+
godot-input-audit . --generate-gd scripts\generated\input_actions.gd
|
|
25
|
+
godot-input-audit . --format json --output input-report.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Run against the sample project:
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
godot-input-audit examples\tiny-godot-project --fail-on none
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What It Checks
|
|
35
|
+
|
|
36
|
+
- Parses the `[input]` section in `project.godot`.
|
|
37
|
+
- Classifies `InputEventKey`, mouse, joypad, and screen-touch event classes.
|
|
38
|
+
- Reports actions missing required device families.
|
|
39
|
+
- Reports duplicate normalized bindings across actions.
|
|
40
|
+
- Generates `INPUT_REFERENCE.md`.
|
|
41
|
+
- Generates optional `InputActions` GDScript constants.
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
45
|
+
- [Device classification](docs/DEVICE_RULES.md)
|
|
46
|
+
- [Rule reference](docs/RULE_REFERENCE.md)
|
|
47
|
+
- [Generated output](docs/GENERATED_OUTPUT.md)
|
|
48
|
+
- [Touch readiness](docs/TOUCH_READINESS.md)
|
|
49
|
+
- [CI usage](docs/CI.md)
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```powershell
|
|
54
|
+
python -m pip install -e .
|
|
55
|
+
python -m unittest discover -s tests -v
|
|
56
|
+
godot-input-audit examples\tiny-godot-project --require keyboard --fail-on none
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Examples and generated docs use generic action names so the repository can be published safely.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "godot-input-map-auditor"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Audit Godot input maps for device coverage, duplicate bindings, and generated references."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Godot Input Map Auditor contributors" }]
|
|
13
|
+
keywords = ["godot", "input", "touch", "gamepad", "ci", "gamedev"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
22
|
+
"Topic :: Utilities"
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/NonniGB/godot-production-toolkit/tree/main/godot-input-map-auditor"
|
|
28
|
+
Issues = "https://github.com/NonniGB/godot-production-toolkit/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
godot-input-audit = "godot_input_auditor.cli:entrypoint"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py311"
|
|
39
|
+
|
|
40
|
+
[tool.ruff.lint]
|
|
41
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
|
|
5
|
+
from .models import Finding, InputAction
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def evaluate_actions(actions: list[InputAction], required_devices: set[str]) -> list[Finding]:
|
|
9
|
+
findings: list[Finding] = []
|
|
10
|
+
if not actions:
|
|
11
|
+
return [
|
|
12
|
+
Finding(
|
|
13
|
+
rule_id="input_map_empty",
|
|
14
|
+
severity="warning",
|
|
15
|
+
action=None,
|
|
16
|
+
message="No input actions were found in the [input] section.",
|
|
17
|
+
)
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
for action in actions:
|
|
21
|
+
if not action.events:
|
|
22
|
+
findings.append(
|
|
23
|
+
Finding(
|
|
24
|
+
rule_id="action_has_no_events",
|
|
25
|
+
severity="warning",
|
|
26
|
+
action=action.name,
|
|
27
|
+
message=f"Input action '{action.name}' has no bound events.",
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
missing = sorted(required_devices - action.devices)
|
|
31
|
+
if missing:
|
|
32
|
+
findings.append(
|
|
33
|
+
Finding(
|
|
34
|
+
rule_id="missing_required_device",
|
|
35
|
+
severity="error",
|
|
36
|
+
action=action.name,
|
|
37
|
+
message=f"Input action '{action.name}' is missing required device(s): {', '.join(missing)}.",
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
findings.extend(_duplicate_binding_findings(actions))
|
|
42
|
+
return findings
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _duplicate_binding_findings(actions: list[InputAction]) -> list[Finding]:
|
|
46
|
+
by_signature: dict[str, list[str]] = defaultdict(list)
|
|
47
|
+
for action in actions:
|
|
48
|
+
for event in action.events:
|
|
49
|
+
if event.signature:
|
|
50
|
+
by_signature[event.signature].append(action.name)
|
|
51
|
+
|
|
52
|
+
findings: list[Finding] = []
|
|
53
|
+
for signature, action_names in sorted(by_signature.items()):
|
|
54
|
+
unique_names = sorted(set(action_names))
|
|
55
|
+
if len(unique_names) > 1:
|
|
56
|
+
findings.append(
|
|
57
|
+
Finding(
|
|
58
|
+
rule_id="duplicate_binding",
|
|
59
|
+
severity="warning",
|
|
60
|
+
action=None,
|
|
61
|
+
message=(
|
|
62
|
+
f"Binding '{signature}' is used by multiple actions: "
|
|
63
|
+
f"{', '.join(unique_names)}."
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
return findings
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .audit import evaluate_actions
|
|
8
|
+
from .input_parser import parse_input_map
|
|
9
|
+
from .models import Finding
|
|
10
|
+
from .reporting import (
|
|
11
|
+
render_gdscript_constants,
|
|
12
|
+
render_json_report,
|
|
13
|
+
render_markdown_reference,
|
|
14
|
+
render_sarif_report,
|
|
15
|
+
render_text_report,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main(argv: list[str] | None = None) -> int:
|
|
20
|
+
parser = _build_parser()
|
|
21
|
+
args = parser.parse_args(argv)
|
|
22
|
+
project = Path(args.project)
|
|
23
|
+
project_file = project if project.name == "project.godot" else project / "project.godot"
|
|
24
|
+
|
|
25
|
+
if not project_file.exists():
|
|
26
|
+
actions = []
|
|
27
|
+
findings = [
|
|
28
|
+
Finding(
|
|
29
|
+
rule_id="missing_project_godot",
|
|
30
|
+
severity="error",
|
|
31
|
+
action=None,
|
|
32
|
+
message="project.godot was not found.",
|
|
33
|
+
)
|
|
34
|
+
]
|
|
35
|
+
else:
|
|
36
|
+
actions = parse_input_map(project_file.read_text(encoding="utf-8"))
|
|
37
|
+
findings = evaluate_actions(actions, required_devices=_parse_required(args.require))
|
|
38
|
+
|
|
39
|
+
if args.write_docs:
|
|
40
|
+
path = Path(args.write_docs)
|
|
41
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
path.write_text(render_markdown_reference(actions), encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
if args.generate_gd:
|
|
45
|
+
path = Path(args.generate_gd)
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
path.write_text(render_gdscript_constants(actions), encoding="utf-8")
|
|
48
|
+
|
|
49
|
+
if args.format == "json":
|
|
50
|
+
rendered = render_json_report(actions, findings)
|
|
51
|
+
elif args.format == "sarif":
|
|
52
|
+
rendered = render_sarif_report(actions, findings)
|
|
53
|
+
else:
|
|
54
|
+
rendered = render_text_report(actions, findings)
|
|
55
|
+
if args.output:
|
|
56
|
+
output = Path(args.output)
|
|
57
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
output.write_text(rendered + "\n", encoding="utf-8")
|
|
59
|
+
else:
|
|
60
|
+
print(rendered)
|
|
61
|
+
|
|
62
|
+
return _exit_code(findings, args.fail_on)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def entrypoint() -> None:
|
|
66
|
+
raise SystemExit(main())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
70
|
+
parser = argparse.ArgumentParser(
|
|
71
|
+
prog="godot-input-audit",
|
|
72
|
+
description="Audit Godot input actions for device coverage and duplicate bindings.",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument("--version", action="version", version="godot-input-audit 0.1.0")
|
|
75
|
+
parser.add_argument("project", help="Godot project directory or project.godot file.")
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--require",
|
|
78
|
+
default="",
|
|
79
|
+
help="Comma-separated device families every action should support, e.g. keyboard,touch.",
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument("--format", choices=["text", "json", "sarif"], default="text")
|
|
82
|
+
parser.add_argument("--output", help="Write report to a file instead of stdout.")
|
|
83
|
+
parser.add_argument("--write-docs", help="Write Markdown input reference.")
|
|
84
|
+
parser.add_argument("--generate-gd", help="Write GDScript constants file.")
|
|
85
|
+
parser.add_argument("--fail-on", choices=["warning", "error", "none"], default="warning")
|
|
86
|
+
return parser
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_required(raw: str) -> set[str]:
|
|
90
|
+
return {part.strip() for part in raw.split(",") if part.strip()}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _exit_code(findings: list[Finding], fail_on: str) -> int:
|
|
94
|
+
if fail_on == "none":
|
|
95
|
+
return 0
|
|
96
|
+
severities = {finding.severity for finding in findings}
|
|
97
|
+
if fail_on == "error":
|
|
98
|
+
return 1 if "error" in severities else 0
|
|
99
|
+
return 1 if ("error" in severities or "warning" in severities) else 0
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
sys.exit(main())
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from .models import InputAction, InputEvent
|
|
6
|
+
|
|
7
|
+
SECTION_RE = re.compile(r"^\[([^\]]+)\]$")
|
|
8
|
+
EVENT_RE = re.compile(r"Object\((InputEvent[A-Za-z0-9_]+)(.*?)\)", re.DOTALL)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_input_map(project_godot_content: str) -> list[InputAction]:
|
|
12
|
+
actions: list[InputAction] = []
|
|
13
|
+
in_input_section = False
|
|
14
|
+
current_name: str | None = None
|
|
15
|
+
current_lines: list[str] = []
|
|
16
|
+
brace_depth = 0
|
|
17
|
+
|
|
18
|
+
for raw_line in project_godot_content.splitlines():
|
|
19
|
+
line = raw_line.strip()
|
|
20
|
+
section = SECTION_RE.match(line)
|
|
21
|
+
if section:
|
|
22
|
+
if current_name is not None:
|
|
23
|
+
actions.append(_parse_action(current_name, "\n".join(current_lines)))
|
|
24
|
+
current_name = None
|
|
25
|
+
current_lines = []
|
|
26
|
+
brace_depth = 0
|
|
27
|
+
in_input_section = section.group(1) == "input"
|
|
28
|
+
continue
|
|
29
|
+
|
|
30
|
+
if not in_input_section or not line or line.startswith(";") or line.startswith("#"):
|
|
31
|
+
continue
|
|
32
|
+
|
|
33
|
+
if current_name is None:
|
|
34
|
+
if "=" not in line:
|
|
35
|
+
continue
|
|
36
|
+
name, rest = line.split("=", 1)
|
|
37
|
+
current_name = name.strip().strip('"')
|
|
38
|
+
current_lines = [rest]
|
|
39
|
+
brace_depth = rest.count("{") - rest.count("}")
|
|
40
|
+
if brace_depth <= 0:
|
|
41
|
+
actions.append(_parse_action(current_name, "\n".join(current_lines)))
|
|
42
|
+
current_name = None
|
|
43
|
+
current_lines = []
|
|
44
|
+
continue
|
|
45
|
+
|
|
46
|
+
current_lines.append(line)
|
|
47
|
+
brace_depth += line.count("{") - line.count("}")
|
|
48
|
+
if brace_depth <= 0:
|
|
49
|
+
actions.append(_parse_action(current_name, "\n".join(current_lines)))
|
|
50
|
+
current_name = None
|
|
51
|
+
current_lines = []
|
|
52
|
+
brace_depth = 0
|
|
53
|
+
|
|
54
|
+
if current_name is not None:
|
|
55
|
+
actions.append(_parse_action(current_name, "\n".join(current_lines)))
|
|
56
|
+
|
|
57
|
+
return actions
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_action(name: str, body: str) -> InputAction:
|
|
61
|
+
events = []
|
|
62
|
+
for match in EVENT_RE.finditer(body):
|
|
63
|
+
event_type = match.group(1)
|
|
64
|
+
event_body = match.group(2)
|
|
65
|
+
device = _classify_event(event_type)
|
|
66
|
+
events.append(InputEvent(event_type=event_type, device=device, signature=_signature(event_type, event_body)))
|
|
67
|
+
return InputAction(name=name, events=events)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _classify_event(event_type: str) -> str:
|
|
71
|
+
if event_type == "InputEventKey":
|
|
72
|
+
return "keyboard"
|
|
73
|
+
if event_type in {"InputEventMouseButton", "InputEventMouseMotion"}:
|
|
74
|
+
return "mouse"
|
|
75
|
+
if event_type in {"InputEventJoypadButton", "InputEventJoypadMotion"}:
|
|
76
|
+
return "gamepad"
|
|
77
|
+
if event_type in {"InputEventScreenTouch", "InputEventScreenDrag", "InputEventGesture"}:
|
|
78
|
+
return "touch"
|
|
79
|
+
return "unknown"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _signature(event_type: str, event_body: str) -> str:
|
|
83
|
+
if event_type == "InputEventKey":
|
|
84
|
+
return f"keyboard:key:{_field(event_body, 'physical_keycode') or _field(event_body, 'keycode')}"
|
|
85
|
+
if event_type == "InputEventMouseButton":
|
|
86
|
+
return f"mouse:button:{_field(event_body, 'button_index')}"
|
|
87
|
+
if event_type == "InputEventJoypadButton":
|
|
88
|
+
return f"gamepad:button:{_field(event_body, 'button_index')}"
|
|
89
|
+
if event_type == "InputEventJoypadMotion":
|
|
90
|
+
return f"gamepad:axis:{_field(event_body, 'axis')}"
|
|
91
|
+
if event_type in {"InputEventScreenTouch", "InputEventScreenDrag", "InputEventGesture"}:
|
|
92
|
+
return f"touch:index:{_field(event_body, 'index') or 'any'}"
|
|
93
|
+
return f"{event_type}:{' '.join(event_body.split())}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _field(event_body: str, name: str) -> str:
|
|
97
|
+
match = re.search(rf'"{re.escape(name)}"\s*:\s*([^,\)]+)', event_body)
|
|
98
|
+
if not match:
|
|
99
|
+
return ""
|
|
100
|
+
return match.group(1).strip().strip('"')
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class InputEvent:
|
|
8
|
+
event_type: str
|
|
9
|
+
device: str
|
|
10
|
+
signature: str
|
|
11
|
+
|
|
12
|
+
def to_dict(self) -> dict[str, str]:
|
|
13
|
+
return {"event_type": self.event_type, "device": self.device, "signature": self.signature}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class InputAction:
|
|
18
|
+
name: str
|
|
19
|
+
events: list[InputEvent] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def devices(self) -> set[str]:
|
|
23
|
+
return {event.device for event in self.events if event.device != "unknown"}
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> dict[str, object]:
|
|
26
|
+
return {
|
|
27
|
+
"name": self.name,
|
|
28
|
+
"devices": sorted(self.devices),
|
|
29
|
+
"events": [event.to_dict() for event in self.events],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Finding:
|
|
35
|
+
rule_id: str
|
|
36
|
+
severity: str
|
|
37
|
+
action: str | None
|
|
38
|
+
message: str
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict[str, object]:
|
|
41
|
+
return {
|
|
42
|
+
"rule_id": self.rule_id,
|
|
43
|
+
"severity": self.severity,
|
|
44
|
+
"action": self.action,
|
|
45
|
+
"message": self.message,
|
|
46
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from .models import Finding, InputAction
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def render_text_report(actions: list[InputAction], findings: list[Finding]) -> str:
|
|
10
|
+
lines = [
|
|
11
|
+
"Godot Input Map Auditor",
|
|
12
|
+
f"Scanned {len(actions)} action(s): {len(findings)} finding(s).",
|
|
13
|
+
]
|
|
14
|
+
for action in actions:
|
|
15
|
+
devices = ", ".join(sorted(action.devices)) or "none"
|
|
16
|
+
lines.append(f"- {action.name}: {devices} ({len(action.events)} event(s))")
|
|
17
|
+
if findings:
|
|
18
|
+
for finding in findings:
|
|
19
|
+
lines.append(f"[{finding.severity.upper()}] {finding.rule_id}: {finding.message}")
|
|
20
|
+
else:
|
|
21
|
+
lines.append("No findings.")
|
|
22
|
+
return "\n".join(lines)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def render_json_report(actions: list[InputAction], findings: list[Finding]) -> str:
|
|
26
|
+
payload = {
|
|
27
|
+
"tool": "godot-input-map-auditor",
|
|
28
|
+
"summary": {
|
|
29
|
+
"actions": len(actions),
|
|
30
|
+
"findings": len(findings),
|
|
31
|
+
"errors": sum(1 for finding in findings if finding.severity == "error"),
|
|
32
|
+
"warnings": sum(1 for finding in findings if finding.severity == "warning"),
|
|
33
|
+
},
|
|
34
|
+
"actions": [action.to_dict() for action in actions],
|
|
35
|
+
"findings": [finding.to_dict() for finding in findings],
|
|
36
|
+
}
|
|
37
|
+
return json.dumps(payload, indent=2, sort_keys=True)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def render_sarif_report(actions: list[InputAction], findings: list[Finding]) -> str:
|
|
41
|
+
rules = {}
|
|
42
|
+
results = []
|
|
43
|
+
for finding in findings:
|
|
44
|
+
rules.setdefault(
|
|
45
|
+
finding.rule_id,
|
|
46
|
+
{
|
|
47
|
+
"id": finding.rule_id,
|
|
48
|
+
"name": finding.rule_id,
|
|
49
|
+
"shortDescription": {"text": finding.message},
|
|
50
|
+
},
|
|
51
|
+
)
|
|
52
|
+
results.append(
|
|
53
|
+
{
|
|
54
|
+
"ruleId": finding.rule_id,
|
|
55
|
+
"level": _sarif_level(finding.severity),
|
|
56
|
+
"message": {"text": finding.message},
|
|
57
|
+
"locations": [
|
|
58
|
+
{
|
|
59
|
+
"physicalLocation": {"artifactLocation": {"uri": "project.godot"}},
|
|
60
|
+
"logicalLocations": [{"name": finding.action or "input"}],
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
payload = {
|
|
66
|
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
67
|
+
"version": "2.1.0",
|
|
68
|
+
"runs": [
|
|
69
|
+
{
|
|
70
|
+
"tool": {
|
|
71
|
+
"driver": {
|
|
72
|
+
"name": "godot-input-map-auditor",
|
|
73
|
+
"rules": list(rules.values()),
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"results": results,
|
|
77
|
+
}
|
|
78
|
+
],
|
|
79
|
+
}
|
|
80
|
+
return json.dumps(payload, indent=2, sort_keys=True)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def render_markdown_reference(actions: list[InputAction]) -> str:
|
|
84
|
+
lines = [
|
|
85
|
+
"# Input Reference",
|
|
86
|
+
"",
|
|
87
|
+
"| Action | Devices | Events |",
|
|
88
|
+
"|---|---|---:|",
|
|
89
|
+
]
|
|
90
|
+
for action in sorted(actions, key=lambda item: item.name):
|
|
91
|
+
devices = ", ".join(sorted(action.devices)) or "none"
|
|
92
|
+
lines.append(f"| {action.name} | {devices} | {len(action.events)} |")
|
|
93
|
+
if not actions:
|
|
94
|
+
lines.append("| none | none | 0 |")
|
|
95
|
+
lines.append("")
|
|
96
|
+
return "\n".join(lines)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def render_gdscript_constants(actions: list[InputAction]) -> str:
|
|
100
|
+
lines = [
|
|
101
|
+
"class_name InputActions",
|
|
102
|
+
"",
|
|
103
|
+
"# Generated by godot-input-map-auditor.",
|
|
104
|
+
]
|
|
105
|
+
for action in sorted(actions, key=lambda item: item.name):
|
|
106
|
+
lines.append(f'const {_constant_name(action.name)} = "{action.name}"')
|
|
107
|
+
lines.append("")
|
|
108
|
+
return "\n".join(lines)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _constant_name(action: str) -> str:
|
|
112
|
+
value = re.sub(r"[^A-Za-z0-9]+", "_", action).strip("_").upper()
|
|
113
|
+
if not value:
|
|
114
|
+
return "ACTION"
|
|
115
|
+
if value[0].isdigit():
|
|
116
|
+
return f"ACTION_{value}"
|
|
117
|
+
return value
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _sarif_level(severity: str) -> str:
|
|
121
|
+
if severity == "error":
|
|
122
|
+
return "error"
|
|
123
|
+
if severity == "warning":
|
|
124
|
+
return "warning"
|
|
125
|
+
return "note"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: godot-input-map-auditor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Audit Godot input maps for device coverage, duplicate bindings, and generated references.
|
|
5
|
+
Author: Godot Input Map Auditor contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/NonniGB/godot-production-toolkit/tree/main/godot-input-map-auditor
|
|
8
|
+
Project-URL: Issues, https://github.com/NonniGB/godot-production-toolkit/issues
|
|
9
|
+
Keywords: godot,input,touch,gamepad,ci,gamedev
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# Godot Input Map Auditor
|
|
24
|
+
|
|
25
|
+
Audit Godot input actions for keyboard, mouse, gamepad, and touch readiness, then generate input reference docs and strongly named GDScript constants.
|
|
26
|
+
|
|
27
|
+
This is a small CI-friendly tool for projects where input coverage can drift across desktop, controller, and mobile targets.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
python -m pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
When published:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
python -m pip install godot-input-map-auditor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
godot-input-audit C:\Projects\MyGame --require keyboard,touch
|
|
45
|
+
godot-input-audit . --write-docs docs\INPUT_REFERENCE.md
|
|
46
|
+
godot-input-audit . --generate-gd scripts\generated\input_actions.gd
|
|
47
|
+
godot-input-audit . --format json --output input-report.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Run against the sample project:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
godot-input-audit examples\tiny-godot-project --fail-on none
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## What It Checks
|
|
57
|
+
|
|
58
|
+
- Parses the `[input]` section in `project.godot`.
|
|
59
|
+
- Classifies `InputEventKey`, mouse, joypad, and screen-touch event classes.
|
|
60
|
+
- Reports actions missing required device families.
|
|
61
|
+
- Reports duplicate normalized bindings across actions.
|
|
62
|
+
- Generates `INPUT_REFERENCE.md`.
|
|
63
|
+
- Generates optional `InputActions` GDScript constants.
|
|
64
|
+
|
|
65
|
+
## Documentation
|
|
66
|
+
|
|
67
|
+
- [Device classification](docs/DEVICE_RULES.md)
|
|
68
|
+
- [Rule reference](docs/RULE_REFERENCE.md)
|
|
69
|
+
- [Generated output](docs/GENERATED_OUTPUT.md)
|
|
70
|
+
- [Touch readiness](docs/TOUCH_READINESS.md)
|
|
71
|
+
- [CI usage](docs/CI.md)
|
|
72
|
+
|
|
73
|
+
## Development
|
|
74
|
+
|
|
75
|
+
```powershell
|
|
76
|
+
python -m pip install -e .
|
|
77
|
+
python -m unittest discover -s tests -v
|
|
78
|
+
godot-input-audit examples\tiny-godot-project --require keyboard --fail-on none
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Examples and generated docs use generic action names so the repository can be published safely.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/godot_input_auditor/__init__.py
|
|
5
|
+
src/godot_input_auditor/__main__.py
|
|
6
|
+
src/godot_input_auditor/audit.py
|
|
7
|
+
src/godot_input_auditor/cli.py
|
|
8
|
+
src/godot_input_auditor/input_parser.py
|
|
9
|
+
src/godot_input_auditor/models.py
|
|
10
|
+
src/godot_input_auditor/reporting.py
|
|
11
|
+
src/godot_input_map_auditor.egg-info/PKG-INFO
|
|
12
|
+
src/godot_input_map_auditor.egg-info/SOURCES.txt
|
|
13
|
+
src/godot_input_map_auditor.egg-info/dependency_links.txt
|
|
14
|
+
src/godot_input_map_auditor.egg-info/entry_points.txt
|
|
15
|
+
src/godot_input_map_auditor.egg-info/top_level.txt
|
|
16
|
+
tests/test_audit.py
|
|
17
|
+
tests/test_cli.py
|
|
18
|
+
tests/test_parser.py
|
|
19
|
+
tests/test_reporting.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
godot_input_auditor
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from godot_input_auditor.audit import evaluate_actions
|
|
4
|
+
from godot_input_auditor.models import InputAction, InputEvent
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuditTests(unittest.TestCase):
|
|
8
|
+
def test_missing_required_devices_are_reported(self) -> None:
|
|
9
|
+
action = InputAction(
|
|
10
|
+
name="confirm",
|
|
11
|
+
events=[InputEvent(event_type="InputEventKey", device="keyboard", signature="key:13")],
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
findings = evaluate_actions([action], required_devices={"keyboard", "touch"})
|
|
15
|
+
|
|
16
|
+
self.assertEqual(findings[0].rule_id, "missing_required_device")
|
|
17
|
+
self.assertIn("touch", findings[0].message)
|
|
18
|
+
|
|
19
|
+
def test_duplicate_bindings_are_reported(self) -> None:
|
|
20
|
+
actions = [
|
|
21
|
+
InputAction(
|
|
22
|
+
name="move_left",
|
|
23
|
+
events=[InputEvent(event_type="InputEventKey", device="keyboard", signature="key:65")],
|
|
24
|
+
),
|
|
25
|
+
InputAction(
|
|
26
|
+
name="menu_left",
|
|
27
|
+
events=[InputEvent(event_type="InputEventKey", device="keyboard", signature="key:65")],
|
|
28
|
+
),
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
findings = evaluate_actions(actions, required_devices=set())
|
|
32
|
+
|
|
33
|
+
self.assertEqual(len(findings), 1)
|
|
34
|
+
self.assertEqual(findings[0].rule_id, "duplicate_binding")
|
|
35
|
+
self.assertIn("move_left", findings[0].message)
|
|
36
|
+
self.assertIn("menu_left", findings[0].message)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
unittest.main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from contextlib import redirect_stdout
|
|
2
|
+
from io import StringIO
|
|
3
|
+
import tempfile
|
|
4
|
+
import unittest
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from godot_input_auditor.cli import main
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CliTests(unittest.TestCase):
|
|
12
|
+
def test_cli_prints_version(self) -> None:
|
|
13
|
+
stdout = StringIO()
|
|
14
|
+
|
|
15
|
+
with self.assertRaises(SystemExit) as raised:
|
|
16
|
+
with redirect_stdout(stdout):
|
|
17
|
+
main(["--version"])
|
|
18
|
+
|
|
19
|
+
self.assertEqual(raised.exception.code, 0)
|
|
20
|
+
self.assertIn("godot-input-audit 0.1.0", stdout.getvalue())
|
|
21
|
+
|
|
22
|
+
def test_cli_generates_docs_and_constants(self) -> None:
|
|
23
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
24
|
+
project = Path(tmp)
|
|
25
|
+
(project / "project.godot").write_text(
|
|
26
|
+
"""
|
|
27
|
+
[input]
|
|
28
|
+
confirm={
|
|
29
|
+
"events": [Object(InputEventKey,"physical_keycode":13)]
|
|
30
|
+
}
|
|
31
|
+
""",
|
|
32
|
+
encoding="utf-8",
|
|
33
|
+
)
|
|
34
|
+
docs = project / "docs" / "INPUT_REFERENCE.md"
|
|
35
|
+
constants = project / "scripts" / "generated" / "input_actions.gd"
|
|
36
|
+
|
|
37
|
+
exit_code = main(
|
|
38
|
+
[
|
|
39
|
+
str(project),
|
|
40
|
+
"--require",
|
|
41
|
+
"keyboard,touch",
|
|
42
|
+
"--write-docs",
|
|
43
|
+
str(docs),
|
|
44
|
+
"--generate-gd",
|
|
45
|
+
str(constants),
|
|
46
|
+
]
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
self.assertEqual(exit_code, 1)
|
|
50
|
+
self.assertIn("confirm", docs.read_text(encoding="utf-8"))
|
|
51
|
+
self.assertIn('CONFIRM = "confirm"', constants.read_text(encoding="utf-8"))
|
|
52
|
+
|
|
53
|
+
def test_cli_outputs_sarif_report(self) -> None:
|
|
54
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
55
|
+
project = Path(tmp)
|
|
56
|
+
(project / "project.godot").write_text(
|
|
57
|
+
"""
|
|
58
|
+
[input]
|
|
59
|
+
confirm={
|
|
60
|
+
"events": [Object(InputEventKey,"physical_keycode":13)]
|
|
61
|
+
}
|
|
62
|
+
""",
|
|
63
|
+
encoding="utf-8",
|
|
64
|
+
)
|
|
65
|
+
stdout = StringIO()
|
|
66
|
+
|
|
67
|
+
with redirect_stdout(stdout):
|
|
68
|
+
exit_code = main([str(project), "--require", "keyboard,touch", "--format", "sarif", "--fail-on", "none"])
|
|
69
|
+
|
|
70
|
+
sarif = json.loads(stdout.getvalue())
|
|
71
|
+
self.assertEqual(exit_code, 0)
|
|
72
|
+
self.assertEqual(sarif["version"], "2.1.0")
|
|
73
|
+
driver = sarif["runs"][0]["tool"]["driver"]
|
|
74
|
+
self.assertEqual(driver["name"], "godot-input-map-auditor")
|
|
75
|
+
self.assertTrue(driver["rules"])
|
|
76
|
+
self.assertTrue(sarif["runs"][0]["results"])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
unittest.main()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from godot_input_auditor.input_parser import parse_input_map
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
PROJECT = """
|
|
7
|
+
config_version=5
|
|
8
|
+
|
|
9
|
+
[input]
|
|
10
|
+
move_left={
|
|
11
|
+
"deadzone": 0.5,
|
|
12
|
+
"events": [Object(InputEventKey,"physical_keycode":65), Object(InputEventJoypadButton,"button_index":14)]
|
|
13
|
+
}
|
|
14
|
+
tap_select={
|
|
15
|
+
"deadzone": 0.5,
|
|
16
|
+
"events": [Object(InputEventScreenTouch,"index":0)]
|
|
17
|
+
}
|
|
18
|
+
zoom={
|
|
19
|
+
"deadzone": 0.5,
|
|
20
|
+
"events": [Object(InputEventMouseButton,"button_index":4)]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
[rendering]
|
|
24
|
+
renderer/rendering_method="mobile"
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ParserTests(unittest.TestCase):
|
|
29
|
+
def test_parses_actions_and_classifies_device_families(self) -> None:
|
|
30
|
+
actions = parse_input_map(PROJECT)
|
|
31
|
+
by_name = {action.name: action for action in actions}
|
|
32
|
+
|
|
33
|
+
self.assertEqual(set(by_name), {"move_left", "tap_select", "zoom"})
|
|
34
|
+
self.assertEqual(by_name["move_left"].devices, {"keyboard", "gamepad"})
|
|
35
|
+
self.assertEqual(by_name["tap_select"].devices, {"touch"})
|
|
36
|
+
self.assertEqual(by_name["zoom"].devices, {"mouse"})
|
|
37
|
+
|
|
38
|
+
def test_returns_empty_list_when_input_section_missing(self) -> None:
|
|
39
|
+
self.assertEqual(parse_input_map("config_version=5\n"), [])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
unittest.main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from godot_input_auditor.models import InputAction, InputEvent
|
|
5
|
+
from godot_input_auditor.reporting import (
|
|
6
|
+
render_gdscript_constants,
|
|
7
|
+
render_json_report,
|
|
8
|
+
render_markdown_reference,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ReportingTests(unittest.TestCase):
|
|
13
|
+
def test_markdown_reference_lists_actions_and_devices(self) -> None:
|
|
14
|
+
action = InputAction(
|
|
15
|
+
name="move_left",
|
|
16
|
+
events=[InputEvent(event_type="InputEventKey", device="keyboard", signature="key:65")],
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
markdown = render_markdown_reference([action])
|
|
20
|
+
|
|
21
|
+
self.assertIn("# Input Reference", markdown)
|
|
22
|
+
self.assertIn("| move_left | keyboard | 1 |", markdown)
|
|
23
|
+
|
|
24
|
+
def test_gdscript_constants_are_generated(self) -> None:
|
|
25
|
+
actions = [InputAction(name="move_left", events=[]), InputAction(name="ui_accept", events=[])]
|
|
26
|
+
|
|
27
|
+
gdscript = render_gdscript_constants(actions)
|
|
28
|
+
|
|
29
|
+
self.assertIn('const MOVE_LEFT = "move_left"', gdscript)
|
|
30
|
+
self.assertIn('const UI_ACCEPT = "ui_accept"', gdscript)
|
|
31
|
+
|
|
32
|
+
def test_json_report_contains_summary(self) -> None:
|
|
33
|
+
action = InputAction(name="tap", events=[InputEvent("InputEventScreenTouch", "touch", "touch:0")])
|
|
34
|
+
|
|
35
|
+
report = json.loads(render_json_report([action], []))
|
|
36
|
+
|
|
37
|
+
self.assertEqual(report["summary"]["actions"], 1)
|
|
38
|
+
self.assertEqual(report["actions"][0]["devices"], ["touch"])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
unittest.main()
|