Skip to content

Commit 1fcab11

Browse files
committed
Add PanoptoDownloader + deps to venv installer; add test_release.py
- gui.py _ML_PACKAGES: add ffmpeg-progress-yield, pycryptodomex, and PanoptoDownloader (git+GitHub URL, not on PyPI) so fresh installs get all packages needed by the Panopto download pipeline - test/test_release.py: comprehensive release-readiness test suite that validates every package import and script importability against the deployed venv (~/.auto_note/venv/bin/python), covering TestCorePackages, TestLazyPackages (including PanoptoDownloader), TestScriptImports, TestCLISmoke, and TestSystemDeps (ffmpeg/ffprobe)
1 parent 5bc746b commit 1fcab11

2 files changed

Lines changed: 254 additions & 0 deletions

File tree

gui.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ def _install_scripts() -> None:
9090
"httpx",
9191
"playwright",
9292
"canvasapi",
93+
# PanoptoDownloader is not on PyPI; install from GitHub.
94+
# Its declared version pins (requests~=2.27, tqdm~=4.62, yarl~=1.7) are
95+
# conservative — newer versions work fine.
96+
"ffmpeg-progress-yield",
97+
"pycryptodomex",
98+
"git+https://github.com/Panopto-Video-DL/Panopto-Video-DL-lib.git",
9399
]
94100

95101

