初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
@@ -0,0 +1,348 @@
from __future__ import annotations
import argparse
import hashlib
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Sequence
HARNESS_DIR = Path(__file__).resolve().parent
WORKSPACE_ROOT = HARNESS_DIR.parents[2]
SOURCE_DIR = WORKSPACE_ROOT / "核桃派软件源代码"
VENV_DIR = HARNESS_DIR / ".venv"
WORK_DIR = HARNESS_DIR / "work"
DATA_DIR = HARNESS_DIR / "data"
VENV_PYTHON = VENV_DIR / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
REQUIREMENTS_STAMP = VENV_DIR / ".requirements.sha256"
REQUIREMENTS_FILES = (
SOURCE_DIR / "requirements.txt",
SOURCE_DIR / "requirements-dev.txt",
)
PYTEST_TARGETS = {
"config": (
"tests/test_config_store.py",
"tests/test_wifi.py",
"tests/test_templates.py",
"tests/test_animations.py",
"tests/test_animation_preview_fixture.py",
"tests/test_migrate_state_root.py",
),
"display": ("tests/test_display_service.py",),
"api": ("tests/test_api.py", "tests/test_fonts.py", "tests/test_ui_copy_editor.py"),
"python": (),
}
SUITES = (*PYTEST_TARGETS, "frontend", "all")
class HarnessError(RuntimeError):
"""A user-actionable local test environment error."""
def _display_command(command: Sequence[str]) -> str:
return subprocess.list2cmdline([str(part) for part in command])
def _run(
command: Sequence[str | Path],
*,
cwd: Path | None = None,
env: dict[str, str] | None = None,
) -> None:
normalized = [str(part) for part in command]
print(f"> {_display_command(normalized)}", flush=True)
subprocess.run(normalized, cwd=cwd, env=env, check=True)
def _validate_layout() -> None:
missing = [path for path in (*REQUIREMENTS_FILES, SOURCE_DIR / "app") if not path.exists()]
if missing:
joined = "、".join(str(path) for path in missing)
raise HarnessError(f"无法定位项目文件:{joined}")
def _requirements_fingerprint() -> str:
digest = hashlib.sha256()
for path in REQUIREMENTS_FILES:
digest.update(path.name.encode("utf-8"))
digest.update(b"\0")
digest.update(path.read_bytes())
digest.update(b"\0")
return digest.hexdigest()
def setup_environment() -> None:
_validate_layout()
HARNESS_DIR.mkdir(parents=True, exist_ok=True)
if VENV_DIR.exists() and not VENV_PYTHON.is_file():
raise HarnessError(
f"持久环境存在但 Python 不可用:{VENV_PYTHON}。"
"请先把损坏的 .venv 改名留作检查,再手动删除或移走它,然后重新运行 setup;"
"工具不会自动删除现有环境。"
)
if not VENV_PYTHON.is_file():
print(f"创建持久测试环境:{VENV_DIR}", flush=True)
_run((sys.executable, "-m", "venv", VENV_DIR))
fingerprint = _requirements_fingerprint()
recorded = (
REQUIREMENTS_STAMP.read_text(encoding="ascii").strip()
if REQUIREMENTS_STAMP.is_file()
else ""
)
if recorded != fingerprint:
print("依赖首次安装或 requirements 已变化,正在从官方 PyPI 同步。", flush=True)
_run(
(
VENV_PYTHON,
"-m",
"pip",
"install",
"--disable-pip-version-check",
"--upgrade",
"--index-url",
"https://pypi.org/simple",
"-r",
REQUIREMENTS_FILES[-1],
)
)
_run((VENV_PYTHON, "-m", "pip", "check"))
REQUIREMENTS_STAMP.write_text(f"{fingerprint}\n", encoding="ascii")
print("依赖同步完成,已记录 requirements 哈希。", flush=True)
else:
print("requirements 未变化,跳过依赖安装。", flush=True)
_run((VENV_PYTHON, "-m", "pip", "check"))
def _base_environment() -> dict[str, str]:
WORK_DIR.mkdir(parents=True, exist_ok=True)
env = os.environ.copy()
env["MATRIX_DRIVER"] = "mock"
env["PYTHONPYCACHEPREFIX"] = str(WORK_DIR / "pycache")
return env
def _test_environment() -> dict[str, str]:
env = _base_environment()
# Tests that construct an explicit project root remain isolated per case.
# The process-level paths below protect module-level application imports
# from ever reading or migrating either source-tree or manual mock data.
test_data_dir = WORK_DIR / "test-app-data"
env["MATRIX_TEST_ROOT"] = str(test_data_dir)
env["MATRIX_DATA_DIR"] = str(test_data_dir)
env["MATRIX_RUNTIME_DIR"] = str(test_data_dir / "runtime")
for name in ("STATE_DIRECTORY", "RUNTIME_DIRECTORY"):
env.pop(name, None)
return env
def _serve_environment() -> dict[str, str]:
DATA_DIR.mkdir(parents=True, exist_ok=True)
env = _base_environment()
env["MATRIX_DATA_DIR"] = str(DATA_DIR)
env["MATRIX_RUNTIME_DIR"] = str(DATA_DIR / "runtime")
return env
def _remove_path(path: Path) -> bool:
if not path.exists() and not path.is_symlink():
return False
if path.is_dir() and not path.is_symlink():
shutil.rmtree(path)
else:
path.unlink()
print(f"已清理:{path}", flush=True)
return True
def _assert_within(path: Path, root: Path) -> None:
resolved_path = path.resolve(strict=False)
resolved_root = root.resolve(strict=False)
try:
resolved_path.relative_to(resolved_root)
except ValueError as exc:
raise HarnessError(f"拒绝处理工作区外路径:{resolved_path}") from exc
def _prepare_pytest_temp() -> Path:
pytest_temp = WORK_DIR / "pytest"
_assert_within(pytest_temp, HARNESS_DIR)
_remove_path(pytest_temp)
pytest_temp.mkdir(parents=True, exist_ok=True)
return pytest_temp
def _run_pytest(suite: str, env: dict[str, str]) -> None:
pytest_temp = _prepare_pytest_temp()
command: list[str | Path] = [
VENV_PYTHON,
"-m",
"pytest",
"-q",
"-p",
"no:cacheprovider",
f"--basetemp={pytest_temp}",
]
command.extend(PYTEST_TARGETS[suite])
_run(command, cwd=SOURCE_DIR, env=env)
def _run_frontend_tests(env: dict[str, str]) -> None:
node = shutil.which("node")
if not node:
raise HarnessError("未找到 Node.js;frontend/all 套件需要可执行的 node。")
_run(
(
node,
"--test",
"tests/test_animation_ui.mjs",
"tests/test_canvas_tools.mjs",
"tests/test_color_math.mjs",
"tests/test_current_display_playback_ui.mjs",
"tests/test_device_ui.mjs",
"tests/test_font_picker.mjs",
"tests/test_frp_ui.mjs",
"tests/test_interaction_feedback.mjs",
"tests/test_scene_model.mjs",
"tests/test_settings_ui.mjs",
"tests/test_template_management_ui.mjs",
"tests/test_ui_copy_editor.mjs",
"tests/test_wifi_ui.mjs",
"tests/test_workspace_order_ui.mjs",
),
cwd=SOURCE_DIR,
env=env,
)
javascript_files = sorted((SOURCE_DIR / "app" / "static").rglob("*.js"))
if not javascript_files:
raise HarnessError("未找到 app/static 下的 JavaScript 文件。")
for path in javascript_files:
_run((node, "--check", path), cwd=SOURCE_DIR, env=env)
_run((node, "--check", "app/ui_copy_editor/static/editor.js"), cwd=SOURCE_DIR, env=env)
_run((VENV_PYTHON, "scripts/build_ui_copy_catalog.py", "--check"), cwd=SOURCE_DIR, env=env)
def run_tests(suite: str) -> None:
setup_environment()
env = _test_environment()
if suite == "all":
_run_pytest("python", env)
_run_frontend_tests(env)
elif suite == "frontend":
_run_frontend_tests(env)
else:
_run_pytest(suite, env)
print(f"本机测试套件通过:{suite}", flush=True)
def serve(port: int) -> None:
setup_environment()
env = _serve_environment()
print(f"本机 mock 页面:http://127.0.0.1:{port}", flush=True)
try:
_run(
(
VENV_PYTHON,
"-m",
"uvicorn",
"app.main:app",
"--host",
"127.0.0.1",
"--port",
str(port),
),
cwd=SOURCE_DIR,
env=env,
)
except KeyboardInterrupt:
print("本机 mock 服务已停止。", flush=True)
def clean() -> None:
_validate_layout()
fixed_targets = [
WORK_DIR,
DATA_DIR / "runtime",
SOURCE_DIR / ".pytest_cache",
SOURCE_DIR / ".coverage",
SOURCE_DIR / "coverage.xml",
WORKSPACE_ROOT / ".playwright-cli",
WORKSPACE_ROOT / "output" / "playwright",
HARNESS_DIR / "__pycache__",
]
for target in fixed_targets:
_assert_within(target, WORKSPACE_ROOT)
cache_dirs: list[Path] = []
for root in (SOURCE_DIR / "app", SOURCE_DIR / "scripts", SOURCE_DIR / "tests"):
if root.is_dir():
cache_dirs.extend(root.rglob("__pycache__"))
for target in cache_dirs:
_assert_within(target, SOURCE_DIR)
removed = 0
for target in sorted(cache_dirs, key=lambda item: len(item.parts), reverse=True):
removed += int(_remove_path(target))
for target in fixed_targets:
removed += int(_remove_path(target))
if not VENV_PYTHON.is_file():
raise HarnessError("清理后未找到持久 .venv Python;清理验收失败。")
if not REQUIREMENTS_STAMP.is_file():
raise HarnessError("清理后依赖哈希不存在;清理验收失败。")
if (DATA_DIR / "config.json").exists():
print("已保留:data/config.json", flush=True)
print(f"清理完成,共移除 {removed} 个可再生目标;.venv 已保留。", flush=True)
def _port(value: str) -> int:
port = int(value)
if not 1 <= port <= 65535:
raise argparse.ArgumentTypeError("端口必须在 1..65535。")
return port
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="奇妙小屏幕控制器本机持久测试工具")
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("setup", help="创建或更新持久 .venv")
test_parser = subparsers.add_parser("test", help="运行所需的本机测试套件")
test_parser.add_argument("--suite", choices=SUITES, default="all")
serve_parser = subparsers.add_parser("serve", help="以前台方式启动 mock 网页")
serve_parser.add_argument("--port", type=_port, default=8765)
subparsers.add_parser("clean", help="清理可再生产物,保留 .venv 和本机配置")
return parser
def main(argv: Sequence[str] | None = None) -> int:
args = _parser().parse_args(argv)
try:
if args.command == "setup":
setup_environment()
elif args.command == "test":
run_tests(args.suite)
elif args.command == "serve":
serve(args.port)
elif args.command == "clean":
clean()
else:
raise HarnessError(f"未知命令:{args.command}")
except (HarnessError, OSError, subprocess.CalledProcessError) as exc:
print(f"本机测试工具失败:{exc}", file=sys.stderr, flush=True)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())