Files
OSGKeyboard/Scripts/resolve_test_suite.py
2026-08-25 13:42:00 +08:00

249 lines
7.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Resolve OSGKeyboard test suite groups/presets from Tests/suite-manifest.json.
Usage:
resolve_test_suite.py list
resolve_test_suite.py resolve <name> [<name> ...]
resolve_test_suite.py validate
resolve_test_suite.py xcodebuild-args <name> [<name> ...]
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MANIFEST_PATH = ROOT / "Tests" / "suite-manifest.json"
TEST_ROOTS = [
ROOT / "OSGKeyboardTests",
ROOT / "OSGKeyboardExtTests",
ROOT / "OSGKeyboardMacTests",
]
SHARED_TEST_ROOT = ROOT / "Tests"
SHARED_TEST_TARGETS = ["OSGKeyboardTests", "OSGKeyboardMacTests"]
XCTEST_CLASS_PATTERN = re.compile(
r"^[ \t]*(?:(?:@\w+(?:\([^)]*\))?)[ \t]+)*"
r"(?:final[ \t]+)?class[ \t]+([A-Za-z_]\w*)"
r"[ \t]*:[ \t]*XCTestCase\b",
re.MULTILINE,
)
def load_manifest() -> dict:
with MANIFEST_PATH.open(encoding="utf-8") as fh:
return json.load(fh)
def known_names(manifest: dict) -> set[str]:
return set(manifest["groups"]) | set(manifest["presets"])
def expand_names(manifest: dict, names: list[str]) -> list[str]:
"""Expand presets/groups into a de-duplicated ordered list of atomic groups."""
groups = manifest["groups"]
presets = manifest["presets"]
ordered: list[str] = []
seen: set[str] = set()
def add_group(group_id: str) -> None:
if group_id not in groups:
raise SystemExit(f"unknown group: {group_id}")
if group_id in seen:
return
seen.add(group_id)
ordered.append(group_id)
for name in names:
if name in presets:
for group_id in presets[name]["groups"]:
add_group(group_id)
elif name in groups:
add_group(name)
else:
known = ", ".join(sorted(known_names(manifest)))
raise SystemExit(f"unknown preset/group: {name}\nKnown: {known}")
return ordered
def collect_tests(manifest: dict, group_ids: list[str]) -> list[str]:
tests: list[str] = []
seen: set[str] = set()
for group_id in group_ids:
for test_id in manifest["groups"][group_id]["tests"]:
if test_id in seen:
raise SystemExit(
f"duplicate test id across selected groups: {test_id}"
)
seen.add(test_id)
tests.append(test_id)
return tests
def discover_xctest_classes(path: Path) -> list[str]:
"""Return every XCTestCase class declared in a Swift test source file."""
return XCTEST_CLASS_PATTERN.findall(path.read_text(encoding="utf-8"))
def discover_on_disk_test_classes() -> dict[str, Path]:
"""Map Target/ClassName -> swift path for *Tests.swift files (exclude helpers)."""
found: dict[str, Path] = {}
for root in TEST_ROOTS:
if not root.is_dir():
continue
target = root.name
for path in sorted(root.glob("*Tests.swift")):
# Skip non-XCTest helpers that happen to end with Tests (none today).
for class_name in discover_xctest_classes(path):
test_id = f"{target}/{class_name}"
found[test_id] = path
if SHARED_TEST_ROOT.is_dir():
for path in sorted(SHARED_TEST_ROOT.glob("*Tests.swift")):
for class_name in discover_xctest_classes(path):
for target in SHARED_TEST_TARGETS:
test_id = f"{target}/{class_name}"
found[test_id] = path
return found
def validate(manifest: dict) -> int:
errors: list[str] = []
warnings: list[str] = []
# Each class appears in at most one group.
ownership: dict[str, str] = {}
for group_id, group in manifest["groups"].items():
for test_id in group["tests"]:
if test_id in ownership:
errors.append(
f"duplicate membership: {test_id} in "
f"{ownership[test_id]} and {group_id}"
)
else:
ownership[test_id] = group_id
on_disk = discover_on_disk_test_classes()
for test_id in sorted(on_disk):
if test_id not in ownership:
errors.append(f"on-disk test class missing from manifest: {test_id}")
for test_id in sorted(ownership):
if test_id not in on_disk:
errors.append(f"manifest lists missing test class: {test_id}")
# Presets must only reference known groups; no nested presets.
for preset_id, preset in manifest["presets"].items():
for group_id in preset["groups"]:
if group_id not in manifest["groups"]:
errors.append(
f"preset {preset_id} references unknown group: {group_id}"
)
# live_api may be empty by design.
if not manifest["groups"]["live_api"]["tests"]:
warnings.append("live_api group is empty (placeholder for future live smoke)")
for warning in warnings:
print(f"warning: {warning}", file=sys.stderr)
if errors:
for err in errors:
print(f"error: {err}", file=sys.stderr)
return 1
print(
f"OK: {len(ownership)} test classes across "
f"{len(manifest['groups'])} groups / {len(manifest['presets'])} presets"
)
return 0
def print_list(manifest: dict) -> None:
print("Presets:")
for preset_id, preset in manifest["presets"].items():
groups = ", ".join(preset["groups"])
print(f" {preset_id:12} {preset['description']}")
print(f" → {groups}")
print("\nAtomic groups:")
for group_id, group in manifest["groups"].items():
count = len(group["tests"])
print(
f" {group_id:12} [{group['platform']}] "
f"{count} class(es) — {group['description']}"
)
def split_by_platform(
manifest: dict, group_ids: list[str]
) -> tuple[list[str], list[str]]:
ios: list[str] = []
mac: list[str] = []
for group_id in group_ids:
platform = manifest["groups"][group_id]["platform"]
tests = manifest["groups"][group_id]["tests"]
if platform == "mac":
mac.extend(tests)
else:
ios.extend(tests)
return ios, mac
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("list", help="List presets and groups")
sub.add_parser("validate", help="Validate manifest vs on-disk *Tests.swift")
resolve_p = sub.add_parser(
"resolve", help="Print expanded group ids and test identifiers"
)
resolve_p.add_argument("names", nargs="+")
xcode_p = sub.add_parser(
"xcodebuild-args",
help="Print JSON with ios/mac -only-testing lists for the shell runner",
)
xcode_p.add_argument("names", nargs="+")
args = parser.parse_args()
manifest = load_manifest()
if args.cmd == "list":
print_list(manifest)
return 0
if args.cmd == "validate":
return validate(manifest)
group_ids = expand_names(manifest, args.names)
tests = collect_tests(manifest, group_ids)
if args.cmd == "resolve":
print("groups:", " ".join(group_ids) if group_ids else "(none)")
for test_id in tests:
print(test_id)
return 0
if args.cmd == "xcodebuild-args":
ios_tests, mac_tests = split_by_platform(manifest, group_ids)
payload = {
"groups": group_ids,
"defaults": manifest["defaults"],
"ios_tests": ios_tests,
"mac_tests": mac_tests,
}
json.dump(payload, sys.stdout, indent=2)
print()
return 0
return 1
if __name__ == "__main__":
sys.exit(main())