test/test_release.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
"""
2+
Release-readiness tests — verify the VENV (the Python used in the released
3+
AppImage) has every package that every pipeline script imports.
4+
5+
Run with the project's conda env so pytest itself is available, but the
6+
imports are tested against the *venv* Python (~/.auto_note/venv/bin/python)
7+
which is what the GUI actually invokes.
8+
9+
conda run -n auto-note python -m pytest test/test_release.py -v
10+
"""
11+
from __future__ import annotations
12+
13+
import subprocess
14+
import sys
15+
from pathlib import Path
16+
17+
import pytest
18+
19+
# ── Resolve paths ─────────────────────────────────────────────────────────────
20+
21+
PROJECT_DIR = Path(__file__).parent.parent
22+
_AUTO_NOTE = Path.home() / ".auto_note"
23+
SCRIPTS_DIR = _AUTO_NOTE / "scripts"
24+
VENV_PYTHON = str(_AUTO_NOTE / "venv" / "bin" / "python")
25+
26+
pytestmark = pytest.mark.skipif(
27+
not Path(VENV_PYTHON).exists(),
28+
reason=f"Venv not found at {VENV_PYTHON} — run the app installer first",
29+
)
30+
31+
32+
def _venv_import(module: str) -> tuple[bool, str]:
33+
"""Return (success, error_msg) for importing module in the venv."""
34+
r = subprocess.run(
35+
[VENV_PYTHON, "-c", f"import {module}"],
36+
capture_output=True, text=True,
37+
)
38+
return r.returncode == 0, r.stderr.strip()
39+
40+
41+
def _assert_import(module: str, pip_name: str | None = None) -> None:
42+
ok, err = _venv_import(module)
43+
install_hint = pip_name or module
44+
assert ok, (
45+
f"Venv is missing '{module}' (install: pip install {install_hint}).\n"
46+
f"Error: {err}"
47+
)
48+
49+
50+
# ── Core packages (used at module-import time) ────────────────────────────────
51+
52+
class TestCorePackages:
53+
"""Every package imported at the top of a pipeline script must be present."""
54+
55+
def test_canvasapi(self):
56+
_assert_import("canvasapi")
57+
58+
def test_tqdm(self):
59+
_assert_import("tqdm")
60+
61+
def test_requests(self):
62+
_assert_import("requests")
63+
64+
def test_faiss(self):
65+
_assert_import("faiss")
66+
67+
def test_numpy(self):
68+
_assert_import("numpy")
69+
70+
def test_torch(self):
71+
_assert_import("torch")
72+
73+
74+
# ── Lazy / conditional packages ───────────────────────────────────────────────
75+
76+
class TestLazyPackages:
77+
"""Packages imported only inside functions / on first use."""
78+
79+
def test_faster_whisper(self):
80+
_assert_import("faster_whisper", "faster-whisper")
81+
82+
def test_sentence_transformers(self):
83+
_assert_import("sentence_transformers", "sentence-transformers")
84+
85+
def test_openai(self):
86+
_assert_import("openai")
87+
88+
def test_anthropic(self):
89+
_assert_import("anthropic")
90+
91+
def test_pymupdf(self):
92+
_assert_import("fitz", "pymupdf")
93+
94+
def test_pptx(self):
95+
_assert_import("pptx", "python-pptx")
96+
97+
def test_docx(self):
98+
_assert_import("docx", "python-docx")
99+
100+
def test_pillow(self):
101+
_assert_import("PIL", "pillow")
102+
103+
def test_playwright(self):
104+
_assert_import("playwright", "playwright")
105+
106+
def test_httpx(self):
107+
_assert_import("httpx")
108+
109+
def test_PanoptoDownloader(self):
110+
_assert_import(
111+
"PanoptoDownloader",
112+
"git+https://github.com/Panopto-Video-DL/Panopto-Video-DL-lib.git",
113+
)
114+
115+
def test_ffmpeg_progress_yield(self):
116+
_assert_import("ffmpeg_progress_yield", "ffmpeg-progress-yield")
117+
118+
def test_pycryptodomex(self):
119+
_assert_import("Cryptodome", "pycryptodomex")
120+
121+
122+
# ── Script-level importability ────────────────────────────────────────────────
123+
124+
class TestScriptImports:
125+
"""Each installed script must be importable from SCRIPTS_DIR."""
126+
127+
@pytest.fixture(autouse=True)
128+
def _skip_if_not_installed(self):
129+
if not SCRIPTS_DIR.exists():
130+
pytest.skip("Scripts not installed yet")
131+
132+
def _run_import(self, script_stem: str) -> tuple[bool, str]:
133+
r = subprocess.run(
134+
[VENV_PYTHON, "-c", f"import {script_stem}; print('OK')"],
135+
capture_output=True, text=True,
136+
cwd=str(SCRIPTS_DIR),
137+
)
138+
return r.returncode == 0, (r.stdout + r.stderr).strip()
139+
140+
def test_downloader_importable(self):
141+
ok, out = self._run_import("downloader")
142+
assert ok, f"downloader import failed:\n{out}"
143+
144+
def test_extract_caption_importable(self):
145+
ok, out = self._run_import("extract_caption")
146+
assert ok, f"extract_caption import failed:\n{out}"
147+
148+
def test_semantic_alignment_importable(self):
149+
ok, out = self._run_import("semantic_alignment")
150+
assert ok, f"semantic_alignment import failed:\n{out}"
151+
152+
def test_alignment_parser_importable(self):
153+
ok, out = self._run_import("alignment_parser")
154+
assert ok, f"alignment_parser import failed:\n{out}"
155+
156+
def test_note_generation_importable(self):
157+
ok, out = self._run_import("note_generation")
158+
assert ok, (
159+
f"note_generation import failed (alignment_parser missing?):\n{out}"
160+
)
161+
162+
def test_all_scripts_present(self):
163+
required = [
164+
"downloader.py",
165+
"extract_caption.py",
166+
"semantic_alignment.py",
167+
"alignment_parser.py",
168+
"note_generation.py",
169+
]
170+
for name in required:
171+
assert (SCRIPTS_DIR / name).exists(), (
172+
f"{name} not in {SCRIPTS_DIR} — "
173+
"_install_scripts() may not have copied it"
174+
)
175+
176+
177+
# ── CLI smoke tests (venv Python) ─────────────────────────────────────────────
178+
179+
class TestCLISmoke:
180+
"""--help on every script should exit 0 with the venv Python."""
181+
182+
@pytest.fixture(autouse=True)
183+
def _skip_if_not_installed(self):
184+
if not SCRIPTS_DIR.exists():
185+
pytest.skip("Scripts not installed yet")
186+
187+
def _help(self, script: str, timeout: int = 15) -> subprocess.CompletedProcess:
188+
return subprocess.run(
189+
[VENV_PYTHON, str(SCRIPTS_DIR / script), "--help"],
190+
capture_output=True, text=True, timeout=timeout,
191+
)
192+
193+
def test_downloader_help(self):
194+
r = self._help("downloader.py")
195+
assert r.returncode == 0, r.stderr[:500]
196+
assert "--material-list" in r.stdout
197+
198+
def test_extract_caption_help(self):
199+
r = self._help("extract_caption.py")
200+
assert r.returncode == 0, r.stderr[:500]
201+
assert "--video" in r.stdout
202+
203+
def test_note_generation_help(self):
204+
r = self._help("note_generation.py")
205+
assert r.returncode == 0, r.stderr[:500]
206+
assert "--course" in r.stdout
207+
208+
def test_downloader_no_args_prints_help(self):
209+
r = subprocess.run(
210+
[VENV_PYTHON, str(SCRIPTS_DIR / "downloader.py")],
211+
capture_output=True, text=True, timeout=10,
212+
)
213+
assert r.returncode == 0
214+
215+
def test_note_generation_missing_materials_clean_exit(self):
216+
"""note_generation with unknown course must print [error] not traceback."""
217+
r = subprocess.run(
218+
[VENV_PYTHON, str(SCRIPTS_DIR / "note_generation.py"),
219+
"--course", "99999"],
220+
capture_output=True, text=True, timeout=15,
221+
cwd=str(_AUTO_NOTE),
222+
)
223+
combined = r.stdout + r.stderr
224+
assert "Traceback" not in combined, (
225+
f"Unhandled traceback — should print [error] message:\n{combined[:1000]}"
226+
)
227+
assert r.returncode != 0 # should fail with sys.exit(1)
228+
229+
230+
# ── ffmpeg availability (needed by PanoptoDownloader) ─────────────────────────
231+
232+
class TestSystemDeps:
233+
def test_ffmpeg_on_path(self):
234+
"""PanoptoDownloader calls ffmpeg at runtime — it must be on PATH."""
235+
r = subprocess.run(
236+
["ffmpeg", "-version"], capture_output=True, text=True
237+
)
238+
assert r.returncode == 0, (
239+
"ffmpeg not found on PATH. Install with: sudo pacman -S ffmpeg"
240+
)
241+
242+
def test_ffprobe_on_path(self):
243+
r = subprocess.run(
244+
["ffprobe", "-version"], capture_output=True, text=True
245+
)
246+
assert r.returncode == 0, (
247+
"ffprobe not found on PATH. Install with: sudo pacman -S ffmpeg"
248+
)

0 commit comments

Comments
 (0)