Skip to content

Commit 3d207b5

Browse files
committed
Fix ASCII encoding crash in packaged app; add bundle smoke test
The macOS .app bundle runs with LANG=C, causing Python to default to ASCII encoding. Alembic's fileConfig() then choked on non-ASCII chars (em dash, arrow) in alembic.ini and run.py. Fixed by: - Replacing non-ASCII chars in runtime-read files - Adding encoding="utf-8" to fileConfig() call Also adds scripts/test_app_bundle.py and Makefile targets (pack, test-bundle, pack-and-test) for local smoke-testing before releases. Made-with: Cursor
1 parent 7034c65 commit 3d207b5

6 files changed

Lines changed: 122 additions & 4 deletions

File tree

Makefile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ dist: clean ## builds source and wheel package
8181
uv build
8282
ls -l dist
8383

84+
pack: clean-build ## build the app bundle with flet pack
85+
uv run python scripts/pack_app.py
86+
87+
test-bundle: ## smoke-test the packaged app bundle (build first with `make pack`)
88+
uv run python scripts/test_app_bundle.py
89+
90+
pack-and-test: pack test-bundle ## build and smoke-test the app bundle
91+
8492
install: ## install the package with uv
8593
uv sync
8694

scripts/test_app_bundle.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Smoke-test for the packaged app bundle.
2+
3+
Launches the platform-specific executable produced by ``flet pack``
4+
(via ``scripts/pack_app.py``) and verifies it survives initial startup
5+
without crashing.
6+
7+
Usage:
8+
uv run python scripts/test_app_bundle.py # test only
9+
uv run python scripts/test_app_bundle.py --build # build then test
10+
"""
11+
12+
import os
13+
import signal
14+
import subprocess
15+
import sys
16+
import time
17+
from pathlib import Path
18+
19+
import typer
20+
from loguru import logger
21+
22+
APP_NAME = "Tuttle"
23+
DIST_DIR = Path(__file__).resolve().parent.parent / "dist"
24+
STARTUP_WAIT_SECONDS = 8
25+
26+
27+
def _find_executable() -> Path:
28+
if sys.platform.startswith("darwin"):
29+
exe = DIST_DIR / f"{APP_NAME}.app" / "Contents" / "MacOS" / APP_NAME
30+
elif sys.platform.startswith("win"):
31+
exe = DIST_DIR / f"{APP_NAME}.exe"
32+
else:
33+
exe = DIST_DIR / APP_NAME
34+
return exe
35+
36+
37+
def _build():
38+
logger.info("Building app bundle ...")
39+
result = subprocess.run(
40+
[sys.executable, "scripts/pack_app.py"],
41+
cwd=DIST_DIR.parent,
42+
)
43+
if result.returncode != 0:
44+
logger.error(f"Build failed with exit code {result.returncode}")
45+
raise typer.Exit(code=1)
46+
logger.info("Build succeeded")
47+
48+
49+
def _launch_and_check(exe: Path) -> bool:
50+
"""Return True if the app survives startup without crashing."""
51+
logger.info(f"Launching {exe}")
52+
env = {**os.environ, "LANG": "C"} # simulate .app bundle locale
53+
54+
proc = subprocess.Popen(
55+
[str(exe)],
56+
stdout=subprocess.PIPE,
57+
stderr=subprocess.PIPE,
58+
start_new_session=True,
59+
env=env,
60+
)
61+
62+
try:
63+
logger.info(f"Waiting {STARTUP_WAIT_SECONDS}s for startup ...")
64+
time.sleep(STARTUP_WAIT_SECONDS)
65+
exit_code = proc.poll()
66+
67+
if exit_code is not None:
68+
_, stderr = proc.communicate(timeout=5)
69+
logger.error(
70+
f"App crashed during startup (exit code {exit_code})\n"
71+
f"stderr:\n{stderr.decode(errors='replace')}"
72+
)
73+
return False
74+
75+
logger.info("App is still running after startup — smoke test passed")
76+
return True
77+
78+
finally:
79+
try:
80+
pgid = os.getpgid(proc.pid)
81+
os.killpg(pgid, signal.SIGTERM)
82+
proc.wait(timeout=5)
83+
except (ProcessLookupError, ChildProcessError, subprocess.TimeoutExpired):
84+
try:
85+
proc.kill()
86+
proc.wait(timeout=3)
87+
except Exception:
88+
pass
89+
logger.info("App process terminated")
90+
91+
92+
def main(
93+
build: bool = typer.Option(False, "--build", "-b", help="Build before testing"),
94+
):
95+
if build:
96+
_build()
97+
98+
exe = _find_executable()
99+
if not exe.exists():
100+
logger.error(
101+
f"Executable not found at {exe}\n" "Run with --build or `make pack` first."
102+
)
103+
raise typer.Exit(code=1)
104+
105+
ok = _launch_and_check(exe)
106+
raise typer.Exit(code=0 if ok else 1)
107+
108+
109+
if __name__ == "__main__":
110+
typer.run(main)

tuttle/migrations/alembic.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# not from this file. See env.py for details.
55

66
[alembic]
7-
# path to migration scripts relative to this file
7+
# path to migration scripts -- relative to this file
88
script_location = %(here)s
99

1010
# template used to generate migration file names

tuttle/migrations/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
# Interpret the config file for Python logging, if present
3939
if config.config_file_name is not None:
40-
fileConfig(config.config_file_name)
40+
fileConfig(config.config_file_name, encoding="utf-8")
4141

4242
# The target metadata for autogenerate support
4343
target_metadata = SQLModel.metadata

tuttle/migrations/run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def run_migrations(db_url: str) -> None:
8383
logger.debug(f"Database is already at head revision ({head})")
8484
return
8585

86-
logger.info(f"Running migrations: {current} {head}")
86+
logger.info(f"Running migrations: {current} -> {head}")
8787
command.upgrade(cfg, "head")
8888
logger.info("Migrations completed successfully")
8989

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)