모델이 티어에 따라 2~17GB 를 차지하는데 %APPDATA% 에 박혀 있어서 C 드라이브가 좁은 사람은 손쓸 방법이 없었다. 처음 켤 때 한 번 묻고, 그 선택을 기억하고, 설정에서 바꿀 수 있게 한다. 닭과 달걀 문제가 하나 있다 — "어디에 둘지" 는 사용자가 정하는데 그 선택 자체는 고정된 곳에 적어둬야 다음 실행에 찾을 수 있다. 그래서 OS 표준 폴더에는 위치를 가리키는 쪽지(location.json) 한 장만 두고, 내용물은 전부 고른 폴더에 둔다. - storage.py 신설: 쪽지 읽기/쓰기, 경로 검증, 여유 공간, 데이터 이동 - config.py 의 CONFIG_PATH 를 config_path() 로 바꾼다. import 시점 상수라 위치를 바꿔도 옛 경로를 계속 가리켰다 - 위치가 정해지기 전에는 파일 로그를 열지 않는다. 열면 고르지도 않은 기본 폴더가 만들어지고 거기에 로그가 남는다 - 이전 버전 사용자에게는 묻지 않는다. 기본 위치에 쓰던 흔적이 있으면 그대로 쓴다 — 같은 자리를 다시 고르는 헛수고를 시킬 이유가 없다 - 고른 디스크가 빠져도 죽지 않고 기본 위치로 물러난다 - 위치를 바꿀 때 받아둔 모델을 옮길지 물어본다. 수 GB 라 창이 얼어붙지 않도록 별도 스레드에서 옮긴다 - windows_smoke 에 이 화면 렌더링 점검을 추가하고, 스크린샷을 호스트로 회수한다. 경로 표기와 디스크 조회는 OS 마다 다르게 도는 부분이다
This commit is contained in:
61
tests/test_data_location.py
Normal file
61
tests/test_data_location.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""저장 위치 대화상자 — 못 쓰는 폴더를 고르면 진행을 막는지."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("PySide6")
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
|
||||
from PySide6.QtWidgets import QApplication # noqa: E402
|
||||
|
||||
from livesub import storage # noqa: E402
|
||||
from livesub.ui.data_location import DataLocationDialog # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def qapp():
|
||||
app = QApplication.instance() or QApplication([])
|
||||
yield app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def os_dir(tmp_path, monkeypatch):
|
||||
base = tmp_path / "appdata" / "LiveSub"
|
||||
monkeypatch.setattr(storage, "_os_config_dir", lambda: base)
|
||||
return base
|
||||
|
||||
|
||||
def test_first_run_dialog_starts_on_the_default_folder(qapp, os_dir):
|
||||
dialog = DataLocationDialog(storage.default_data_dir(), first_run=True)
|
||||
try:
|
||||
assert dialog.selected() == os_dir
|
||||
assert dialog.ok_button.isEnabled()
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
def test_empty_path_blocks_confirmation(qapp, os_dir):
|
||||
dialog = DataLocationDialog(storage.default_data_dir(), first_run=True)
|
||||
try:
|
||||
dialog.path_edit.setText(" ")
|
||||
assert not dialog.ok_button.isEnabled()
|
||||
dialog.path_edit.setText(str(os_dir.parent / "elsewhere"))
|
||||
assert dialog.ok_button.isEnabled()
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
|
||||
|
||||
def test_change_dialog_blocks_a_subfolder_of_the_current_one(qapp, tmp_path):
|
||||
current = tmp_path / "current"
|
||||
current.mkdir()
|
||||
dialog = DataLocationDialog(current, first_run=False)
|
||||
try:
|
||||
dialog.path_edit.setText(str(current / "inside"))
|
||||
assert not dialog.ok_button.isEnabled()
|
||||
dialog.path_edit.setText(str(tmp_path / "other"))
|
||||
assert dialog.ok_button.isEnabled()
|
||||
finally:
|
||||
dialog.deleteLater()
|
||||
124
tests/test_storage.py
Normal file
124
tests/test_storage.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""저장 위치 선택 — 고른 곳에 실제로 쌓이고, 다음 실행 때 그대로 따라가는지."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from livesub import storage
|
||||
from livesub.config import AppConfig, config_path
|
||||
from livesub.constants import models_dir, user_data_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def os_dir(tmp_path, monkeypatch):
|
||||
"""OS 표준 설정 폴더(쪽지가 놓이는 곳)를 임시 폴더로 바꾼다."""
|
||||
base = tmp_path / "appdata" / "LiveSub"
|
||||
monkeypatch.setattr(storage, "_os_config_dir", lambda: base)
|
||||
return base
|
||||
|
||||
|
||||
def test_first_run_is_not_configured(os_dir):
|
||||
assert storage.is_configured() is False
|
||||
assert storage.stored_data_dir() is None
|
||||
|
||||
|
||||
def test_choice_is_remembered(os_dir, tmp_path):
|
||||
chosen = tmp_path / "D" / "LiveSub"
|
||||
storage.set_data_dir(chosen)
|
||||
|
||||
assert storage.is_configured() is True
|
||||
assert storage.stored_data_dir() == chosen.resolve()
|
||||
assert storage.data_dir() == chosen.resolve()
|
||||
|
||||
|
||||
def test_everything_lands_in_the_chosen_folder(os_dir, tmp_path):
|
||||
"""쪽지만 OS 폴더에 남고 내용물은 고른 곳으로 가야 한다."""
|
||||
chosen = tmp_path / "games" / "LiveSub"
|
||||
storage.set_data_dir(chosen)
|
||||
|
||||
assert user_data_dir() == chosen.resolve()
|
||||
assert config_path().parent == chosen.resolve()
|
||||
assert models_dir() == chosen.resolve() / "models"
|
||||
|
||||
AppConfig().save()
|
||||
assert (chosen / "config.json").is_file()
|
||||
assert not (os_dir / "config.json").exists()
|
||||
assert list(os_dir.iterdir()) == [os_dir / storage.POINTER_NAME]
|
||||
|
||||
|
||||
def test_existing_install_is_not_asked_again(os_dir):
|
||||
"""위치 선택이 없던 버전에서 올라온 사람에게 다시 묻지 않는다."""
|
||||
os_dir.mkdir(parents=True)
|
||||
(os_dir / "models").mkdir()
|
||||
|
||||
assert storage.is_configured() is True
|
||||
assert storage.stored_data_dir() is None # 쪽지는 없지만
|
||||
assert storage.data_dir() == os_dir # 쓰던 자리를 그대로 쓴다
|
||||
|
||||
|
||||
def test_broken_pointer_falls_back_to_default(os_dir):
|
||||
os_dir.mkdir(parents=True)
|
||||
(os_dir / storage.POINTER_NAME).write_text("{ 깨진 json", encoding="utf-8")
|
||||
|
||||
assert storage.stored_data_dir() is None
|
||||
assert storage.data_dir() == os_dir
|
||||
|
||||
|
||||
def test_unusable_location_falls_back_instead_of_crashing(os_dir, tmp_path):
|
||||
"""고른 디스크가 빠졌거나 경로가 막혔을 때 죽지 않고 기본 위치로 물러난다."""
|
||||
blocker = tmp_path / "blocker"
|
||||
blocker.write_text("파일이라 하위 폴더를 만들 수 없다", encoding="utf-8")
|
||||
os_dir.mkdir(parents=True)
|
||||
(os_dir / storage.POINTER_NAME).write_text(
|
||||
json.dumps({"data_dir": str(blocker / "LiveSub")}), encoding="utf-8"
|
||||
)
|
||||
|
||||
assert storage.data_dir() == os_dir
|
||||
|
||||
|
||||
def test_check_target_rejects_unusable_choices(tmp_path):
|
||||
current = tmp_path / "current"
|
||||
current.mkdir()
|
||||
|
||||
assert storage.check_target("") is not None
|
||||
assert storage.check_target(current / "inside", current) is not None # 자기 하위
|
||||
assert storage.check_target(current, current) is None # 같은 곳은 문제 아님
|
||||
assert storage.check_target(tmp_path / "new") is None # 아직 없어도 만들 수 있으면 OK
|
||||
|
||||
|
||||
def test_move_data_carries_models_and_config(tmp_path):
|
||||
old, new = tmp_path / "old", tmp_path / "new"
|
||||
(old / "models" / "whisper").mkdir(parents=True)
|
||||
(old / "models" / "whisper" / "model.bin").write_bytes(b"x" * 16)
|
||||
(old / "config.json").write_text("{}", encoding="utf-8")
|
||||
(old / "livesub.log").write_text("로그는 안 옮긴다", encoding="utf-8")
|
||||
|
||||
moved = storage.move_data(old, new)
|
||||
|
||||
assert sorted(moved) == ["config.json", "models"]
|
||||
assert (new / "models" / "whisper" / "model.bin").read_bytes() == b"x" * 16
|
||||
assert not (old / "models").exists()
|
||||
assert (old / "livesub.log").is_file() # 실행 중 잠겨 있으므로 제외 대상
|
||||
|
||||
|
||||
def test_move_data_never_overwrites_existing(tmp_path):
|
||||
old, new = tmp_path / "old", tmp_path / "new"
|
||||
old.mkdir()
|
||||
new.mkdir()
|
||||
(old / "config.json").write_text("옛날 것", encoding="utf-8")
|
||||
(new / "config.json").write_text("새 위치에 이미 있던 것", encoding="utf-8")
|
||||
|
||||
assert storage.move_data(old, new) == []
|
||||
assert (new / "config.json").read_text(encoding="utf-8") == "새 위치에 이미 있던 것"
|
||||
assert (old / "config.json").is_file()
|
||||
|
||||
|
||||
def test_move_data_to_same_place_does_nothing(tmp_path):
|
||||
same = tmp_path / "same"
|
||||
same.mkdir()
|
||||
(same / "config.json").write_text("{}", encoding="utf-8")
|
||||
|
||||
assert storage.move_data(same, same) == []
|
||||
assert (same / "config.json").is_file()
|
||||
Reference in New Issue
Block a user