MIL-009: Executable with PyInstaller
Review record RC-048 (Go, autonomous mode, not independent). Task: MIL-009#1 Task: MIL-009#2 Task: MIL-009#3 Task: MIL-009#4 Task: MIL-009#5 Task: MIL-009#6 Task: MIL-009#7 Task: MIL-009#8
This commit is contained in:
@@ -105,6 +105,22 @@ def test_level_screen_shows_title_with_font_on_window_background(
|
||||
assert level_view.frame.cget("bg") == BACKGROUND_COLOR
|
||||
|
||||
|
||||
def test_level_screen_sets_window_title_background_and_padding(
|
||||
tk_root: tk.Tk,
|
||||
) -> None:
|
||||
tk_root.title("tk")
|
||||
tk_root.config(padx=0, pady=0, bg="white")
|
||||
|
||||
view = LevelView(tk_root, LevelViewModel(), lambda level: None)
|
||||
try:
|
||||
assert tk_root.title() == WINDOW_TITLE
|
||||
assert tk_root.cget("bg") == BACKGROUND_COLOR
|
||||
assert int(str(tk_root.cget("padx"))) == WINDOW_PADDING
|
||||
assert int(str(tk_root.cget("pady"))) == WINDOW_PADDING
|
||||
finally:
|
||||
view.destroy()
|
||||
|
||||
|
||||
def test_level_screen_frame_has_the_card_window_size(level_view: LevelView) -> None:
|
||||
assert level_view.frame.winfo_width() == CARD_WIDTH
|
||||
assert level_view.frame.winfo_height() == LEVEL_SCREEN_HEIGHT
|
||||
|
||||
@@ -241,3 +241,50 @@ def test_start_raises_vocabulary_error_for_the_caller(fake_views: None) -> None:
|
||||
application = _application(_FakeRoot(), error=True)
|
||||
with pytest.raises(VocabularyNotFoundError):
|
||||
application.start(Level(100))
|
||||
|
||||
|
||||
def test_resolve_data_files_uses_bundled_list_when_default_folder_has_none(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
bundled = tmp_path / "bundle" / "ms-da.csv"
|
||||
monkeypatch.setattr(app, "DEFAULT_DATA_DIR", tmp_path / "data")
|
||||
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", bundled)
|
||||
vocabulary, progress = app.resolve_data_files(None)
|
||||
assert vocabulary == bundled
|
||||
assert progress == tmp_path / "data" / "words_to_learn.csv"
|
||||
|
||||
|
||||
def test_resolve_data_files_prefers_the_default_folder_list(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
(tmp_path / "ms-da.csv").write_text("x", encoding="utf-8")
|
||||
monkeypatch.setattr(app, "DEFAULT_DATA_DIR", tmp_path)
|
||||
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", tmp_path / "other.csv")
|
||||
vocabulary, _ = app.resolve_data_files(None)
|
||||
assert vocabulary == tmp_path / "ms-da.csv"
|
||||
|
||||
|
||||
def test_resolve_data_files_uses_a_given_folder_alone(
|
||||
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", tmp_path / "other.csv")
|
||||
vocabulary, progress = app.resolve_data_files(tmp_path / "given")
|
||||
assert vocabulary == tmp_path / "given" / "ms-da.csv"
|
||||
assert progress == tmp_path / "given" / "words_to_learn.csv"
|
||||
|
||||
|
||||
def test_create_application_draws_first_card_from_bundled_list(
|
||||
fake_views: None, monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
||||
) -> None:
|
||||
bundled = tmp_path / "bundle" / "ms-da.csv"
|
||||
bundled.parent.mkdir()
|
||||
bundled.write_text("ms;da\nsaya;jeg\n", encoding="utf-8")
|
||||
data = tmp_path / "data"
|
||||
monkeypatch.setattr(app.tk, "Tk", _FakeRoot)
|
||||
monkeypatch.setattr(app, "DEFAULT_DATA_DIR", data)
|
||||
monkeypatch.setattr(app, "BUNDLED_VOCABULARY_PATH", bundled)
|
||||
application = app.create_application(None, False, Level(None))
|
||||
view = cast(_FakeFlashCardView, application.view)
|
||||
assert view.view_model.text == "saya"
|
||||
view.view_model.is_known()
|
||||
assert (data / "words_to_learn.csv").is_file()
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Tests of the executable build tool.
|
||||
|
||||
@file test_build_executable.py
|
||||
@brief Checks the PyInstaller arguments and the missing PyInstaller message.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
from flash_card.constants import EXECUTABLE_NAME, ICON_FILE_NAME
|
||||
|
||||
_TOOL_PATH = Path(__file__).resolve().parents[2] / "tools" / "build_executable.py"
|
||||
ROOT = Path("project")
|
||||
ASSETS = ROOT / "src" / "flash_card" / "presentation" / "assets"
|
||||
|
||||
|
||||
def _load_tool() -> ModuleType:
|
||||
spec = importlib.util.spec_from_file_location("build_executable", _TOOL_PATH)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _option(arguments: list[str], name: str) -> str:
|
||||
return arguments[arguments.index(name) + 1]
|
||||
|
||||
|
||||
def test_build_arguments_start_with_the_packaging_flags() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT)
|
||||
assert arguments[:3] == ["--onefile", "--windowed", "--noconfirm"]
|
||||
|
||||
|
||||
def test_build_arguments_name_icon_and_folders() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT)
|
||||
assert _option(arguments, "--name") == EXECUTABLE_NAME == "flash-card"
|
||||
assert _option(arguments, "--icon") == str(ASSETS / ICON_FILE_NAME)
|
||||
assert _option(arguments, "--paths") == str(ROOT / "src")
|
||||
assert _option(arguments, "--distpath") == str(ROOT / "dist")
|
||||
assert _option(arguments, "--workpath") == str(ROOT / "build")
|
||||
assert _option(arguments, "--specpath") == str(ROOT / "build")
|
||||
|
||||
|
||||
def test_build_arguments_add_assets_and_word_list() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT)
|
||||
pairs = [
|
||||
arguments[i + 1] for i, value in enumerate(arguments) if value == "--add-data"
|
||||
]
|
||||
assert pairs == [
|
||||
f"{ASSETS}{os.pathsep}flash_card/presentation/assets",
|
||||
f"{ROOT / 'data' / 'ms-da.csv'}{os.pathsep}data",
|
||||
]
|
||||
|
||||
|
||||
def test_build_arguments_end_with_the_entry_script() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT, clean=True)
|
||||
assert arguments[-1] == str(ROOT / "src" / "flash_card" / "__main__.py")
|
||||
|
||||
|
||||
def test_build_arguments_include_speech_packages_by_default() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT)
|
||||
assert arguments.count("--collect-all") == 2
|
||||
assert "gtts" in arguments
|
||||
assert "playsound" in arguments
|
||||
|
||||
|
||||
def test_build_arguments_leave_out_speech_packages_without_speech() -> None:
|
||||
arguments = _load_tool().build_arguments(ROOT, speech=False)
|
||||
assert "--collect-all" not in arguments
|
||||
assert "--hidden-import" not in arguments
|
||||
|
||||
|
||||
def test_build_arguments_clean_only_when_asked() -> None:
|
||||
tool = _load_tool()
|
||||
assert "--clean" not in tool.build_arguments(ROOT)
|
||||
assert "--clean" in tool.build_arguments(ROOT, clean=True)
|
||||
|
||||
|
||||
def test_main_returns_two_and_explains_when_pyinstaller_is_missing(
|
||||
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
def refuse(name: str, package: str | None = None) -> ModuleType:
|
||||
raise ImportError(name)
|
||||
|
||||
tool = _load_tool()
|
||||
monkeypatch.setattr(importlib, "import_module", refuse)
|
||||
assert tool.main([]) == 2
|
||||
assert 'pip install -e ".[build]"' in capsys.readouterr().err
|
||||
@@ -35,3 +35,27 @@ def test_project_root_contains_pyproject() -> None:
|
||||
|
||||
def test_csv_encoding_drops_byte_order_mark() -> None:
|
||||
assert constants.CSV_ENCODING == "utf-8-sig"
|
||||
|
||||
|
||||
def test_assets_dir_holds_the_four_images_and_the_icon() -> None:
|
||||
names = [
|
||||
constants.CARD_FRONT_IMAGE_NAME,
|
||||
constants.CARD_BACK_IMAGE_NAME,
|
||||
constants.RIGHT_IMAGE_NAME,
|
||||
constants.WRONG_IMAGE_NAME,
|
||||
constants.ICON_FILE_NAME,
|
||||
]
|
||||
assert len(set(names)) == 5
|
||||
assert all((constants.ASSETS_DIR / name).is_file() for name in names)
|
||||
|
||||
|
||||
def test_bundled_vocabulary_is_the_word_list_of_the_bundled_data_dir() -> None:
|
||||
assert constants.BUNDLED_VOCABULARY_PATH == (
|
||||
constants.BUNDLED_DATA_DIR / constants.VOCABULARY_FILE_NAME
|
||||
)
|
||||
assert constants.BUNDLED_VOCABULARY_PATH.is_file()
|
||||
|
||||
|
||||
def test_executable_and_icon_names() -> None:
|
||||
assert constants.EXECUTABLE_NAME == "flash-card"
|
||||
assert constants.ICON_FILE_NAME == "flash_card.ico"
|
||||
|
||||
@@ -16,6 +16,7 @@ from flash_card.constants import (
|
||||
CARD_FRONT_IMAGE_NAME,
|
||||
CARD_HEIGHT,
|
||||
CARD_WIDTH,
|
||||
ICON_FILE_NAME,
|
||||
RIGHT_IMAGE_NAME,
|
||||
WRONG_IMAGE_NAME,
|
||||
)
|
||||
@@ -56,3 +57,18 @@ def test_generate_images_writes_png_with_expected_size(
|
||||
assert data.startswith(_SIGNATURE)
|
||||
assert data[12:16] == b"IHDR"
|
||||
assert struct.unpack(">II", data[16:24]) == (width, height)
|
||||
|
||||
|
||||
def test_generate_images_writes_icon_with_one_256_pixel_png(generated: Path) -> None:
|
||||
data = (generated / ICON_FILE_NAME).read_bytes()
|
||||
reserved, kind, count = struct.unpack("<HHH", data[:6])
|
||||
width, height, _colors, _zero, planes, bpp, size, offset = struct.unpack(
|
||||
"<BBBBHHII", data[6:22]
|
||||
)
|
||||
assert (reserved, kind, count) == (0, 1, 1)
|
||||
assert (width, height, planes, bpp) == (0, 0, 1, 32)
|
||||
assert offset == 22
|
||||
assert size == len(data) - 22
|
||||
png = data[offset:]
|
||||
assert png.startswith(_SIGNATURE)
|
||||
assert struct.unpack(">II", png[16:24]) == (256, 256)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Tests of the runtime helpers.
|
||||
|
||||
@file test_runtime.py
|
||||
@brief Checks where resources and the executable are, frozen or from source.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from flash_card import runtime
|
||||
|
||||
BUNDLE = Path("bundle")
|
||||
EXECUTABLE = Path("apps") / "flash-card.exe"
|
||||
ROOT = Path("project")
|
||||
|
||||
|
||||
def test_pick_resource_root_returns_bundle_when_frozen() -> None:
|
||||
assert runtime.pick_resource_root(True, BUNDLE, ROOT) == BUNDLE
|
||||
|
||||
|
||||
def test_pick_resource_root_returns_root_when_not_frozen() -> None:
|
||||
assert runtime.pick_resource_root(False, BUNDLE, ROOT) == ROOT
|
||||
|
||||
|
||||
def test_pick_resource_root_returns_root_when_bundle_is_unknown() -> None:
|
||||
assert runtime.pick_resource_root(True, None, ROOT) == ROOT
|
||||
|
||||
|
||||
def test_pick_executable_dir_returns_executable_folder_when_frozen() -> None:
|
||||
assert runtime.pick_executable_dir(True, EXECUTABLE, ROOT) == EXECUTABLE.parent
|
||||
|
||||
|
||||
def test_pick_executable_dir_returns_root_when_not_frozen() -> None:
|
||||
assert runtime.pick_executable_dir(False, EXECUTABLE, ROOT) == ROOT
|
||||
|
||||
|
||||
def test_is_frozen_is_false_in_a_test_run() -> None:
|
||||
assert runtime.is_frozen() is False
|
||||
|
||||
|
||||
def test_resource_root_is_project_root_in_a_source_run() -> None:
|
||||
assert (runtime.resource_root() / "pyproject.toml").is_file()
|
||||
|
||||
|
||||
def test_executable_dir_is_project_root_in_a_source_run() -> None:
|
||||
assert runtime.executable_dir() == runtime.source_root()
|
||||
Reference in New Issue
Block a user