Voice / tests /_helpers.py
Wiself's picture
Update voice.py, vlib, tests, bundles
4a1430c
Raw History Blame Contribute Delete
6.04 kB
"""Shared fixtures for the voice product suite. Stdlib only.
Top: unit + download tests run offline: python3 -m unittest discover -s tests
Live: VOICE_TEST_LIVE=1 enables test_live/test_voices/test_e2e (real HF fetches).
E2E: also needs a llama-cli binary (LLAMA_CLI or PATH); skips otherwise.
"""
import argparse
import importlib.util
import os
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
os.environ.setdefault("VOICE_NO_VENV", "1")
REPO_ROOT = Path(__file__).resolve().parent.parent
VOICE_PY = REPO_ROOT / "voice.py"
spec = importlib.util.spec_from_file_location("voice", str(VOICE_PY))
voice = importlib.util.module_from_spec(spec)
sys.modules["voice"] = voice
spec.loader.exec_module(voice)
# Live fixtures: small, stable, public.
CATGIRL_REPO = "xcx0902/Qwen3-1.7B-catgirl" # sharded safetensors finetune (the head)
ABL_REPO = "DevQuasar/huihui-ai.Qwen3-1.7B-abliterated-GGUF"
ABL_FILE = "huihui-ai.Qwen3-1.7B-abliterated.Q8_0.gguf" # quant head for passthrough
BASE_REPO = "unsloth/Qwen3-1.7B-GGUF"
BASE_FILE = "Qwen3-1.7B-UD-IQ2_XXS.gguf" # smallest full GGUF: cast target + runner
LIVE = os.environ.get("VOICE_TEST_LIVE") == "1"
def find_llama_cli():
# LLAMA_CLI env wins, then PATH. No machine paths: shippable anywhere.
import shutil
env = os.environ.get("LLAMA_CLI")
if env and Path(env).is_file():
return env
return shutil.which("llama-cli")
LLAMA_CLI = find_llama_cli()
needs_llama = unittest.skipUnless(
LIVE and LLAMA_CLI, "needs VOICE_TEST_LIVE=1 and a llama-cli binary")
def cache_dir():
d = Path(os.environ.get("VOICE_TEST_CACHE") or (Path.home() / ".cache" / "voice-tests"))
d.mkdir(parents=True, exist_ok=True)
return d
def fetch_file(url, dest, label="", n_parts=8):
"""Full-file fetch through the product's own download_range (resumeable)."""
dest = Path(dest)
if dest.is_file() and dest.stat().st_size > 0:
return dest
import time
import urllib.request
last = None
for attempt in range(6):
req = urllib.request.Request(url, method="HEAD")
try:
with urllib.request.urlopen(req, timeout=60) as r:
size = int(r.headers["Content-Length"])
etag = r.headers.get("ETag")
break
except urllib.error.HTTPError as e:
last = e
if e.code in (429, 500, 502, 503) and attempt < 5:
time.sleep(voice._bounded_retry_after(
getattr(e, "headers", None) or {}, 2.0 * (2 ** attempt), cap=60))
continue
raise
else:
raise last
part = dest.parent / (dest.name + ".parts")
combined = voice.download_range(url, 0, size - 1, part, etag, label=label or dest.name,
n_parts=n_parts)
import shutil
shutil.move(str(combined), str(dest))
return dest
class IsolatedVoices(unittest.TestCase):
"""Each test gets a fresh VOICES_DIR; restored afterwards."""
def setUp(self):
self._old = voice.VOICES_DIR
self.vdir = Path(tempfile.mkdtemp(prefix="voices-"))
voice.VOICES_DIR = self.vdir
voice_ctx = sys.modules.get("vlib.ctx")
if voice_ctx is not None:
voice_ctx.VOICES_DIR = self.vdir
# registry + reports + parts all derive from VOICES_DIR at call time,
# except these module constants: repoint them too.
self._old_reports = (voice.REPORTS_JSONL, voice.REPORTS_OUTBOX, voice.OPERATIONS_LOG,
voice.HF_TOKEN_PATH)
voice.REPORTS_JSONL = self.vdir / "reports.jsonl"
voice.REPORTS_OUTBOX = self.vdir / "outbox"
voice.OPERATIONS_LOG = self.vdir / "operations.log"
voice.HF_TOKEN_PATH = self.vdir / ".hf_token"
if voice_ctx is not None:
voice_ctx.REPORTS_JSONL = voice.REPORTS_JSONL
voice_ctx.REPORTS_OUTBOX = voice.REPORTS_OUTBOX
voice_ctx.OPERATIONS_LOG = voice.OPERATIONS_LOG
voice_ctx.HF_TOKEN_PATH = voice.HF_TOKEN_PATH
def tearDown(self):
voice.VOICES_DIR = self._old
(voice.REPORTS_JSONL, voice.REPORTS_OUTBOX, voice.OPERATIONS_LOG,
voice.HF_TOKEN_PATH) = self._old_reports
voice_ctx = sys.modules.get("vlib.ctx")
if voice_ctx is not None:
voice_ctx.VOICES_DIR = self._old
(voice_ctx.REPORTS_JSONL, voice_ctx.REPORTS_OUTBOX, voice_ctx.OPERATIONS_LOG,
voice_ctx.HF_TOKEN_PATH) = self._old_reports
shutil.rmtree(self.vdir, ignore_errors=True)
def ns(**kw):
a = dict(non_interactive=True, yes=True, verbose=False)
a.update(kw)
return argparse.Namespace(**a)
def run_guarded(argv, timeout, **kw):
"""subprocess.run with a kill guarantee: no orphan survives a timeout,
a KeyboardInterrupt, or a runner kill. Returns CompletedProcess; raises
subprocess.TimeoutExpired (after reaping) or KeyboardInterrupt (clean)."""
import signal
import subprocess
kw.setdefault("stdin", subprocess.DEVNULL)
proc = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, **{k: v for k, v in kw.items()
if k in ("stdin", "cwd", "env")})
try:
out, err = proc.communicate(timeout=timeout)
return subprocess.CompletedProcess(argv, proc.returncode, out, err)
except subprocess.TimeoutExpired:
proc.kill()
try:
out, err = proc.communicate(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
out, err = proc.communicate()
raise subprocess.TimeoutExpired(argv, timeout, out, err)
except BaseException:
# KeyboardInterrupt / runner death: take the child with us, then propagate
try:
proc.kill()
except Exception:
pass
try:
proc.wait(timeout=10)
except Exception:
pass
raise