mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
144 lines
5.9 KiB
Python
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()) == []
|