Files
tick-stock-panel/backend/tests/test_macos_data_dir.py
T

144 lines
5.9 KiB
Python

"""macOS 数据目录移出 .app 包 — 路径解析与包内旧数据迁移。
背景: ≤0.3.6 的 frozen data 目录 = exe 同级, mac 上落在 TSP.app 包内,
覆盖安装替换 App 时旧包(含数据)整体进废纸篓, 用户数据全部"清空"。
修复: darwin frozen 默认 → ~/Library/Application Support/TSP (包外),
启动时自动把包内/兄弟目录的旧数据搬过来。
"""
from __future__ import annotations
import sys
from pathlib import Path
from app import config as config_mod
from app.config import _user_data_root
from app.tickflow.repository import DataStore
# ── 路径解析 ─────────────────────────────────────────────────────────
def test_darwin_frozen_uses_app_support_outside_bundle(tmp_path, monkeypatch):
home = tmp_path / "fakehome"
monkeypatch.setattr(config_mod, "_IS_FROZEN", True)
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setattr(Path, "home", lambda: home)
assert _user_data_root() == home / "Library" / "Application Support" / "TSP"
def test_windows_frozen_keeps_exe_sibling_data(tmp_path, monkeypatch):
exe = tmp_path / "TSP" / "TSP.exe"
monkeypatch.setattr(config_mod, "_IS_FROZEN", True)
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setattr(sys, "executable", str(exe))
assert _user_data_root() == exe.parent / "data"
def test_dev_mode_uses_project_root(monkeypatch):
monkeypatch.setattr(config_mod, "_IS_FROZEN", False)
assert _user_data_root() == config_mod._PROJECT_ROOT / "data"
# ── 迁移 (直接构造最小实例调方法, 不触发完整 __init__) ────────────────
def _repo_with_data_dir(new_dir: Path) -> DataStore:
repo = DataStore.__new__(DataStore)
repo.data_dir = new_dir
return repo
def _make_real_data(root: Path) -> None:
(root / "kline_daily_enriched" / "date=2026-10-10").mkdir(parents=True)
(root / "kline_daily_enriched" / "date=2026-10-10" / "part.parquet").write_bytes(b"x")
(root / "user_data").mkdir(parents=True)
(root / "user_data" / "secrets.json").write_text("{}", encoding="utf-8")
def test_darwin_bundle_data_migrates_to_new_location(tmp_path, monkeypatch):
"""包内旧数据(用户从废纸篓恢复/从 dmg 运行过)首次启动自动搬出。"""
new_dir = tmp_path / "newdata"
new_dir.mkdir()
bundle_data = tmp_path / "TSP.app" / "Contents" / "MacOS" / "data"
_make_real_data(bundle_data)
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setattr(sys, "executable", str(bundle_data.parent / "TSP"))
_repo_with_data_dir(new_dir)._migrate_legacy_data_dir()
assert (new_dir / "user_data" / "secrets.json").exists()
assert (new_dir / "kline_daily_enriched" / "date=2026-10-10" / "part.parquet").exists()
assert not bundle_data.exists() # 旧包内目录已清空
def test_migration_skips_when_new_location_has_data(tmp_path, monkeypatch):
"""新位置已有真实数据 → 绝不覆盖 (用户已在两套路径都跑过)。"""
new_dir = tmp_path / "newdata"
_make_real_data(new_dir)
(new_dir / "user_data" / "secrets.json").write_text('{"mine": 1}', encoding="utf-8")
bundle_data = tmp_path / "TSP.app" / "Contents" / "MacOS" / "data"
_make_real_data(bundle_data)
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setattr(sys, "executable", str(bundle_data.parent / "TSP"))
_repo_with_data_dir(new_dir)._migrate_legacy_data_dir()
assert (new_dir / "user_data" / "secrets.json").read_text(encoding="utf-8") == '{"mine": 1}'
assert bundle_data.exists() # 旧数据原样保留
def test_migration_noop_without_legacy(tmp_path, monkeypatch):
new_dir = tmp_path / "newdata"
new_dir.mkdir()
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setattr(sys, "executable", str(tmp_path / "TSP.app" / "Contents" / "MacOS" / "TSP"))
_repo_with_data_dir(new_dir)._migrate_legacy_data_dir()
assert list(new_dir.iterdir()) == []
def test_windows_sibling_legacy_still_migrates(tmp_path, monkeypatch):
"""既有行为回归: Windows 旧版兄弟目录 TickFlowStockPanel_Data 照常搬迁。"""
new_dir = tmp_path / "TSP" / "data"
new_dir.mkdir(parents=True)
legacy = tmp_path / "TSP" / "TickFlowStockPanel_Data"
_make_real_data(legacy)
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setattr(sys, "executable", str(tmp_path / "TSP" / "TSP.exe"))
_repo_with_data_dir(new_dir)._migrate_legacy_data_dir()
assert (new_dir / "user_data" / "secrets.json").exists()
assert not legacy.exists()
def test_migration_same_path_guard(tmp_path, monkeypatch):
"""防御: legacy 候选与当前 data_dir 同一路径时不动 (Windows 逻辑回归中的边界)。"""
data_dir = tmp_path / "TSP" / "data"
_make_real_data(data_dir)
monkeypatch.setattr(sys, "frozen", True, raising=False)
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setattr(sys, "executable", str(data_dir / "TSP.exe"))
_repo_with_data_dir(data_dir)._migrate_legacy_data_dir()
# 数据原样, 无自搬自
assert (data_dir / "user_data" / "secrets.json").exists()
def test_dev_mode_never_migrates(tmp_path, monkeypatch):
"""非 frozen (开发/Docker) 直接返回, 即使存在旧目录也不动。"""
new_dir = tmp_path / "newdata"
new_dir.mkdir()
legacy = new_dir.parent / "TickFlowStockPanel_Data"
_make_real_data(legacy)
monkeypatch.delattr(sys, "frozen", raising=False)
_repo_with_data_dir(new_dir)._migrate_legacy_data_dir()
assert legacy.exists()
assert list(new_dir.iterdir()) == []