Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
247
infra/peertube-workers/win/tests/test_win_worker.py
Normal file
247
infra/peertube-workers/win/tests/test_win_worker.py
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
"""Tests for the Windows worker port (win/). Runs on Linux; no GPU, no Windows host.
|
||||
|
||||
Covers the three pieces the wave doc says must hold before first hardware contact:
|
||||
1. ffmpeg-qsv.py argument rewriting matches the validated Linux adapter's
|
||||
contract and fails closed on unreviewed commands (exit 78).
|
||||
2. the capability-query passthrough list matches what the shipped runner
|
||||
actually generates (verified in the 0.2.0 bundle).
|
||||
3. pt-worker-win.py Meter/Admission semantics are byte-identical to the
|
||||
Linux controller's for every decision input (states, hysteresis, floors).
|
||||
"""
|
||||
import importlib.util
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
WIN = Path(__file__).resolve().parents[2] / 'win'
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def load(name, path):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
qsv = load('qsv_win', WIN / 'ffmpeg-qsv.py')
|
||||
win = load('worker_win', WIN / 'pt-worker-win.py')
|
||||
linux = load('worker_linux', ROOT / 'pt-worker.py')
|
||||
|
||||
PROBE = {'streams': [{'codec_type': 'video', 'codec_name': 'h264', 'pix_fmt': 'yuv420p',
|
||||
'width': 1920, 'height': 1080}],
|
||||
'format': {'duration': '10.0'}}
|
||||
|
||||
|
||||
class RewriteTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.old_state, self.old_probe = qsv.STATE, qsv.probe
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
qsv.STATE = Path(self.tmp.name)
|
||||
def stub_probe(path): # no real ffprobe.exe on this Linux host
|
||||
name = str(path).lower()
|
||||
if name.endswith(('.png', '.mjpeg', '.webp')):
|
||||
return {'streams': [{**PROBE['streams'][0], 'codec_name': 'png',
|
||||
'pix_fmt': 'rgba'}], 'format': {'duration': '0'}}
|
||||
return PROBE
|
||||
qsv.probe = stub_probe
|
||||
|
||||
def tearDown(self):
|
||||
qsv.STATE, qsv.probe = self.old_state, self.old_probe
|
||||
self.tmp.cleanup()
|
||||
|
||||
def rewritten(self, args, data=PROBE):
|
||||
out, _ = qsv.rewrite(args, input_probe=data)
|
||||
return out
|
||||
|
||||
def test_h264_web_video_becomes_qsv_pipeline(self):
|
||||
out = self.rewritten(['-i', 'in.mp4', '-vf', 'scale=w=-2:h=720', '-c:v', 'libx264',
|
||||
'-preset', 'veryfast', '-crf', '18', '-maxrate', '1500K',
|
||||
'-bufsize', '3000K', '-g', '96', '-x264-params', 'nofast-pskip=1',
|
||||
'-movflags', '+faststart', 'out.mp4'])
|
||||
text = ' '.join(out)
|
||||
self.assertIn('-hwaccel qsv', text)
|
||||
self.assertIn('-hwaccel_output_format qsv', text)
|
||||
self.assertIn('-c:v h264_qsv', text)
|
||||
self.assertIn('scale_qsv=w=1280:h=720', text)
|
||||
self.assertIn('-preset medium', text)
|
||||
self.assertIn('-b:v 1500K', text) # runner's rate cap kept, CRF dropped
|
||||
self.assertIn('-g 96', text) # runner's GOP kept
|
||||
self.assertNotIn('libx264', text)
|
||||
self.assertNotIn('-crf', text)
|
||||
|
||||
def test_hevc_source_uses_hevc_qsv_decode(self):
|
||||
data = {**PROBE, 'streams': [{**PROBE['streams'][0], 'codec_name': 'hevc', 'pix_fmt': 'yuv420p10le'}]}
|
||||
out = self.rewritten(['-i', 'in.mkv', '-c:v', 'libx264', 'out.mp4'], data)
|
||||
self.assertIn('-c:v hevc_qsv', ' '.join(out))
|
||||
|
||||
def test_no_vf_gets_default_scale_qsv(self):
|
||||
out = self.rewritten(['-i', 'in.mp4', '-c:v', 'libx264', 'out.mp4'])
|
||||
self.assertIn('scale_qsv=w=1920:h=1080', ' '.join(out))
|
||||
|
||||
def test_audio_only_passthrough(self):
|
||||
out, info = qsv.rewrite(['-i', 'in.mp4', '-vn', '-c:a', 'aac', 'out.m4a'])
|
||||
self.assertEqual(info, None)
|
||||
|
||||
def test_capability_queries_passthrough(self):
|
||||
for args in (['-version'], ['-encoders'], ['-hwaccels']):
|
||||
out, info = qsv.rewrite(args)
|
||||
self.assertEqual((out, info), (args, None))
|
||||
|
||||
def test_fail_closed_cpu_or_unknown(self):
|
||||
# Note: -vf contents are validated after the (stubbed) probe, matching
|
||||
# the Linux adapter, which needs the probe to resolve -2 dimensions.
|
||||
for args in (['-i', 'in.mp4', '-c:v', 'mpeg4', 'out.mp4'],
|
||||
['-i', 'in.mp4', '-c:v', 'libx264', '-filter_complex', 'x', 'out.mp4'],
|
||||
['-i', 'in.mp4', '-c:v', 'libx264', '-vf', 'scale=w=-2:h=720,hue=s=0', 'out.mp4'],
|
||||
['-i', 'in.png', '-c:v', 'libx264', '-vf', 'scale=w=-2:h=720', 'out.mp4'],
|
||||
['-i', 'in.mp4', '-c:v', 'libx264', '-vf', 'scale=w=0:h=720', 'out.mp4']):
|
||||
with self.assertRaises(ValueError):
|
||||
qsv.rewrite(args)
|
||||
|
||||
def test_refusal_process_exit_78(self):
|
||||
import os
|
||||
env = {**os.environ, 'PT_STATE_DIR': str(self.tmp.name), 'PT_FFPROBE_EXE': 'ffprobe.exe'}
|
||||
p = subprocess.run([sys.executable, str(WIN / 'ffmpeg-qsv.py'),
|
||||
'-i', 'x.mp4', '-c:v', 'libx264', 'o.mp4'],
|
||||
capture_output=True, text=True, env=env)
|
||||
self.assertEqual(p.returncode, 78)
|
||||
|
||||
def test_events_have_fixed_reasons_only(self):
|
||||
import os
|
||||
env = {**os.environ, 'PT_STATE_DIR': str(self.tmp.name), 'PT_FFPROBE_EXE': 'ffprobe.exe'}
|
||||
p = subprocess.run([sys.executable, str(WIN / 'ffmpeg-qsv.py'),
|
||||
'-i', 'x.mp4', '-c:v', 'libx264', 'o.mp4'],
|
||||
capture_output=True, text=True, env=env)
|
||||
events = [json.loads(line) for line in
|
||||
(Path(self.tmp.name) / 'events.jsonl').read_text().splitlines()]
|
||||
self.assertEqual(events[0]['kind'], 'gpu_failure')
|
||||
self.assertEqual(events[0]['reason'], 'ValueError')
|
||||
self.assertNotIn('x.mp4', p.stderr)
|
||||
|
||||
|
||||
class RunnerContractTests(unittest.TestCase):
|
||||
"""Verified directly in the shipped runner 0.2.0 bundle this session."""
|
||||
|
||||
def test_queries_actually_issued_by_runner(self):
|
||||
bundle = Path('/tmp/peertube-worker-inspect/runtime/node_modules/@peertube/peertube-runner'
|
||||
'/dist/peertube-runner.js')
|
||||
if not bundle.exists():
|
||||
self.skipTest('pinned runner bundle not present')
|
||||
text = bundle.read_text(errors='replace')
|
||||
# Exactly the queries fluent-ffmpeg's capability probe issues in 0.2.0
|
||||
# (-hwaccels is NOT issued; the shim passes it through anyway, harmlessly).
|
||||
for query in ('-version', '-formats', '-encoders', '-filters', '-codecs'):
|
||||
self.assertIn(query, text)
|
||||
self.assertNotIn('-hwaccels', text)
|
||||
self.assertIn('encodersToTry = this.availableEncoders.encodersToTry', text)
|
||||
self.assertIn('"libx264"', text)
|
||||
|
||||
def test_spawn_without_shell_and_ffprobe_sibling(self):
|
||||
bundle = Path('/tmp/peertube-worker-inspect/runtime/node_modules/@peertube/peertube-runner'
|
||||
'/dist/peertube-runner.js')
|
||||
if not bundle.exists():
|
||||
self.skipTest('pinned runner bundle not present')
|
||||
text = bundle.read_text(errors='replace')
|
||||
self.assertNotIn('shell: true', text) # EINVAL on .cmd/.bat shims
|
||||
self.assertIn('utils.isWindows ? "ffprobe.exe" : "ffprobe"', text)
|
||||
self.assertIn('!utils.isWindows', text) # `nice` skipped on win32
|
||||
|
||||
|
||||
class AdmissionParityTests(unittest.TestCase):
|
||||
"""The Windows controller must decide identically to the Linux controller."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.old_win, self.old_lin = win.STATE, linux.STATE
|
||||
win.STATE = linux.STATE = Path(self.tmp.name)
|
||||
self.now = time.time()
|
||||
|
||||
def tearDown(self):
|
||||
win.STATE, linux.STATE = self.old_win, self.old_lin
|
||||
self.tmp.cleanup()
|
||||
|
||||
def meter_with_transfers(self, module, mbps):
|
||||
meter = module.Meter()
|
||||
for direction in ('download', 'upload'):
|
||||
meter.consume({'kind': 'transfer', 'time': self.now, 'ok': True, 'direction': direction,
|
||||
'bytes': int(mbps * 1e6 / 8), 'seconds': 1})
|
||||
return meter
|
||||
|
||||
def sample(self, **overrides):
|
||||
base = {'tailscale_path': 'direct', 'latency_ms': 5, 'network_ok': True,
|
||||
'registered': True, 'gpu_ready': True, 'gpu_tested_at': self.now,
|
||||
'gpu_accessible': True, 'on_ac': True, 'disk_free_bytes': 100 * 1024 ** 3,
|
||||
'temperature_c': 50, 'throttle_counts': {}, 'active_jobs': 0}
|
||||
base.update(overrides)
|
||||
return base
|
||||
|
||||
def parity(self, overrides, control=None, decisions=1):
|
||||
control = control or {'paused': False}
|
||||
outputs = []
|
||||
for module in (linux, win):
|
||||
policy = module.Admission({})
|
||||
meter = self.meter_with_transfers(module, 50)
|
||||
for _ in range(decisions):
|
||||
outputs.append(policy.decide(self.sample(**overrides), meter, control, self.now))
|
||||
self.assertEqual(outputs[0], outputs[1],
|
||||
f'Linux and Windows controllers diverge on {overrides}: {outputs}')
|
||||
return outputs[0]
|
||||
|
||||
def test_happy_path(self):
|
||||
self.parity({}, decisions=4)
|
||||
|
||||
def test_power_loss_drains_identically(self):
|
||||
self.assertEqual(self.parity({'on_ac': False}), 'DRAINING')
|
||||
|
||||
def test_unknown_power_drains_identically(self):
|
||||
self.assertEqual(self.parity({'on_ac': None}), 'DRAINING')
|
||||
|
||||
def test_missing_gpu_proof_blocks_identically(self):
|
||||
self.assertEqual(self.parity({'gpu_ready': False, 'gpu_accessible': False}), 'GPU_UNAVAILABLE')
|
||||
|
||||
def test_gpu_failure_event_after_proof_blocks_identically(self):
|
||||
outputs = []
|
||||
for module in (linux, win):
|
||||
policy = module.Admission({})
|
||||
meter = self.meter_with_transfers(module, 50)
|
||||
meter.consume({'kind': 'gpu_failure', 'time': self.now + 1})
|
||||
outputs.append(policy.decide(self.sample(gpu_tested_at=self.now), meter,
|
||||
{'paused': False}, self.now + 2))
|
||||
self.assertEqual(outputs[0], 'GPU_UNAVAILABLE')
|
||||
self.assertEqual(outputs[0], outputs[1])
|
||||
|
||||
def test_low_disk_and_thermal(self):
|
||||
self.assertEqual(self.parity({'disk_free_bytes': 10 * 1024 ** 3}), 'LOW_DISK')
|
||||
self.assertEqual(self.parity({'temperature_c': 90}), 'THERMAL_THROTTLED')
|
||||
|
||||
def test_derp_disallowed_and_latency_ceiling(self):
|
||||
self.assertEqual(self.parity({'tailscale_path': 'DERP'}), 'NETWORK_DEGRADED')
|
||||
self.assertEqual(self.parity({'latency_ms': 500}), 'NETWORK_DEGRADED')
|
||||
|
||||
def test_bandwidth_floor_hysteresis(self):
|
||||
# 5 Mbps < 10 Mbps floor: every sample is bad, both controllers degrade.
|
||||
for module in (linux, win):
|
||||
policy = module.Admission({})
|
||||
meter = self.meter_with_transfers(module, 5)
|
||||
outputs = [policy.decide(self.sample(), meter, {'paused': False}, self.now)
|
||||
for _ in range(3)]
|
||||
self.assertEqual(outputs, ['NETWORK_DEGRADED'] * 3, module.__name__)
|
||||
|
||||
def test_recovery_factor(self):
|
||||
outputs = []
|
||||
for module in (linux, win):
|
||||
policy = module.Admission({})
|
||||
meter = self.meter_with_transfers(module, 11)
|
||||
outputs.append(policy.decide(self.sample(), meter, {'paused': False}, self.now))
|
||||
outputs.append(policy.decide(self.sample(), meter, {'paused': False}, self.now))
|
||||
self.assertEqual(outputs[0], 'NETWORK_DEGRADED')
|
||||
self.assertEqual(outputs[0], outputs[1])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=1)
|
||||
Loading…
Add table
Add a link
Reference in a new issue