add justfile as primary task runner, slim Makefile to CI-only. add styled argparse help formatter, parallel test execution (-j8 default), argparse for test-apps.py. release.py accepts --dryrun alias

This commit is contained in:
Richard Macias
2026-02-27 18:13:17 -06:00
parent cd20e372dc
commit 1b003007ea
6 changed files with 287 additions and 102 deletions

View File

@@ -1,25 +1,23 @@
#!/usr/bin/env python3
"""Live validation that app configs can resolve to downloadable APKs.
Usage:
python scripts/test-apps.py src/applications.json
python scripts/test-apps.py src/applications.json Dolphin
python scripts/test-apps.py src/applications.json --id org.dolphinemu.dolphinemu
Set GITHUB_TOKEN in .env or environment to avoid API rate limits.
"""
import argparse
import json
import os
import re
import ssl
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from html.parser import HTMLParser
from typing import Any
from urllib.parse import urljoin, urlparse
from urllib.request import Request, urlopen
from help_formatter import StyledHelpFormatter
from utils import load_dotenv
USER_AGENT = (
@@ -560,39 +558,49 @@ def print_result(
def main() -> int:
load_dotenv()
if len(sys.argv) < 2:
print("Usage: python test-apps.py <json_file> [name_filter] [--id <app_id>] [--verbose] [--apks]")
print()
print("Examples:")
print(" python test-apps.py src/applications.json # test all apps")
print(" python test-apps.py src/applications.json Dolphin # filter by name")
print(" python test-apps.py src/applications.json --id org.dolphinemu.dolphinemu")
print(" python test-apps.py src/applications.json --verbose # show APK URLs")
print(" python test-apps.py src/applications.json --apks # show numbered APK list")
return 1
parser = argparse.ArgumentParser(
description="Live-test app configs resolve to downloadable APKs.",
formatter_class=StyledHelpFormatter,
)
parser.add_argument(
"name",
nargs="?",
help="Filter by app name (case-insensitive substring match)",
)
parser.add_argument(
"-f", "--file",
default="src/applications.json",
help="Path to applications.json (default: src/applications.json)",
)
parser.add_argument(
"--id",
dest="id_filter",
help="Filter by exact app ID",
)
parser.add_argument(
"--verbose",
action="store_true",
help="Show APK download URLs",
)
parser.add_argument(
"--apks",
action="store_true",
help="Show numbered APK list with preferredApkIndex marker",
)
parser.add_argument(
"-j", "--jobs",
type=int,
default=8,
help="Number of parallel workers (default: 8, use 1 for serial)",
)
args = parser.parse_args()
json_file = sys.argv[1]
args = sys.argv[2:]
verbose = "--verbose" in args
if verbose:
args.remove("--verbose")
show_apks = "--apks" in args
if show_apks:
args.remove("--apks")
id_filter = None
if "--id" in args:
idx = args.index("--id")
if idx + 1 < len(args):
id_filter = args[idx + 1]
args = args[:idx] + args[idx + 2:]
else:
print("Error: --id requires an argument")
return 1
name_filter = " ".join(args).lower() if args else None
json_file = args.file
name_filter = args.name.lower() if args.name else None
id_filter = args.id_filter
verbose = args.verbose
show_apks = args.apks
workers = max(args.jobs, 1)
try:
with open(json_file, "r", encoding="utf-8") as f:
@@ -620,22 +628,39 @@ def main() -> int:
" Set it with: export GITHUB_TOKEN=<your_token>\n"
)
print(f"Testing {len(apps)} app(s)...\n")
serial = workers == 1 or len(apps) == 1
mode = "serial" if serial else f"{workers} workers"
print(f"Testing {len(apps)} app(s) ({mode})...\n")
results = []
for app in apps:
result = test_app(app)
results.append(result)
print_result(result, verbose=verbose, show_apks=show_apks)
wall_start = time.monotonic()
if serial:
results = []
for app in apps:
result = test_app(app)
results.append(result)
print_result(result, verbose=verbose, show_apks=show_apks)
else:
result_map: dict[str, TestResult] = {}
with ThreadPoolExecutor(max_workers=workers) as pool:
futures = {pool.submit(test_app, app): app for app in apps}
for future in as_completed(futures):
result = future.result()
result_map[result.app_id] = result
# Print in original order
results = [result_map[app["id"]] for app in apps]
for result in results:
print_result(result, verbose=verbose, show_apks=show_apks)
wall_ms = int((time.monotonic() - wall_start) * 1000)
passed = sum(1 for r in results if r.passed)
failed = sum(1 for r in results if not r.passed)
warned = sum(1 for r in results if r.warnings)
total_time = sum(r.duration_ms for r in results)
sum_time = sum(r.duration_ms for r in results)
print(f"\n{'=' * 60}")
print(f"Results: {passed} passed, {failed} failed, {warned} with warnings")
print(f"Time: {total_time / 1000:.1f}s total")
print(f"Time: {wall_ms / 1000:.1f}s wall, {sum_time / 1000:.1f}s cumulative")
if failed > 0:
print(f"\nFailed apps:")