refactor (untested)
This commit is contained in:
29
tests/__init__.py
Normal file
29
tests/__init__.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""Smoke + focused tests for meetus.
|
||||
|
||||
Run from the repo root:
|
||||
python -m unittest discover -s tests -t . -v
|
||||
|
||||
This package's import makes the repo importable and provides a `cv2` stub (only
|
||||
cv2 is missing in minimal envs; numpy/PIL are real and not shadowed).
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
STUBS_DIR = Path(__file__).resolve().parent / "_stubs"
|
||||
|
||||
for _p in (str(STUBS_DIR), str(REPO_ROOT)):
|
||||
if _p not in sys.path:
|
||||
sys.path.insert(0, _p)
|
||||
|
||||
|
||||
def subprocess_env(extra=None):
|
||||
"""Env for subprocess tests: repo root + cv2 stub on PYTHONPATH."""
|
||||
env = dict(os.environ)
|
||||
pp = env.get("PYTHONPATH", "")
|
||||
parts = [str(REPO_ROOT), str(STUBS_DIR)] + ([pp] if pp else [])
|
||||
env["PYTHONPATH"] = os.pathsep.join(parts)
|
||||
if extra:
|
||||
env.update(extra)
|
||||
return env
|
||||
2
tests/_stubs/cv2.py
Normal file
2
tests/_stubs/cv2.py
Normal file
@@ -0,0 +1,2 @@
|
||||
# Empty stub so meetus.frame_extractor (which does `import cv2`) imports in test
|
||||
# environments without OpenCV. Tests never exercise cv2 functionality.
|
||||
109
tests/test_batch.py
Normal file
109
tests/test_batch.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Deeper tests for ctrl/batch.sh: recursive + space-safe discovery, audio
|
||||
inclusion, structure mirroring, flag forwarding, failure resilience, and the
|
||||
stdin-isolation fix (a greedy child must not stall the batch)."""
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tests import REPO_ROOT
|
||||
|
||||
BATCH = REPO_ROOT / "ctrl" / "batch.sh"
|
||||
|
||||
|
||||
def run_batch(args, env_extra=None):
|
||||
env = dict(os.environ)
|
||||
if env_extra:
|
||||
env.update(env_extra)
|
||||
return subprocess.run(["bash", str(BATCH), *args], cwd=REPO_ROOT, env=env,
|
||||
capture_output=True, text=True)
|
||||
|
||||
|
||||
def stub(d, body):
|
||||
p = d / "pystub"
|
||||
p.write_text("#!/bin/bash\n" + body + "\n")
|
||||
p.chmod(0o755)
|
||||
return str(p)
|
||||
|
||||
|
||||
class TestBatchDry(unittest.TestCase):
|
||||
def test_recursive_spaces_audio_and_mirror(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ind = tmp / "in"
|
||||
(ind / "team a" / "sub one").mkdir(parents=True)
|
||||
(ind / "team a" / "sub one" / "session 1.mkv").touch()
|
||||
(ind / "clip.MP4").touch() # case-insensitive
|
||||
(ind / "radio.ogg").touch() # audio
|
||||
(ind / "notes.txt").touch() # ignored
|
||||
r = run_batch(["-i", str(ind), "-o", str(tmp / "out"), "-n"])
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
out = r.stdout
|
||||
self.assertIn("session 1.mkv", out)
|
||||
self.assertIn("clip.MP4", out)
|
||||
self.assertIn("radio.ogg", out)
|
||||
self.assertNotIn("notes.txt", out)
|
||||
self.assertIn("Found 3 video", out)
|
||||
self.assertIn(str(tmp / "out" / "team a" / "sub one"), out)
|
||||
|
||||
|
||||
class TestBatchRun(unittest.TestCase):
|
||||
def test_forward_flags_and_mirror(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ind = tmp / "in" / "team a"
|
||||
ind.mkdir(parents=True)
|
||||
(ind / "session 1.mkv").touch()
|
||||
r = run_batch(["-i", str(tmp / "in"), "-o", str(tmp / "out"),
|
||||
"--", "--embed-images", "--diarize"],
|
||||
env_extra={"PYTHON": "echo"})
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
line = next(l for l in r.stdout.splitlines() if "process_meeting.py" in l)
|
||||
self.assertIn("--output-dir", line)
|
||||
self.assertIn(str(tmp / "out" / "team a"), line)
|
||||
self.assertIn("--embed-images", line)
|
||||
self.assertIn("session 1.mkv", line)
|
||||
|
||||
def test_empty_forward_does_not_crash(self):
|
||||
# set -u + empty FORWARD array guard.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ind = tmp / "in"
|
||||
ind.mkdir()
|
||||
(ind / "x.mp4").touch()
|
||||
r = run_batch(["-i", str(ind), "-o", str(tmp / "out")],
|
||||
env_extra={"PYTHON": "echo"})
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("process_meeting.py", r.stdout)
|
||||
|
||||
def test_continues_past_failures(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ind = tmp / "in"
|
||||
ind.mkdir()
|
||||
(ind / "a.mp4").touch()
|
||||
(ind / "b.mp4").touch()
|
||||
r = run_batch(["-i", str(ind), "-o", str(tmp / "out")],
|
||||
env_extra={"PYTHON": stub(tmp, "exit 1")})
|
||||
self.assertNotEqual(r.returncode, 0) # nonzero when any failed
|
||||
self.assertIn("0 ok, 2 failed", r.stdout)
|
||||
self.assertIn("FAILED", r.stderr)
|
||||
|
||||
def test_stdin_isolation_processes_all_items(self):
|
||||
# A child that drains stdin (like ffmpeg) must NOT eat the file list.
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp = Path(tmp)
|
||||
ind = tmp / "in"
|
||||
ind.mkdir()
|
||||
for n in ("a.mp4", "b.mp4", "c.mp4"):
|
||||
(ind / n).touch()
|
||||
greedy = stub(tmp, 'shift; cat >/dev/null; echo "DID $1"')
|
||||
r = run_batch(["-i", str(ind), "-o", str(tmp / "out")],
|
||||
env_extra={"PYTHON": greedy})
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertEqual(r.stdout.count("DID "), 3)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
77
tests/test_config.py
Normal file
77
tests/test_config.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Deeper tests for WorkflowConfig — the --transcript-formats parsing and the
|
||||
post-cleanup shape (embed-images only; deprecated analysis attrs gone)."""
|
||||
import unittest
|
||||
|
||||
from meetus.workflow import WorkflowConfig
|
||||
|
||||
|
||||
def cfg(**kw):
|
||||
kw.setdefault("video", "sample.mp4")
|
||||
return WorkflowConfig(**kw)
|
||||
|
||||
|
||||
class TestTranscriptFormats(unittest.TestCase):
|
||||
def test_default_is_json_only(self):
|
||||
self.assertEqual(cfg().transcript_formats, ["json"])
|
||||
|
||||
def test_srt_implies_json(self):
|
||||
# User shouldn't have to list json; it's always the base.
|
||||
self.assertEqual(cfg(transcript_formats="srt").transcript_formats,
|
||||
["json", "srt"])
|
||||
|
||||
def test_multiple(self):
|
||||
self.assertEqual(cfg(transcript_formats="srt,txt").transcript_formats,
|
||||
["json", "srt", "txt"])
|
||||
|
||||
def test_all_expands(self):
|
||||
self.assertEqual(set(cfg(transcript_formats="all").transcript_formats),
|
||||
{"json", "srt", "vtt", "txt", "tsv"})
|
||||
|
||||
def test_dedup_and_json_kept(self):
|
||||
self.assertEqual(cfg(transcript_formats="json,srt,srt").transcript_formats,
|
||||
["json", "srt"])
|
||||
|
||||
def test_whitespace_and_case(self):
|
||||
self.assertEqual(cfg(transcript_formats=" SRT , Txt ").transcript_formats,
|
||||
["json", "srt", "txt"])
|
||||
|
||||
def test_invalid_raises(self):
|
||||
with self.assertRaises(ValueError):
|
||||
cfg(transcript_formats="srt,bogus")
|
||||
|
||||
|
||||
class TestDefaults(unittest.TestCase):
|
||||
def test_scalar_defaults(self):
|
||||
c = cfg()
|
||||
self.assertEqual(c.embed_quality, 80)
|
||||
self.assertEqual(c.format, "detailed")
|
||||
self.assertEqual(c.whisper_model, "medium")
|
||||
self.assertEqual(c.scene_threshold, 15.0)
|
||||
|
||||
def test_to_dict_analysis_is_embed_images(self):
|
||||
self.assertEqual(cfg().to_dict()["analysis"]["method"], "embed-images")
|
||||
|
||||
def test_to_dict_whisper_has_diarize_and_formats(self):
|
||||
d = cfg(diarize=True, transcript_formats="srt").to_dict()
|
||||
self.assertTrue(d["whisper"]["diarize"])
|
||||
self.assertEqual(d["whisper"]["transcript_formats"], ["json", "srt"])
|
||||
|
||||
|
||||
class TestDeprecatedRemoved(unittest.TestCase):
|
||||
"""The OCR/vision/hybrid surface was removed in the cleanup."""
|
||||
|
||||
def test_no_deprecated_attrs(self):
|
||||
c = cfg()
|
||||
for attr in ("use_vision", "use_hybrid", "hybrid_llm_cleanup",
|
||||
"hybrid_llm_model", "vision_model", "vision_context",
|
||||
"ocr_engine", "no_deduplicate"):
|
||||
self.assertFalse(hasattr(c, attr), f"{attr} should be removed")
|
||||
|
||||
def test_extra_kwargs_are_ignored(self):
|
||||
# Stray kwargs (e.g. an old flag) must not crash construction.
|
||||
c = cfg(use_vision=True, ocr_engine="tesseract")
|
||||
self.assertFalse(hasattr(c, "use_vision"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
55
tests/test_makefile.py
Normal file
55
tests/test_makefile.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Smoke for the Makefile wrapper: OUT defaults to IN, the baked-in FLAGS, and
|
||||
the AUDIO_LANG knob (named to avoid the LANG locale env var)."""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tests import REPO_ROOT
|
||||
|
||||
|
||||
def make(target, **vars):
|
||||
args = ["make", target] + [f"{k}={v}" for k, v in vars.items()]
|
||||
return subprocess.run(args, cwd=REPO_ROOT, capture_output=True, text=True,
|
||||
env=dict(os.environ))
|
||||
|
||||
|
||||
@unittest.skipUnless(shutil.which("make"), "make not installed")
|
||||
class TestMakefile(unittest.TestCase):
|
||||
def test_dry_out_defaults_to_in(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
ind = Path(tmp) / "in"
|
||||
ind.mkdir()
|
||||
(ind / "x.mp4").touch()
|
||||
r = make("dry", IN=str(ind))
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn(f"Output: {ind}", r.stdout)
|
||||
self.assertIn("Found 1 video", r.stdout)
|
||||
|
||||
def test_batch_baked_in_flags(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
ind = Path(tmp) / "in"
|
||||
ind.mkdir()
|
||||
(ind / "x.mp4").touch()
|
||||
r = make("batch", IN=str(ind), PYTHON="echo")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
line = next(l for l in r.stdout.splitlines() if "process_meeting.py" in l)
|
||||
self.assertIn("--scene-threshold 10", line)
|
||||
self.assertIn("--transcript-formats srt", line)
|
||||
self.assertIn("--embed-images", line)
|
||||
self.assertNotIn("--language", line) # no AUDIO_LANG -> no --language
|
||||
|
||||
def test_audio_lang_appends_language(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
ind = Path(tmp) / "in"
|
||||
ind.mkdir()
|
||||
(ind / "x.mp4").touch()
|
||||
r = make("batch", IN=str(ind), PYTHON="echo", AUDIO_LANG="es")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("--language es", r.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
57
tests/test_smoke.py
Normal file
57
tests/test_smoke.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Smoke: the wired package imports, deprecated stays unwired, and the cleaned
|
||||
CLI surface is what we expect."""
|
||||
import subprocess
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from tests import REPO_ROOT, subprocess_env
|
||||
|
||||
|
||||
class TestImports(unittest.TestCase):
|
||||
def test_core_modules_import(self):
|
||||
import meetus # noqa: F401
|
||||
import meetus.workflow # noqa: F401 (frame_extractor → cv2 stub)
|
||||
import meetus.frame_extractor # noqa: F401
|
||||
import meetus.transcript_merger # noqa: F401
|
||||
import meetus.output_manager # noqa: F401
|
||||
import meetus.cache_manager # noqa: F401
|
||||
|
||||
def test_deprecated_importable_for_reference(self):
|
||||
import meetus.deprecated.ocr_processor # noqa: F401
|
||||
import meetus.deprecated.vision_processor # noqa: F401
|
||||
|
||||
def test_workflow_does_not_import_deprecated(self):
|
||||
src = (REPO_ROOT / "meetus" / "workflow.py").read_text()
|
||||
self.assertNotIn("from .deprecated", src)
|
||||
self.assertNotIn("import deprecated", src)
|
||||
|
||||
|
||||
class TestCliHelp(unittest.TestCase):
|
||||
def _help(self, rel):
|
||||
return subprocess.run(
|
||||
[sys.executable, str(REPO_ROOT / rel), "--help"],
|
||||
cwd=REPO_ROOT, env=subprocess_env(), capture_output=True, text=True,
|
||||
)
|
||||
|
||||
def test_process_meeting_help_is_clean(self):
|
||||
r = self._help("process_meeting.py")
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
out = r.stdout
|
||||
for kept in ("--embed-images", "--diarize", "--transcript-formats",
|
||||
"--scene-detection", "--skip-cache-frames", "--extract-only"):
|
||||
self.assertIn(kept, out, f"{kept} should still be offered")
|
||||
for gone in ("--use-vision", "--use-hybrid", "--ocr-engine",
|
||||
"--hybrid-llm", "--vision-model", "--vision-context",
|
||||
"--no-deduplicate", "--skip-cache-analysis"):
|
||||
self.assertNotIn(gone, out, f"{gone} should be removed from the CLI")
|
||||
|
||||
def test_summarizers_offer_output_dir(self):
|
||||
for rel in ("ctrl/summarize/summarize_simple.py",
|
||||
"ctrl/summarize/compile_meeting.py"):
|
||||
r = self._help(rel)
|
||||
self.assertEqual(r.returncode, 0, r.stderr)
|
||||
self.assertIn("--output-dir", r.stdout, rel)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
46
tests/test_summarize.py
Normal file
46
tests/test_summarize.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Deeper test for the summarizer --output-dir work: compile_meeting.default_output
|
||||
(strips _enhanced, honors a base dir)."""
|
||||
import importlib.util
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from tests import REPO_ROOT
|
||||
|
||||
|
||||
def _load(rel, name):
|
||||
spec = importlib.util.spec_from_file_location(name, REPO_ROOT / rel)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
class TestCompileDefaultOutput(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.cm = _load("ctrl/summarize/compile_meeting.py", "compile_meeting")
|
||||
|
||||
def test_next_to_transcript_strips_enhanced(self):
|
||||
t = Path("/x/20260101-001-foo/foo_enhanced.txt")
|
||||
self.assertEqual(self.cm.default_output(t, "reference"),
|
||||
Path("/x/20260101-001-foo/foo_reference.md"))
|
||||
|
||||
def test_base_dir_override(self):
|
||||
t = Path("/x/20260101-001-foo/foo_enhanced.txt")
|
||||
self.assertEqual(self.cm.default_output(t, "reference", Path("/out")),
|
||||
Path("/out/foo_reference.md"))
|
||||
|
||||
def test_no_enhanced_suffix(self):
|
||||
t = Path("/x/run/foo.txt")
|
||||
self.assertEqual(self.cm.default_output(t, "reference"),
|
||||
Path("/x/run/foo_reference.md"))
|
||||
|
||||
def test_run_folder_mirroring(self):
|
||||
# How main() builds base_dir for --output-dir: <out>/<transcript's run dir>.
|
||||
t = Path("/runs/20260101-001-foo/foo_enhanced.txt")
|
||||
base = Path("/out") / t.parent.name
|
||||
self.assertEqual(self.cm.default_output(t, "reference", base),
|
||||
Path("/out/20260101-001-foo/foo_reference.md"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user