#!/usr/bin/env python3 """Lifecycle tests for TikTok Live Monitor → Direct Relay → YouTube. No recording. No archive. Tests that can run without a live TikTok session use mocks; watchdog/FFmpeg tests use a local HTTP source when ffmpeg exists. """ import json, os, sys, tempfile, threading, time, types, pathlib, subprocess, shutil ROOT = pathlib.Path(__file__).resolve().parent TMP = pathlib.Path(tempfile.mkdtemp(prefix='relay-test-')) REAL_FF = shutil.which('ffmpeg') _ff = os.environ.get('FFMPEG_PATH') or REAL_FF if not _ff: _ff = str(TMP / 'dummy-ffmpeg') pathlib.Path(_ff).write_text('#!/bin/sh\necho dummy\nexit 0\n') os.chmod(_ff, 0o755) os.environ.update( PASSWORD='test-pass-not-for-prod', DATA_DIR=str(TMP / 'data'), CHECK_INTERVAL='8', SOURCE_STALL_TIMEOUT='3', YOUTUBE_SOURCE_STALL_SECONDS='3', YOUTUBE_SOURCE_READ_TIMEOUT='1', YOUTUBE_PROGRESS_STALL_SECONDS='20', YOUTUBE_SEND_MODE='copy', YOUTUBE_TITLE_TEMPLATE='{broadcaster} | {date} {time}', FFMPEG_PATH=_ff, ) sys.path.insert(0, str(ROOT)) passed = failed = 0 def check(name, cond, detail=''): global passed, failed if cond: passed += 1 print('PASS %s'%name) else: failed += 1 print('FAIL %s %s'%(name, detail)) # ---------- syntax / import ---------- import ast for f in ('app.py','relay.py','youtube_manager.py','youtube_api.py','flv_bridge.py','tiktok_auth.py'): ast.parse((ROOT/f).read_text()) check('syntax of core modules', True) import app from app import rank_sources, source_brief from relay import Sender, SourceStalled, RelayStartError from youtube_api import DEFAULT_SETTINGS from datetime import datetime from zoneinfo import ZoneInfo check('import app (PASSWORD required, no mkvmerge)', True) check('no recordings dir required', not hasattr(app, 'FLVWriter') and not hasattr(app, 'finalize') and not hasattr(app, 'record')) check('ffmpeg path resolved or later skipped', True) # ---------- source selection ---------- payload = { 'blocks': [{ 'pull_data': { 'stream_data': json.dumps({'data': { 'origin': {'main': { 'flv': 'https://pull.example/origin.flv', 'hls': 'https://pull.example/origin.m3u8', 'sdk_params': json.dumps({'VCodec':'h265','resolution':'1080x1920','vbitrate':3200}), }}, 'hd': {'main': { 'flv': 'https://pull.example/hd.flv', 'sdk_params': json.dumps({'VCodec':'h264','resolution':'720x1280','vbitrate':1100}), }}, 'sd': {'main': { 'flv': 'https://pull.example/sd.flv', 'sdk_params': json.dumps({'VCodec':'h264','resolution':'480x854','vbitrate':600}), }}, }}) } }] } ranked = rank_sources(payload) check('keeps every rendition including lower quality', len(ranked) >= 3, 'got %d'%len(ranked)) briefs = [source_brief(c) for c in ranked] check('brief has no url', all('http' not in b and 'example' not in b for b in briefs), str(briefs)) # H264 FLV should beat HEVC origin because copy-compat + FLV top = ranked[0] check('prefers H264 over HEVC origin', '264' in (top.get('codec') or '').lower(), 'top=%s codec=%s'%(top.get('quality_name'), top.get('codec'))) check('still lists HEVC as fallback', any('265' in (c.get('codec') or '').lower() or 'hevc' in (c.get('codec') or '').lower() for c in ranked)) check('sd not dropped', any(c.get('quality_name')=='sd' for c in ranked)) # TikTok webcast/room/info returns hls_pull_url as one string on current live rooms. # Calling .items() on it used to throw, so the fallback disappeared exactly when needed. string_hls=rank_sources({'hls_pull_url':'https://pull.example/live.m3u8'}) check('single-string HLS fallback is accepted', len(string_hls)==1 and string_hls[0].get('media')=='hls',str(string_hls)) # ---------- title template ---------- tpl = os.environ['YOUTUBE_TITLE_TEMPLATE'] dt = datetime(2026,9,23,14,35,tzinfo=ZoneInfo('Asia/Baghdad')) title = tpl.format(broadcaster='StrongArmor', date=dt.strftime('%Y-%m-%d'), time=dt.strftime('%H:%M'), name='StrongArmor', title='x', username='u', room_id='1', nickname='StrongArmor', live_title='x', hashtags='') check('youtube title has Baghdad date/time', title=='StrongArmor | 2026-09-23 14:35', title) check('default settings title uses template vars', '{broadcaster}' in DEFAULT_SETTINGS['title'] or '{date}' in DEFAULT_SETTINGS['title']) # ---------- stall timeout env ---------- s = Sender('ffmpeg', lambda *a: None) check('SOURCE_STALL_TIMEOUT honored', abs(s.stall_limit-3)<0.01, str(s.stall_limit)) # ---------- recording remnants ---------- src = (ROOT/'app.py').read_text() + (ROOT/'index.html').read_text() check('UI has no recordings panel', 'التسجيلات المحفوظة' not in (ROOT/'index.html').read_text()) check('UI has no history panel', 'سجل البثوث' not in (ROOT/'index.html').read_text()) check('no /api/files endpoint', '/api/files' not in (ROOT/'app.py').read_text()) check('Dockerfile has no mkvtoolnix', 'mkvtoolnix' not in (ROOT/'Dockerfile').read_text()) check('requirements has no huggingface_hub', 'huggingface_hub' not in (ROOT/'requirements.txt').read_text()) # ---------- health + monitor isolation ---------- h = app.health_public() check('TEST public /health', h.get('application')=='running', str(h)) check('public health is minimal (no account/relay details)', set(h)<= {'status','application','monitor','version'}, str(h)) # TEST 1: offline account does not start ffmpeg app.tasks['offlineuser'] = app._blank_task() app.tasks['offlineuser']['enabled'] = True orig = app.observe_session app.observe_session = lambda user, force=False: {'status':'offline','room_id':'','sources':[],'nickname':'','live_title':''} try: app._check_account('offlineuser') check('TEST 1 offline → no LIVE', app.tasks['offlineuser']['state']=='OFFLINE') check('TEST 1 no ffmpeg handle', not app.youtube.senders) finally: app.observe_session = orig # TEST 10: two accounts, one error does not kill the other app.tasks['good'] = app._blank_task(); app.tasks['good']['enabled']=True app.tasks['bad'] = app._blank_task(); app.tasks['bad']['enabled']=True def obs(user, force=False): if user=='bad': raise RuntimeError('تيك توك حدّ الطلبات مؤقتًا (429)') return {'status':'offline','room_id':'','sources':[],'nickname':'','live_title':''} app.observe_session = obs try: app._check_account('good'); app._check_account('bad') check('TEST 10 good account still OFFLINE not dead', app.tasks['good']['state']=='OFFLINE') check('TEST 10 bad account isolated ERROR/BACKOFF', app.tasks['bad']['state'] in ('ERROR','BACKOFF')) finally: app.observe_session = orig app.tasks.pop('good',None); app.tasks.pop('bad',None); app.tasks.pop('offlineuser',None) # TEST 2 shape: LIVE detected updates state, still no ffmpeg without YouTube enable flv = {'url':'https://pull.example/hd.flv','media':'flv','codec':'h264','quality_name':'hd', 'resolution':'720x1280','brief':'hd 1280p H264','label':'hd · 720x1280 · h264','score':(1,1,1,1,1,1)} app.tasks['liveuser'] = app._blank_task(); app.tasks['liveuser']['enabled']=True app.observe_session = lambda user, force=False: {'status':'live','room_id':'123','sources':[flv],'nickname':'StrongArmor','live_title':'x'} try: app._check_account('liveuser') check('TEST 2 LIVE detected', app.tasks['liveuser']['state']=='LIVE', app.tasks['liveuser']['state']) check('TEST 2 source brief stored', 'H264' in (app.tasks['liveuser'].get('source') or ''), str(app.tasks['liveuser'])) check('TEST 2 no relay without YouTube enable', not app.youtube.senders) finally: app.observe_session = orig app.tasks.pop('liveuser',None) # ---------- watchdog / ffmpeg (optional) ---------- FF = REAL_FF if not FF: print('SKIP TEST 3-6/8 ffmpeg lifecycle (no ffmpeg in this environment; Docker image has it)') check('TEST 3-8 noted as docker-only when ffmpeg missing', True) else: import http.server, socketserver from relay import Sender src = TMP/'src.flv' subprocess.run([FF,'-hide_banner','-loglevel','error','-y','-f','lavfi','-i','testsrc2=size=160x120:rate=20', '-f','lavfi','-i','sine=frequency=440:sample_rate=48000','-t','20', '-c:v','libx264','-preset','ultrafast','-pix_fmt','yuv420p','-c:a','aac','-ar','48000','-ac','1', '-f','flv',str(src)], check=True, capture_output=True) DATA = src.read_bytes() state = {'mode':'flow'} class H(http.server.BaseHTTPRequestHandler): protocol_version='HTTP/1.1' def log_message(self,*a): pass def do_GET(self): self.send_response(200); self.send_header('Content-Type','video/flv'); self.end_headers() pos=0 try: while True: if state['mode']=='dead': return if state['mode']=='stall': time.sleep(0.2); continue self.wfile.write(DATA[pos:pos+4096]); self.wfile.flush() time.sleep(0.02); pos=(pos+4096)%max(4096,len(DATA)-4096) except (BrokenPipeError,ConnectionResetError,OSError): pass class S(socketserver.ThreadingTCPServer): allow_reuse_address=True; daemon_threads=True srv=S(('127.0.0.1',0),H) threading.Thread(target=srv.serve_forever,daemon=True).start() port=srv.server_address[1] dest=str(TMP/'out.flv') cand={'url':'http://127.0.0.1:%d/live.flv'%port,'media':'flv','codec':'h264','label':'hd 720p H264','brief':'hd 720p H264'} logs=[] sender=Sender(FF, lambda u,m: logs.append(m)) stop=threading.Event() # TEST 3: continuous data → relay stays alive a few seconds t0=time.monotonic(); err=None def run(): global err try: sender.send('u',cand,dest,stop) except Exception as e: err=e th=threading.Thread(target=run,daemon=True); th.start() time.sleep(4) alive=sender.alive() check('TEST 3 continuous data keeps FFmpeg alive', alive, 'alive=%s err=%s'%(alive,err)) snap=sender.snapshot() check('TEST 3 bytes received', (snap.get('source_bytes') or 0)>0, str(snap.get('source_bytes'))) # TEST 5: stall → watchdog state['mode']='stall' stalled=False t_wait=time.monotonic() while time.monotonic()-t_wait<12: if any('WATCHDOG' in m or 'stall' in m.lower() or 'صمت' in m for m in logs) or isinstance(err,SourceStalled) or not sender.alive(): stalled=True; break time.sleep(0.3) check('TEST 5 stall watchdog fires', stalled or not sender.alive(), 'err=%s logs=%s'%(err, logs[-6:])) stop.set(); sender.terminate(); th.join(timeout=8) check('TEST 4/6 cleanup leaves no process', sender.process is None or sender.process.poll() is not None) # TEST 8-ish: stop after live check('TEST 8 session ends, sender not alive', not sender.alive()) srv.shutdown() # ---------- backoff ---------- from youtube_manager import ManagedYouTube logs=[] yt=ManagedYouTube('ffmpeg', lambda u,force=False: {'status':'offline','room_id':'','sources':[]}, lambda u,m: logs.append((u,m)), lambda: ['a'], TMP/'yt.json') yt.retry['a']=1 yt.retry['a']=yt._backoff('a')*2 check('TEST 11 backoff increases', yt._backoff('a')>=2, str(yt._backoff('a'))) yt.retry['a']=9999 check('TEST 11 backoff capped', yt._backoff('a')<=yt.retry_max) check('reliability selection keeps lower H264 after preferred 720-class source', any(c.get('quality_name')=='sd' for c in yt.video_sources(ranked)), str([c.get('quality_name') for c in yt.video_sources(ranked)])) # Same-codec lower rendition is spliced without reopening RTMP; quality is restored only on a # later natural refresh after the stable hold interval. high={'url':'https://x/high.flv','media':'flv','codec':'h264','quality_name':'hd', 'resolution':'720x1280','width':720,'height':1280,'label':'hd','brief':'hd 720p H264'} low={'url':'https://x/low.flv','media':'flv','codec':'h264','quality_name':'sd', 'resolution':'480x854','width':480,'height':854,'label':'sd','brief':'sd 480p H264'} _saved_observe,_saved_select=yt._observe,yt._select_source yt._observe=lambda *a,**k: {'status':'live','room_id':'1','sources':[high,low]} yt._select_source=lambda user,obs: [dict(high,user=user),dict(low,user=user)] yt._rendition_pin['adaptive']=yt._source_pin(high) chosen=yt._refresh_flv('adaptive',high) check('stalled high FLV falls to lower same-codec source in the same publisher', chosen and chosen.get('quality_name')=='sd' and yt._quality_fallback.get('adaptive'),str(chosen)) os.environ['YOUTUBE_SOURCE_UPGRADE_SECONDS']='60' yt._quality_fallback['adaptive']['since']=time.monotonic()-61 yt._rendition_pin['adaptive']=yt._source_pin(low) restored=yt._refresh_flv('adaptive',low) check('stable lower source restores preferred quality on a later natural refresh', restored and restored.get('quality_name')=='hd' and 'adaptive' not in yt._quality_fallback,str(restored)) yt._observe,yt._select_source=_saved_observe,_saved_select os.environ.pop('YOUTUBE_SOURCE_UPGRADE_SECONDS',None) # ---------- unstable source fallback bookkeeping ---------- yt.mark_unstable('bob', {'quality_name':'origin','media':'flv','codec':'h265','resolution':'1080x1920','brief':'origin 1080p HEVC'}, 'stalled') check('TEST 7 unstable source recorded', 'origin|flv|h265|1080x1920' in yt._unstable.get('bob',set())) obs={'status':'live','room_id':'1','sources':[ {'url':'https://x/a.flv','media':'flv','codec':'h265','quality_name':'origin','resolution':'1080x1920','label':'origin','brief':'origin 1080p HEVC'}, {'url':'https://x/b.flv','media':'flv','codec':'h264','quality_name':'hd','resolution':'720x1280','label':'hd','brief':'hd 720p H264'}, ]} # video_sources may filter; monkeypatch probe to skip network yt.probe=lambda user,c: True yt.video_sources=staticmethod(lambda sources: sources) sel=yt._select_source('bob', obs) check('TEST 7 fallback prefers non-unstable', sel and 'h264' in (sel[0].get('codec') or '').lower(), str([(c.get('quality_name'),c.get('codec')) for c in sel])) hls_obs={'status':'live','room_id':'1','sources':[ {'url':'https://x/a.flv','media':'flv','codec':'h264','quality_name':'hd','resolution':'720x1280','label':'hd flv','brief':'hd 720p H264'}, {'url':'https://x/a.m3u8','media':'hls','codec':'h264','quality_name':'hd','resolution':'720x1280','label':'hd hls','brief':'hd 720p H264 HLS'}, ]} yt.starve['bob']=1 yt._unstable.pop('bob', None) sel=yt._select_source('bob', hls_obs) check('first FLV stall prefers HLS', sel and sel[0].get('media')=='hls', str([(c.get('media'),c.get('quality_name')) for c in (sel or [])])) yt.starve.pop('bob', None) # ---------- hub / env RTMPS without OAuth ---------- from ingest import hub, env_rtmp_destination, format_session_title, rtmp_configured os.environ['YOUTUBE_RTMP_URL']='rtmps://a.rtmps.youtube.com:443/live2' os.environ['YOUTUBE_STREAM_KEY']='unit-test-key' check('env RTMPS dest built without OAuth', env_rtmp_destination().endswith('/unit-test-key')) check('rtmp_configured true', rtmp_configured()) title,_,_=format_session_title('StrongArmor', datetime(2026,9,23,14,35,tzinfo=ZoneInfo('Asia/Baghdad'))) check('format_session_title Baghdad', title=='StrongArmor | 2026-09-23 14:35', title) class _Alive: def is_alive(self):return True check('hub occupy first session', hub.occupy('alice','direct', thread=_Alive())) check('TEST 14 duplicate relay blocked', not hub.occupy('alice','oauth', thread=_Alive())) check('other broadcaster independent', hub.occupy('bob','oauth', thread=_Alive())) check('single env key busy', hub.env_key_busy()) hub.release('alice'); hub.release('bob') check('hub released', not hub.busy('alice') and not hub.busy('bob')) class _PushSnd: def alive(self):return True class _IdleSnd: def alive(self):return False class _PushTh: def is_alive(self):return True check('env occupy first', hub.occupy('e1','direct', sender=_PushSnd(), thread=_PushTh(), dest='env')) check('second env publisher blocked', hub.env_key_busy()) check('oauth dest independent of env', hub.occupy('e2','oauth', sender=_PushSnd(), thread=_PushTh(), dest='oauth')) check('active_count is ffmpeg not idle threads', hub.active_count()==2) hub.release('e1'); hub.release('e2') check('env free after release', not hub.env_key_busy()) h=app.health_public() check('public health hides last_error', 'last_error' not in h, str(h.keys())) check('public health hides last_error_user', 'last_error_user' not in h, str(h.keys())) check('health has version', bool(h.get('version'))) # Null TikTok payloads must be Offline, not AttributeError → sticky last_error orig_gj = app.get_json app.get_json = lambda url, params, user=None: {'data': {'liveRoom': None, 'user': None}} try: obs = app._observe_session('nullroom') check('null liveRoom/user → offline not crash', obs.get('status')=='offline', str(obs)) finally: app.get_json = orig_gj app.get_json = lambda url, params, user=None: {'data': None} try: obs = app._observe_session('nulldata') check('null data → offline not crash', obs.get('status')=='offline', str(obs)) finally: app.get_json = orig_gj app.tasks['staleerr'] = app._blank_task() app.tasks['staleerr']['enabled'] = True app.tasks['staleerr']['last_error'] = 'فشل طلب بيانات البث' app.tasks['staleerr']['state'] = 'BACKOFF' orig = app.observe_session app.observe_session = lambda user, force=False: {'status':'offline','room_id':'','sources':[],'nickname':'','live_title':''} try: app._check_account('staleerr') check('offline clears last_error', app.tasks['staleerr'].get('last_error')=='', str(app.tasks['staleerr'])) check('offline after error is OFFLINE', app.tasks['staleerr']['state']=='OFFLINE') h=app.health_detail() check('authenticated detail last_error empty after offline', not h.get('last_error'), str(h)) h=app.health_public() check('public health does not list broadcasters', 'broadcasters' not in h) check('public health hides metadata automation', 'metadata_automation' not in h) finally: app.observe_session = orig app.tasks.pop('staleerr', None) # OAuth off + env dest → DirectRelay occupies (video path does not need OAuth) app.youtube.journal.change(lambda d: d.update(enabled=False)) app.tasks['diruser']=app._blank_task(); app.tasks['diruser']['enabled']=True app.observe_session = lambda user, force=False: {'status':'live','room_id':'99','sources':[flv],'nickname':'N','live_title':'x'} try: app._maybe_start_direct('diruser', {'status':'live','room_id':'99','sources':[flv],'nickname':'N','live_title':'x'}) check('DirectRelay starts without OAuth', 'diruser' in app._direct_handles or hub.busy('diruser')) finally: app._stop_direct('diruser','test') app.observe_session = orig app.tasks.pop('diruser', None) check('DirectRelay cleaned up', not hub.busy('diruser') and 'diruser' not in app._direct_handles) # dead sender restart without waiting a full scan class _DeadTh: def is_alive(self):return False class _LiveTh: def is_alive(self):return True class _DeadSnd: def alive(self):return False def terminate(self):pass called=[] yt.journal.change(lambda d: d['events'].update({'bob:1':{ 'username':'bob','room_id':'1','closing':False,'offline_since':None, 'bound':True,'broadcast_id':'B','youtube_status':'live','title':'t'}})) yt.senders['bob:1']={'thread':_DeadTh(),'stop':threading.Event(),'sender':_DeadSnd()} yt.start_sender=lambda key: called.append(key) n=yt.restart_dead_senders() check('dead sender restart without full scan', n==1 and called==['bob:1'], 'n=%s called=%s'%(n,called)) yt.senders['bob:1']={'thread':_LiveTh(),'stop':threading.Event(),'sender':_DeadSnd()} called.clear() check('live sender not restarted', yt.restart_dead_senders()==0 and not called) yt.journal.change(lambda d: d['events'].update({'bob:1': dict(d['events']['bob:1'], offline_since=time.time(), offline_count=1, youtube_status='live')})) yt.senders['bob:1']={'thread':_DeadTh(),'stop':threading.Event(),'sender':_DeadSnd()} called.clear() check('first offline blip still restarts dead sender', yt.restart_dead_senders()==1 and called==['bob:1'], 'n=%s called=%s'%(yt.restart_dead_senders() if False else 1, called)) yt.journal.change(lambda d: d['events'].update({'bob:1': dict(d['events']['bob:1'], offline_since=time.time(), offline_count=2, youtube_status='live')})) yt.senders['bob:1']={'thread':_DeadTh(),'stop':threading.Event(),'sender':_DeadSnd()} called.clear() check('confirmed offline skips restart', yt.restart_dead_senders()==0 and not called) stall_obs={'status':'live','room_id':'1','sources':[ {'url':'https://x/a.flv','media':'flv','codec':'h264','quality_name':'hd','resolution':'720x1280','label':'hd','brief':'hd 720p H264'}, ]} yt.video_sources=staticmethod(lambda sources: sources) yt._skip_probe_once['bob']=True probed=[] yt.probe=lambda user,c: probed.append(c) or True sel=yt._select_source('bob', stall_obs) check('stall skip probe uses ranked sources', bool(sel) and not probed, 'sel=%s probed=%s'%(bool(sel), len(probed))) yt._skip_hold.add('bob') yt._held['bob']=(time.monotonic(), {'status':'live','room_id':'9','sources':[{'url':'https://x/old.flv'}]}) yt.observe=lambda user,force=False: {'status':'offline','room_id':'','sources':[]} fresh=yt._observe('bob', allow_hold=True) check('skip hold after stall is honest offline', fresh.get('status')=='offline', str(fresh)) yt.senders.pop('bob:1', None) yt.journal.change(lambda d: d['events'].pop('bob:1', None)) df=(ROOT/'Dockerfile').read_text() check('Dockerfile stall default 12', 'SOURCE_STALL_TIMEOUT=12' in df and 'YOUTUBE_SOURCE_STALL_SECONDS=12' in df) check('Dockerfile concurrent cap 4', 'MAX_CONCURRENT_RELAYS=4' in df and 'YOUTUBE_MAX_EVENTS=4' in df) check('Dockerfile defaults to byte-copy AVC/Enhanced-HEVC without forced x264', 'YOUTUBE_LIGHT_MAX=0' in df and 'YOUTUBE_AUTO_TRANSCODE=0' in df and 'YOUTUBE_FORCE_TRANSCODE_USERS=' not in df and 'YOUTUBE_ENHANCED_RTMP=auto' in df and 'stream_monitor.py' in df and 'YOUTUBE_HEALTH_INTERVAL=20' in df and 'STREAM_EVENT_MAX=10000' in df) mgr_src=(ROOT/'youtube_manager.py').read_text() check('run_manager calls restart_dead_senders', 'restart_dead_senders' in mgr_src.split('def run_manager')[1]) pump=(ROOT/'relay.py').read_text() empty_i=pump.find("if not chunk:") last_i=pump.find("sess['last'] = time.monotonic()", pump.find('def _pump_source')) check('empty FLV chunks do not refresh last', empty_i!=-1 and last_i!=-1 and empty_i>16)&255,(ts>>8)&255,ts&255,(ts>>24)&255,0,0,0]) return hdr+body+(11+size).to_bytes(4,'big') def _file(tags): return b'FLV\x01\x05'+(9).to_bytes(4,'big')+b'\x00\x00\x00\x00'+tags br=FLVBridge(detect_format_change=True) out1=br.feed(_file(_flv_avc(0, seq=True)+_flv_avc(40, seq=False))) check('first FLV emits file header', out1.startswith(b'FLV')) last_before=br.last_out_ts br.reconnect() out2=br.feed(_file(_flv_avc(0, seq=True)+_flv_avc(40, seq=False, payload=b'\x01'))) check('reconnect drops second FLV header', bool(out2) and not out2.startswith(b'FLV'), repr(out2[:8])) check('reconnect DTS stays monotonic', br.last_out_ts is not None and last_before is not None and br.last_out_ts>last_before, 'before=%s after=%s'%(last_before, br.last_out_ts)) check('health version is stall12', any(x in str(h.get('version') or '') for x in ('multi','guard','v2.')), str(h.get('version'))) # first TikTok offline blip must not kill FFmpeg (YouTube spinner root cause) class _KeepTh: def is_alive(self):return True def join(self,timeout=None):pass class _KeepSnd: def alive(self):return True def terminate(self):self.killed=True def snapshot(self):return {'source_age':1} killed=False yt.journal.change(lambda d: d['events'].update({'keep:1':{ 'username':'keep','room_id':'1','closing':False,'offline_since':None,'offline_count':0, 'bound':True,'broadcast_id':'B','youtube_status':'live','title':'t','was_live':True, 'marker':'m','created_at':time.time(),'note':''}})) yt.senders['keep:1']={'thread':_KeepTh(),'stop':threading.Event(),'sender':_KeepSnd()} yt.observe=lambda user,force=False: {'status':'offline','room_id':'1','sources':[]} yt.drive_event('keep:1') check('first offline blip keeps sender', 'keep:1' in yt.senders and yt.senders['keep:1']['thread'].is_alive()) yt.senders.pop('keep:1', None) yt.journal.change(lambda d: d['events'].pop('keep:1', None)) # disk: DATA_DIR should not contain video files vids=[p for p in (TMP/'data').rglob('*') if p.suffix.lower() in ('.mp4','.mkv','.flv','.ts') and p.is_file()] check('TEST 12 no video files in DATA_DIR', not vids, str(vids[:5])) print('\n%d passed, %d failed'%(passed, failed)) sys.exit(1 if failed else 0)