ZONOS3-Mini / tests /test_lifecycle.py
fffiloni's picture
Deploy ZONOS3 Mini ZeroGPU integration v0.1.0
6d1acb0 verified
Raw History Blame Contribute Delete
5.79 kB
from concurrent.futures import ThreadPoolExecutor
import json
import threading
import time
from types import SimpleNamespace
import gradio as gr
import numpy as np
import pytest
import soundfile as sf
from controller import Controller
from lifecycle import AlreadyRunning, Cancelled, Jobs, check_cancelled
class FakeEngine:
"""Lifecycle fixture only. This is not an inference or audio-quality test."""
def __init__(self, fail=False):
self.calls=0; self.active=0; self.peak=0; self.lock=threading.Lock(); self.fail=fail
def prepare(self, request, reference):
return request
def run(self, request, marker, progress):
with self.lock:
self.calls+=1; self.active+=1; self.peak=max(self.peak,self.active)
try:
time.sleep(.02)
check_cancelled(marker)
if self.fail: raise RuntimeError("private user text must not be exposed")
return SimpleNamespace(pcm=np.zeros(4410,dtype=np.float32),seconds=.02,cancelled=False)
finally:
with self.lock:self.active-=1
def invoke(controller, session="session-a", **changes):
args=dict(text="A short test.",picker=json.dumps({"voice":"Savannah","language":"en_us"}),
source="Preset",clip=None,consent=False,normalize=True,seed=42,randomize=False,
temperature=.75,top_p=.69,top_k=80,repetition_penalty=2.4,rate=0,
request=gr.Request(session_hash=session), progress=lambda *a,**k:None)
args.update(changes)
return controller.generate(**args)
def test_double_submit_and_owner_isolation(tmp_path):
jobs=Jobs(tmp_path)
a=jobs.begin("a"); b=jobs.begin("b")
with pytest.raises(AlreadyRunning):jobs.begin("a")
assert jobs.cancel("a")
with pytest.raises(Cancelled):check_cancelled(a.marker)
check_cancelled(b.marker)
jobs.finish(a);jobs.finish(b)
assert jobs.active()==0 and not list(tmp_path.glob("*.cancel"))
def test_success_cleanup_precedes_final_yield(tmp_path):
c=Controller(FakeEngine(),tmp_path,["Savannah"])
stream=invoke(c)
next(stream) # request received
next(stream) # GPU returned; finalizing
result=next(stream)
assert c.jobs.active()==0
assert c.gate.acquire(blocking=False)
c.gate.release()
payload=json.loads(result[0]); assert payload["seed"]==42
assert len(payload["wave"])==640
wav=list(c.outputs.glob("*.wav")); assert len(wav)==1
assert sf.info(wav[0]).samplerate==44100
stream.close() # Consumer never needs to resume to trigger cleanup.
def test_cancel_before_gpu_and_retry(tmp_path):
engine=FakeEngine();c=Controller(engine,tmp_path,["Savannah"])
stream=invoke(c);next(stream)
c.cancel(gr.Request(session_hash="session-a"))
values=list(stream)
assert "Cancelled" in values[-1][1] and values[-1][0]==gr.skip()
assert engine.calls==0 and c.jobs.active()==0
assert not list(c.outputs.glob("*.wav"))
assert "Ready" in list(invoke(c))[-1][1]
def test_generator_closed_early_cleans_job(tmp_path):
c=Controller(FakeEngine(),tmp_path,["Savannah"])
stream=invoke(c);next(stream);stream.close()
assert c.jobs.active()==0
def test_engine_failure_keeps_previous_audio_and_releases_slot(tmp_path):
e=FakeEngine();c=Controller(e,tmp_path,["Savannah"])
first=list(invoke(c))[-1][0]
files_before=set(c.outputs.glob("*.wav"));e.fail=True
stream=invoke(c);seen=[]
with pytest.raises(gr.Error):
for value in stream:seen.append(value)
assert seen[-1][0]==gr.skip() and seen[-1][2]["interactive"] is True
assert "private user text" not in seen[-1][1]
assert c.jobs.active()==0 and e.active==0
assert set(c.outputs.glob("*.wav"))==files_before
assert json.loads(first)["url"]
def test_cancel_during_gpu_callback(tmp_path):
entered=threading.Event();resume=threading.Event()
class Running(FakeEngine):
def run(self, request, marker, progress):
entered.set();assert resume.wait(2);check_cancelled(marker)
c=Controller(Running(),tmp_path,["Savannah"])
with ThreadPoolExecutor(1) as pool:
future=pool.submit(lambda:list(invoke(c)))
assert entered.wait(2)
c.cancel(gr.Request(session_hash="session-a"));resume.set()
assert "Cancelled" in future.result(timeout=2)[-1][1]
assert c.jobs.active()==0 and not list(c.outputs.glob("*.wav"))
def test_two_sessions_share_one_inference_slot_and_separate_files(tmp_path):
e=FakeEngine();c=Controller(e,tmp_path,["Savannah"])
with ThreadPoolExecutor(2) as pool:
results=list(pool.map(lambda session:list(invoke(c,session))[-1], ["a","b"]))
assert e.peak==1 and e.calls==2 and c.jobs.active()==0
assert json.loads(results[0][0])["url"]!=json.loads(results[1][0])["url"]
def test_waiting_for_slot_is_cancellable(tmp_path):
c=Controller(FakeEngine(),tmp_path,["Savannah"]);c.gate.acquire()
stream=invoke(c);next(stream)
c.cancel(gr.Request(session_hash="session-a"))
assert "Cancelled" in list(stream)[-1][1]
c.gate.release();assert c.jobs.active()==0
def test_validation_error_is_an_api_error_without_gpu_call(tmp_path):
e=FakeEngine();c=Controller(e,tmp_path,["Savannah"])
stream=invoke(c,text="")
first=next(stream)
assert first[0]==gr.skip()
with pytest.raises(gr.Error):next(stream)
assert e.calls==0 and c.jobs.active()==0
def test_duplicate_api_request_cannot_reset_active_controls(tmp_path):
c=Controller(FakeEngine(),tmp_path,["Savannah"])
running=invoke(c);next(running)
duplicate=invoke(c);reply=next(duplicate)
assert reply[2]==gr.skip() and reply[3]==gr.skip()
with pytest.raises(gr.Error):next(duplicate)
assert c.jobs.active()==1
running.close();assert c.jobs.active()==0