style: auto-format with black + isort + ruff + prettier [skip ci-format-check]
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m26s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 39s
AI Code Review / AI Code Review (pull_request) Successful in 6m43s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 10s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 28s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m21s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m26s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m5s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 39s
AI Code Review / AI Code Review (pull_request) Successful in 6m43s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 10s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 28s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m21s
This commit is contained in:
+175
-88
@@ -1,4 +1,5 @@
|
||||
"""GpuEncoderClient 单元测试:mock HTTP,覆盖 health/sync/fallback/singleton 等完整路径。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
@@ -62,8 +63,12 @@ def _fake_response(status: int = 200, body: dict | bytes | None = None, headers=
|
||||
# ── Health check ────────────────────────────────────────────────────
|
||||
class TestHealthCheck:
|
||||
def test_healthy_nvenc_available(self, client):
|
||||
body = {"status": "healthy", "worker": "w1",
|
||||
"gpu": {"name": "Quadro P4000"}, "nvenc": {"h264_nvenc": True, "hevc_nvenc": True}}
|
||||
body = {
|
||||
"status": "healthy",
|
||||
"worker": "w1",
|
||||
"gpu": {"name": "Quadro P4000"},
|
||||
"nvenc": {"h264_nvenc": True, "hevc_nvenc": True},
|
||||
}
|
||||
with mock.patch("urllib.request.urlopen", return_value=_fake_response(body=body)):
|
||||
h = client.check_health()
|
||||
assert h.healthy and h.nvenc_h264 and h.ready
|
||||
@@ -98,55 +103,73 @@ class TestHealthCheck:
|
||||
# ── _post_sync ──────────────────────────────────────────────────────
|
||||
class TestPostSync:
|
||||
def test_completed_job_returns_dict(self, client):
|
||||
result_body = {"job_id":"j1","status":"completed","ffmpeg_rc":0,"uploaded":True,"duration":5.1,"size":123456}
|
||||
result_body = {
|
||||
"job_id": "j1",
|
||||
"status": "completed",
|
||||
"ffmpeg_rc": 0,
|
||||
"uploaded": True,
|
||||
"duration": 5.1,
|
||||
"size": 123456,
|
||||
}
|
||||
with mock.patch("urllib.request.urlopen", return_value=_fake_response(body=result_body)) as m:
|
||||
res = client._post_sync(
|
||||
{"inputs":{"in.mp4":"http://x"},"ffmpeg_args":["-i","in.mp4"],
|
||||
"output_url":"http://relay/k?token=s","timeout":30},
|
||||
mezzanine_path=Path("/tmp/fake.mp4"))
|
||||
{
|
||||
"inputs": {"in.mp4": "http://x"},
|
||||
"ffmpeg_args": ["-i", "in.mp4"],
|
||||
"output_url": "http://relay/k?token=s",
|
||||
"timeout": 30,
|
||||
},
|
||||
mezzanine_path=Path("/tmp/fake.mp4"),
|
||||
)
|
||||
assert res["status"] == "completed" and res["ffmpeg_rc"] == 0
|
||||
req = m.call_args[0][0]
|
||||
assert req.full_url == "http://gpu.example.com:8900/api/render/sync"
|
||||
|
||||
def test_ffmpeg_failure_raises(self, client):
|
||||
body = {"status":"failed","ffmpeg_rc":1,"message":"Invalid data"}
|
||||
body = {"status": "failed", "ffmpeg_rc": 1, "message": "Invalid data"}
|
||||
with mock.patch("urllib.request.urlopen", return_value=_fake_response(body=body)):
|
||||
with pytest.raises(GpuEncodeError, match="rc=1"):
|
||||
client._post_sync({"inputs":{},"ffmpeg_args":[],"output_url":"","timeout":10},
|
||||
mezzanine_path=Path("/tmp/x"))
|
||||
client._post_sync(
|
||||
{"inputs": {}, "ffmpeg_args": [], "output_url": "", "timeout": 10}, mezzanine_path=Path("/tmp/x")
|
||||
)
|
||||
|
||||
def test_http_4xx_raises(self, client):
|
||||
err = urllib.error.HTTPError(url="http://gpu/render/sync", code=422, msg="Unprocessable",
|
||||
hdrs={}, fp=BytesIO(b"bad request"))
|
||||
err = urllib.error.HTTPError(
|
||||
url="http://gpu/render/sync", code=422, msg="Unprocessable", hdrs={}, fp=BytesIO(b"bad request")
|
||||
)
|
||||
with mock.patch("urllib.request.urlopen", side_effect=err):
|
||||
with pytest.raises(GpuEncodeError, match="HTTP 422"):
|
||||
client._post_sync({"inputs":{},"ffmpeg_args":[],"output_url":"","timeout":10},
|
||||
mezzanine_path=Path("/tmp/x"))
|
||||
client._post_sync(
|
||||
{"inputs": {}, "ffmpeg_args": [], "output_url": "", "timeout": 10}, mezzanine_path=Path("/tmp/x")
|
||||
)
|
||||
|
||||
def test_connection_error_raises(self, client):
|
||||
with mock.patch("urllib.request.urlopen", side_effect=urllib.error.URLError("conn refused")):
|
||||
with pytest.raises(GpuEncodeError, match="connection error"):
|
||||
client._post_sync({"inputs":{},"ffmpeg_args":[],"output_url":"","timeout":10},
|
||||
mezzanine_path=Path("/tmp/x"))
|
||||
client._post_sync(
|
||||
{"inputs": {}, "ffmpeg_args": [], "output_url": "", "timeout": 10}, mezzanine_path=Path("/tmp/x")
|
||||
)
|
||||
|
||||
def test_timeout_error_raises(self, client):
|
||||
with mock.patch("urllib.request.urlopen", side_effect=socket.timeout("timed out")):
|
||||
with pytest.raises(GpuEncodeError, match="connection error"):
|
||||
client._post_sync({"inputs":{},"ffmpeg_args":[],"output_url":"","timeout":10},
|
||||
mezzanine_path=Path("/tmp/x"))
|
||||
client._post_sync(
|
||||
{"inputs": {}, "ffmpeg_args": [], "output_url": "", "timeout": 10}, mezzanine_path=Path("/tmp/x")
|
||||
)
|
||||
|
||||
def test_bad_json_raises(self, client):
|
||||
with mock.patch("urllib.request.urlopen", return_value=_fake_response(body=b"not-json")):
|
||||
with pytest.raises(GpuEncodeError, match="bad JSON"):
|
||||
client._post_sync({"inputs":{},"ffmpeg_args":[],"output_url":"","timeout":10},
|
||||
mezzanine_path=Path("/tmp/x"))
|
||||
client._post_sync(
|
||||
{"inputs": {}, "ffmpeg_args": [], "output_url": "", "timeout": 10}, mezzanine_path=Path("/tmp/x")
|
||||
)
|
||||
|
||||
def test_uploaded_false_logs_warning_but_succeeds(self, client, caplog):
|
||||
body = {"status":"completed","ffmpeg_rc":0,"uploaded":False,"job_id":"j"}
|
||||
with mock.patch("urllib.request.urlopen", return_value=_fake_response(body=body)), \
|
||||
caplog.at_level("WARNING"):
|
||||
res = client._post_sync({"inputs":{},"ffmpeg_args":[],"output_url":"","timeout":10},
|
||||
mezzanine_path=Path("/tmp/x"))
|
||||
body = {"status": "completed", "ffmpeg_rc": 0, "uploaded": False, "job_id": "j"}
|
||||
with mock.patch("urllib.request.urlopen", return_value=_fake_response(body=body)), caplog.at_level("WARNING"):
|
||||
res = client._post_sync(
|
||||
{"inputs": {}, "ffmpeg_args": [], "output_url": "", "timeout": 10}, mezzanine_path=Path("/tmp/x")
|
||||
)
|
||||
assert res["status"] == "completed"
|
||||
assert "uploaded=false" in caplog.text
|
||||
|
||||
@@ -239,18 +262,28 @@ class TestDownloadToFile:
|
||||
# ── encode_mezzanine_to_output ─────────────────────────────────────
|
||||
class TestEncodeMezzanine:
|
||||
def test_happy_path_with_audio(self, client, tmp_path):
|
||||
mezz = tmp_path / "mezz.mp4"; mezz.write_bytes(b"M" * 100)
|
||||
mezz = tmp_path / "mezz.mp4"
|
||||
mezz.write_bytes(b"M" * 100)
|
||||
out = tmp_path / "out" / "final.mp4"
|
||||
with (
|
||||
mock.patch.object(client, "_upload_mezzanine", return_value=("http://oss/signed", "osskey1")),
|
||||
mock.patch.object(client, "_post_sync", return_value={
|
||||
"job_id":"j1","status":"completed","ffmpeg_rc":0,"uploaded":True,"size":5000,"duration":1.2,
|
||||
}) as m_post,
|
||||
mock.patch.object(
|
||||
client,
|
||||
"_post_sync",
|
||||
return_value={
|
||||
"job_id": "j1",
|
||||
"status": "completed",
|
||||
"ffmpeg_rc": 0,
|
||||
"uploaded": True,
|
||||
"size": 5000,
|
||||
"duration": 1.2,
|
||||
},
|
||||
) as m_post,
|
||||
mock.patch.object(client, "_download_to_file", return_value=5000) as m_dl,
|
||||
mock.patch.object(client, "_relay_delete") as m_del,
|
||||
mock.patch.object(client, "_delete_oss") as m_ossdel,
|
||||
):
|
||||
result = client.encode_mezzanine_to_output(mezz, out, audio_args=["-c:a","aac"])
|
||||
result = client.encode_mezzanine_to_output(mezz, out, audio_args=["-c:a", "aac"])
|
||||
assert result["output_size"] == 5000 and str(out) == result["output_path"]
|
||||
body = m_post.call_args[0][0]
|
||||
assert "-c:a" in body["ffmpeg_args"] and "aac" in body["ffmpeg_args"]
|
||||
@@ -261,11 +294,16 @@ class TestEncodeMezzanine:
|
||||
m_ossdel.assert_called_once_with("osskey1")
|
||||
|
||||
def test_happy_path_no_audio_uses_an_and_cq(self, client, tmp_path):
|
||||
mezz = tmp_path / "m.mp4"; mezz.write_bytes(b"M")
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"M")
|
||||
out = tmp_path / "o.mp4"
|
||||
with (
|
||||
mock.patch.object(client, "_upload_mezzanine", return_value=("http://oss/u","k")),
|
||||
mock.patch.object(client, "_post_sync", return_value={"status":"completed","ffmpeg_rc":0,"uploaded":True,"job_id":"j"}) as m_post,
|
||||
mock.patch.object(client, "_upload_mezzanine", return_value=("http://oss/u", "k")),
|
||||
mock.patch.object(
|
||||
client,
|
||||
"_post_sync",
|
||||
return_value={"status": "completed", "ffmpeg_rc": 0, "uploaded": True, "job_id": "j"},
|
||||
) as m_post,
|
||||
mock.patch.object(client, "_download_to_file", return_value=100),
|
||||
mock.patch.object(client, "_relay_delete"),
|
||||
mock.patch.object(client, "_delete_oss"),
|
||||
@@ -276,18 +314,26 @@ class TestEncodeMezzanine:
|
||||
assert str(client.crf) in body["ffmpeg_args"]
|
||||
|
||||
def test_bitrate_set_uses_bv_instead_of_cq(self, tmp_path):
|
||||
c = GpuEncoderClient(endpoint="http://gpu", relay_base_url="http://api",
|
||||
relay_internal_base_url="http://api-int:8000", relay_secret="s", bitrate="2M")
|
||||
mezz = tmp_path / "m.mp4"; mezz.write_bytes(b"x")
|
||||
c = GpuEncoderClient(
|
||||
endpoint="http://gpu",
|
||||
relay_base_url="http://api",
|
||||
relay_internal_base_url="http://api-int:8000",
|
||||
relay_secret="s",
|
||||
bitrate="2M",
|
||||
)
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
out = tmp_path / "o.mp4"
|
||||
with (
|
||||
mock.patch.object(c, "_upload_mezzanine", return_value=("http://oss/u","k")),
|
||||
mock.patch.object(c, "_post_sync", return_value={"status":"completed","ffmpeg_rc":0,"uploaded":True,"job_id":"j"}) as m_post,
|
||||
mock.patch.object(c, "_upload_mezzanine", return_value=("http://oss/u", "k")),
|
||||
mock.patch.object(
|
||||
c, "_post_sync", return_value={"status": "completed", "ffmpeg_rc": 0, "uploaded": True, "job_id": "j"}
|
||||
) as m_post,
|
||||
mock.patch.object(c, "_download_to_file", return_value=10),
|
||||
mock.patch.object(c, "_relay_delete"),
|
||||
mock.patch.object(c, "_delete_oss"),
|
||||
):
|
||||
c.encode_mezzanine_to_output(mezz, out, extra_video_args=["-vf","scale=1280:-2"])
|
||||
c.encode_mezzanine_to_output(mezz, out, extra_video_args=["-vf", "scale=1280:-2"])
|
||||
body = m_post.call_args[0][0]
|
||||
assert "-b:v" in body["ffmpeg_args"] and "2M" in body["ffmpeg_args"]
|
||||
assert "-cq" not in body["ffmpeg_args"]
|
||||
@@ -295,44 +341,48 @@ class TestEncodeMezzanine:
|
||||
|
||||
def test_mezzanine_not_found_raises(self, client, tmp_path):
|
||||
with pytest.raises(GpuEncodeError, match="mezzanine file not found"):
|
||||
client.encode_mezzanine_to_output(tmp_path/"nope.mp4", tmp_path/"o.mp4")
|
||||
client.encode_mezzanine_to_output(tmp_path / "nope.mp4", tmp_path / "o.mp4")
|
||||
|
||||
def test_relay_base_not_configured_raises(self, tmp_path):
|
||||
c = GpuEncoderClient(endpoint="http://gpu", relay_base_url="", relay_secret="s")
|
||||
mezz = tmp_path/"m.mp4"; mezz.write_bytes(b"x")
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
with pytest.raises(GpuEncodeError, match="relay_base_url"):
|
||||
c.encode_mezzanine_to_output(mezz, tmp_path/"o.mp4")
|
||||
c.encode_mezzanine_to_output(mezz, tmp_path / "o.mp4")
|
||||
|
||||
def test_unexpected_exception_is_wrapped(self, client, tmp_path):
|
||||
mezz = tmp_path/"m.mp4"; mezz.write_bytes(b"x")
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
with (
|
||||
mock.patch.object(client, "_upload_mezzanine", return_value=("http://oss/u","k")),
|
||||
mock.patch.object(client, "_upload_mezzanine", return_value=("http://oss/u", "k")),
|
||||
mock.patch.object(client, "_post_sync", side_effect=RuntimeError("boom")),
|
||||
mock.patch.object(client, "_delete_oss"),
|
||||
):
|
||||
with pytest.raises(GpuEncodeError, match="unexpected: boom"):
|
||||
client.encode_mezzanine_to_output(mezz, tmp_path/"o.mp4")
|
||||
client.encode_mezzanine_to_output(mezz, tmp_path / "o.mp4")
|
||||
|
||||
def test_gpu_encode_error_re_raised_directly(self, client, tmp_path):
|
||||
mezz = tmp_path/"m.mp4"; mezz.write_bytes(b"x")
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
with (
|
||||
mock.patch.object(client, "_upload_mezzanine", return_value=("http://oss/u","k")),
|
||||
mock.patch.object(client, "_upload_mezzanine", return_value=("http://oss/u", "k")),
|
||||
mock.patch.object(client, "_post_sync", side_effect=GpuEncodeError("direct fail")),
|
||||
mock.patch.object(client, "_delete_oss"),
|
||||
):
|
||||
with pytest.raises(GpuEncodeError, match="direct fail"):
|
||||
client.encode_mezzanine_to_output(mezz, tmp_path/"o.mp4")
|
||||
client.encode_mezzanine_to_output(mezz, tmp_path / "o.mp4")
|
||||
|
||||
def test_oss_cleanup_runs_on_failure(self, client, tmp_path, caplog):
|
||||
mezz = tmp_path/"m.mp4"; mezz.write_bytes(b"x")
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
with (
|
||||
mock.patch.object(client, "_upload_mezzanine", return_value=("http://oss/u","ossk")),
|
||||
mock.patch.object(client, "_upload_mezzanine", return_value=("http://oss/u", "ossk")),
|
||||
mock.patch.object(client, "_post_sync", side_effect=GpuEncodeError("enc fail")),
|
||||
mock.patch.object(client, "_delete_oss", side_effect=Exception("oss down")) as m_ossdel,
|
||||
caplog.at_level("WARNING"),
|
||||
):
|
||||
with pytest.raises(GpuEncodeError):
|
||||
client.encode_mezzanine_to_output(mezz, tmp_path/"o.mp4")
|
||||
client.encode_mezzanine_to_output(mezz, tmp_path / "o.mp4")
|
||||
m_ossdel.assert_called_once_with("ossk")
|
||||
|
||||
|
||||
@@ -352,15 +402,18 @@ class TestRelayDelete:
|
||||
# ── OSS helpers ────────────────────────────────────────────────────
|
||||
class TestOssHelpers:
|
||||
def test_upload_storage_import_error(self, client, tmp_path):
|
||||
mezz = tmp_path/"m.mp4"; mezz.write_bytes(b"x")
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
# 删除 sys.modules 中 packages.shared.storage 使导入失败
|
||||
saved = sys.modules.pop("packages.shared.storage", None)
|
||||
try:
|
||||
real_import = __builtins__.__import__ if hasattr(__builtins__,"__import__") else __import__
|
||||
real_import = __builtins__.__import__ if hasattr(__builtins__, "__import__") else __import__
|
||||
|
||||
def fake_import(name, *a, **kw):
|
||||
if name == "packages.shared.storage" or name.startswith("packages.shared.storage."):
|
||||
raise ImportError("no storage")
|
||||
return real_import(name, *a, **kw)
|
||||
|
||||
with mock.patch("builtins.__import__", side_effect=fake_import):
|
||||
with pytest.raises(GpuEncodeError, match="storage service unavailable"):
|
||||
client._upload_mezzanine(mezz)
|
||||
@@ -369,33 +422,45 @@ class TestOssHelpers:
|
||||
sys.modules["packages.shared.storage"] = saved
|
||||
|
||||
def test_upload_storage_none(self, client, tmp_path):
|
||||
mezz = tmp_path/"m.mp4"; mezz.write_bytes(b"x")
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_storage_service.return_value = None
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_storage_service.return_value = None
|
||||
with mock.patch.dict("sys.modules", {"packages.shared.storage": fake_mod}):
|
||||
with pytest.raises(GpuEncodeError, match="OSS storage not configured"):
|
||||
client._upload_mezzanine(mezz)
|
||||
|
||||
def test_upload_bucket_none(self, client, tmp_path):
|
||||
mezz = tmp_path/"m.mp4"; mezz.write_bytes(b"x")
|
||||
svc = mock.MagicMock(); svc.bucket = None
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_storage_service.return_value = svc
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
svc = mock.MagicMock()
|
||||
svc.bucket = None
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_storage_service.return_value = svc
|
||||
with mock.patch.dict("sys.modules", {"packages.shared.storage": fake_mod}):
|
||||
with pytest.raises(GpuEncodeError, match="OSS storage not configured"):
|
||||
client._upload_mezzanine(mezz)
|
||||
|
||||
def test_upload_failure_raises(self, client, tmp_path):
|
||||
mezz = tmp_path/"m.mp4"; mezz.write_bytes(b"x")
|
||||
svc = mock.MagicMock(); svc.bucket = object(); svc.upload_file.side_effect = RuntimeError("oss err")
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_storage_service.return_value = svc
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
svc = mock.MagicMock()
|
||||
svc.bucket = object()
|
||||
svc.upload_file.side_effect = RuntimeError("oss err")
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_storage_service.return_value = svc
|
||||
with mock.patch.dict("sys.modules", {"packages.shared.storage": fake_mod}):
|
||||
with pytest.raises(GpuEncodeError, match="failed to upload mezzanine"):
|
||||
client._upload_mezzanine(mezz)
|
||||
|
||||
def test_upload_success(self, client, tmp_path):
|
||||
mezz = tmp_path/"m.mp4"; mezz.write_bytes(b"x")
|
||||
svc = mock.MagicMock(); svc.bucket = object()
|
||||
mezz = tmp_path / "m.mp4"
|
||||
mezz.write_bytes(b"x")
|
||||
svc = mock.MagicMock()
|
||||
svc.bucket = object()
|
||||
svc.get_download_url.return_value = "https://oss/signed?sig=abc"
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_storage_service.return_value = svc
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_storage_service.return_value = svc
|
||||
with mock.patch.dict("sys.modules", {"packages.shared.storage": fake_mod}):
|
||||
url, key = client._upload_mezzanine(mezz)
|
||||
assert url.startswith("https://oss/signed")
|
||||
@@ -403,21 +468,26 @@ class TestOssHelpers:
|
||||
svc.upload_file.assert_called_once()
|
||||
|
||||
def test_delete_oss_exception_swallowed(self, client, caplog):
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_storage_service.side_effect = RuntimeError("svc down")
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_storage_service.side_effect = RuntimeError("svc down")
|
||||
with mock.patch.dict("sys.modules", {"packages.shared.storage": fake_mod}), caplog.at_level("DEBUG"):
|
||||
client._delete_oss("somekey")
|
||||
assert "OSS delete" in caplog.text
|
||||
|
||||
def test_delete_oss_bucket_none_noop(self, client):
|
||||
svc = mock.MagicMock(); svc.bucket = None
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_storage_service.return_value = svc
|
||||
svc = mock.MagicMock()
|
||||
svc.bucket = None
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_storage_service.return_value = svc
|
||||
with mock.patch.dict("sys.modules", {"packages.shared.storage": fake_mod}):
|
||||
client._delete_oss("k")
|
||||
svc.delete_file.assert_not_called()
|
||||
|
||||
def test_delete_oss_success(self, client):
|
||||
svc = mock.MagicMock(); svc.bucket = object()
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_storage_service.return_value = svc
|
||||
svc = mock.MagicMock()
|
||||
svc.bucket = object()
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_storage_service.return_value = svc
|
||||
with mock.patch.dict("sys.modules", {"packages.shared.storage": fake_mod}):
|
||||
client._delete_oss("k")
|
||||
svc.delete_file.assert_called_once_with("k")
|
||||
@@ -436,36 +506,51 @@ class TestSingletonFactory:
|
||||
sys.modules["packages.config"] = saved
|
||||
|
||||
def test_build_client_not_enabled_returns_none(self):
|
||||
s = mock.MagicMock(); s.enable_gpu_encode = False
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_shared_settings.return_value = s
|
||||
s = mock.MagicMock()
|
||||
s.enable_gpu_encode = False
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_shared_settings.return_value = s
|
||||
with mock.patch.dict("sys.modules", {"packages.config": fake_mod}):
|
||||
assert _build_client_from_settings() is None
|
||||
|
||||
def test_build_client_missing_endpoint(self):
|
||||
s = mock.MagicMock(); s.enable_gpu_encode = True
|
||||
s.gpu_encode_endpoint = ""; s.gpu_encode_relay_base_url = "http://api"
|
||||
s = mock.MagicMock()
|
||||
s.enable_gpu_encode = True
|
||||
s.gpu_encode_endpoint = ""
|
||||
s.gpu_encode_relay_base_url = "http://api"
|
||||
s.gpu_encode_relay_internal_base_url = ""
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_shared_settings.return_value = s
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_shared_settings.return_value = s
|
||||
with mock.patch.dict("sys.modules", {"packages.config": fake_mod}):
|
||||
assert _build_client_from_settings() is None
|
||||
|
||||
def test_build_client_missing_relay(self):
|
||||
s = mock.MagicMock(); s.enable_gpu_encode = True
|
||||
s.gpu_encode_endpoint = "http://gpu"; s.gpu_encode_relay_base_url = ""
|
||||
s = mock.MagicMock()
|
||||
s.enable_gpu_encode = True
|
||||
s.gpu_encode_endpoint = "http://gpu"
|
||||
s.gpu_encode_relay_base_url = ""
|
||||
s.gpu_encode_relay_internal_base_url = ""
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_shared_settings.return_value = s
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_shared_settings.return_value = s
|
||||
with mock.patch.dict("sys.modules", {"packages.config": fake_mod}):
|
||||
assert _build_client_from_settings() is None
|
||||
|
||||
def test_build_client_success(self):
|
||||
s = mock.MagicMock(); s.enable_gpu_encode = True
|
||||
s.gpu_encode_endpoint = "http://gpu"; s.gpu_encode_relay_base_url = "http://api/"
|
||||
s = mock.MagicMock()
|
||||
s.enable_gpu_encode = True
|
||||
s.gpu_encode_endpoint = "http://gpu"
|
||||
s.gpu_encode_relay_base_url = "http://api/"
|
||||
s.gpu_encode_relay_internal_base_url = "http://api-int:8000/"
|
||||
s.gpu_encode_sync_timeout = 120; s.gpu_encode_health_timeout = 1.0
|
||||
s.gpu_encode_vcodec = "h264_nvenc"; s.gpu_encode_preset = "p7"
|
||||
s.gpu_encode_crf = 20; s.gpu_encode_bitrate = ""; s.gpu_encode_relay_secret = "s"
|
||||
s.gpu_encode_sync_timeout = 120
|
||||
s.gpu_encode_health_timeout = 1.0
|
||||
s.gpu_encode_vcodec = "h264_nvenc"
|
||||
s.gpu_encode_preset = "p7"
|
||||
s.gpu_encode_crf = 20
|
||||
s.gpu_encode_bitrate = ""
|
||||
s.gpu_encode_relay_secret = "s"
|
||||
s.gpu_encode_oss_tmp_prefix = "tmp/x/"
|
||||
fake_mod = mock.MagicMock(); fake_mod.get_shared_settings.return_value = s
|
||||
fake_mod = mock.MagicMock()
|
||||
fake_mod.get_shared_settings.return_value = s
|
||||
with mock.patch.dict("sys.modules", {"packages.config": fake_mod}):
|
||||
c = _build_client_from_settings()
|
||||
assert c is not None and c.endpoint == "http://gpu"
|
||||
@@ -474,8 +559,10 @@ class TestSingletonFactory:
|
||||
assert c.preset == "p7" and c.crf == 20
|
||||
|
||||
def test_get_gpu_encoder_init_failure_returns_none(self, caplog):
|
||||
with mock.patch("packages.shared.gpu_encoder._build_client_from_settings",
|
||||
side_effect=RuntimeError("boom")), caplog.at_level("WARNING"):
|
||||
with (
|
||||
mock.patch("packages.shared.gpu_encoder._build_client_from_settings", side_effect=RuntimeError("boom")),
|
||||
caplog.at_level("WARNING"),
|
||||
):
|
||||
assert get_gpu_encoder() is None
|
||||
assert "failed to init client" in caplog.text
|
||||
|
||||
@@ -493,11 +580,11 @@ class TestSingletonFactory:
|
||||
# ── Constructor edge cases ────────────────────────────────────────
|
||||
class TestConstructor:
|
||||
def test_oss_tmp_prefix_empty_uses_default(self):
|
||||
c = GpuEncoderClient(endpoint="http://gpu", relay_base_url="http://api",
|
||||
relay_secret="s", oss_tmp_prefix="")
|
||||
c = GpuEncoderClient(endpoint="http://gpu", relay_base_url="http://api", relay_secret="s", oss_tmp_prefix="")
|
||||
assert c.oss_tmp_prefix == "tmp/gpu-mezzanine/"
|
||||
|
||||
def test_oss_tmp_prefix_strips_and_adds_slash(self):
|
||||
c = GpuEncoderClient(endpoint="http://gpu", relay_base_url="http://api",
|
||||
relay_secret="s", oss_tmp_prefix="tmp/foo")
|
||||
c = GpuEncoderClient(
|
||||
endpoint="http://gpu", relay_base_url="http://api", relay_secret="s", oss_tmp_prefix="tmp/foo"
|
||||
)
|
||||
assert c.oss_tmp_prefix == "tmp/foo/"
|
||||
|
||||
Reference in New Issue
Block a user