s
This commit is contained in:
@@ -3,12 +3,26 @@ from __future__ import annotations
|
||||
import ast
|
||||
import asyncio
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import web
|
||||
|
||||
|
||||
class _JsonRequest:
|
||||
def __init__(self, payload: dict[str, object]):
|
||||
self._payload = payload
|
||||
|
||||
async def json(self) -> dict[str, object]:
|
||||
return self._payload
|
||||
|
||||
|
||||
def _json_body(resp) -> dict[str, object]: # noqa: ANN001
|
||||
return json.loads(resp.body)
|
||||
|
||||
|
||||
class WebDynamicProcessesTests(unittest.TestCase):
|
||||
def test_process_monitor_entries_include_saved_youtube_pro_channels(self) -> None:
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube2__lane1"}]
|
||||
@@ -153,6 +167,59 @@ class WebDynamicProcessesTests(unittest.TestCase):
|
||||
self.assertEqual(payload["output"], "ok")
|
||||
self.assertEqual(calls, ["kill:youtube", "restart:youtube:youtube.py"])
|
||||
|
||||
def test_get_url_config_does_not_resync_existing_managed_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config_dir = Path(tmp)
|
||||
rel = "URL_config.youtube.ini"
|
||||
(config_dir / rel).write_text("https://live.douyin.com/new\n", encoding="utf-8")
|
||||
|
||||
with patch("web.CONFIG_DIR", config_dir):
|
||||
with patch("web.allows_url_config", return_value=True):
|
||||
with patch("web.managed_url_config_relpath", return_value=rel):
|
||||
with patch("web._sync_youtube_pro_runtime_files", side_effect=AssertionError("unexpected resync")):
|
||||
payload = asyncio.run(web.get_url_config("youtube"))
|
||||
|
||||
self.assertEqual(_json_body(payload)["content"], "https://live.douyin.com/new\n")
|
||||
self.assertEqual(payload.headers.get("cache-control"), "no-store, max-age=0, must-revalidate")
|
||||
|
||||
def test_save_url_config_updates_matching_youtube_pro_channel_store(self) -> None:
|
||||
captured: list[list[dict[str, object]]] = []
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube", "urlLines": "old\n"}]
|
||||
|
||||
with patch("web.allows_url_config", return_value=True):
|
||||
with patch("web.managed_url_config_relpath", return_value="URL_config.youtube.ini"):
|
||||
with patch("web.governed_save_managed_file"):
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
|
||||
payload = asyncio.run(
|
||||
web.save_url_config(
|
||||
_JsonRequest({"content": "https://live.douyin.com/fresh\n"}),
|
||||
process="youtube",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
|
||||
self.assertEqual(captured[-1][0]["urlLines"], "https://live.douyin.com/fresh\n")
|
||||
|
||||
def test_save_config_updates_matching_youtube_pro_channel_stream_key(self) -> None:
|
||||
captured: list[list[dict[str, object]]] = []
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube", "streamKey": "old-key"}]
|
||||
content = "[youtube]\nkey = new-key-123\nrtmps = 是\n"
|
||||
|
||||
with patch("web.managed_youtube_ini_relpath", return_value="youtube.youtube.ini"):
|
||||
with patch("web.governed_save_managed_file"):
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
with patch("web.set_youtube_pro_channels", side_effect=lambda rows: captured.append(rows)):
|
||||
payload = asyncio.run(
|
||||
web.save_config(
|
||||
_JsonRequest({"content": content}),
|
||||
process="youtube",
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(_json_body(payload)["message"], "配置保存成功")
|
||||
self.assertEqual(captured[-1][0]["streamKey"], "new-key-123")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -6,7 +6,13 @@ import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from src.web_process_backend import WebProcessBackend, build_pm2_start_command
|
||||
from src.web_process_backend import (
|
||||
WebProcessBackend,
|
||||
_ffmpeg_pid_matches_process,
|
||||
build_pm2_start_command,
|
||||
pm2_process_entry_needs_recreate,
|
||||
preferred_python_interpreter,
|
||||
)
|
||||
|
||||
|
||||
class WebProcessBackendTests(unittest.TestCase):
|
||||
@@ -17,6 +23,7 @@ class WebProcessBackendTests(unittest.TestCase):
|
||||
venv_python = base_dir / ".venv" / "bin" / "python"
|
||||
venv_python.parent.mkdir(parents=True, exist_ok=True)
|
||||
venv_python.write_text("#!/usr/bin/env python3\n", encoding="utf-8")
|
||||
venv_python.chmod(0o755)
|
||||
script.write_text("print('ok')\n", encoding="utf-8")
|
||||
|
||||
cmd = build_pm2_start_command(base_dir, "youtube2__lane1", "youtube.py")
|
||||
@@ -29,6 +36,62 @@ class WebProcessBackendTests(unittest.TestCase):
|
||||
self.assertIn(str(script), cmd)
|
||||
self.assertIn(str(venv_python), cmd)
|
||||
|
||||
def test_preferred_python_interpreter_falls_back_to_system_python_when_venv_is_missing(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
with patch("src.web_process_backend.shutil_which", side_effect=lambda name: f"/usr/bin/{name}" if name == "python3" else None):
|
||||
with patch("src.web_process_backend.sys.executable", "/definitely/missing/python"):
|
||||
chosen = preferred_python_interpreter(base_dir)
|
||||
|
||||
self.assertEqual(chosen, "/usr/bin/python3")
|
||||
|
||||
def test_pm2_process_entry_needs_recreate_when_python_interpreter_is_missing(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
script = base_dir / "youtube.py"
|
||||
script.write_text("print('ok')\n", encoding="utf-8")
|
||||
item = {
|
||||
"pm_exec_path": str(script),
|
||||
"pm_cwd": str(base_dir),
|
||||
"pm2_env": {
|
||||
"exec_interpreter": str(base_dir / ".venv" / "bin" / "python"),
|
||||
},
|
||||
}
|
||||
|
||||
self.assertTrue(pm2_process_entry_needs_recreate(base_dir, "youtube", "youtube.py", item))
|
||||
|
||||
def test_start_recreates_stale_pm2_entry_instead_of_reusing_broken_interpreter(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
script = base_dir / "youtube.py"
|
||||
script.write_text("print('ok')\n", encoding="utf-8")
|
||||
|
||||
backend = WebProcessBackend(base_dir, base_dir / ".pm2" / "logs")
|
||||
backend._use_pm2 = True
|
||||
stale_item = {
|
||||
"pm_exec_path": str(script),
|
||||
"pm_cwd": str(base_dir),
|
||||
"pm2_env": {
|
||||
"exec_interpreter": str(base_dir / ".venv" / "bin" / "python"),
|
||||
},
|
||||
}
|
||||
|
||||
with patch.object(backend, "ensure_pm2_runtime", AsyncMock()):
|
||||
with patch.object(backend, "_pm2_jlist_index", AsyncMock(return_value={"youtube": stale_item})):
|
||||
with patch.object(backend, "clear_process_logs", AsyncMock()):
|
||||
with patch("src.web_process_backend.build_pm2_start_command", return_value="pm2 start fresh") as build_cmd:
|
||||
with patch("src.web_process_backend.run_pm2_result", new=AsyncMock(return_value=(0, "deleted"))) as run_pm2_result:
|
||||
with patch("src.web_process_backend._shell_out", new=AsyncMock(return_value=(0, "created"))) as shell_out:
|
||||
with patch.object(backend, "_set_pm2_desired_running", AsyncMock()) as set_desired:
|
||||
with patch.object(backend, "_pm2_save", AsyncMock()):
|
||||
text = asyncio.run(backend.start("youtube", "youtube.py"))
|
||||
|
||||
self.assertEqual(text, "created")
|
||||
build_cmd.assert_called_once_with(base_dir, "youtube", "youtube.py")
|
||||
run_pm2_result.assert_awaited_once_with("delete youtube", timeout=20.0)
|
||||
shell_out.assert_awaited_once_with("pm2 start fresh", timeout=45.0)
|
||||
set_desired.assert_awaited_once_with("youtube", True)
|
||||
|
||||
def test_parse_pm2_describe_marks_missing_jlist_process_as_not_found(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
@@ -156,6 +219,19 @@ class WebProcessBackendTests(unittest.TestCase):
|
||||
self.assertEqual(pids, [9876])
|
||||
finder.assert_called_once_with(4321, root_pgid=6789)
|
||||
|
||||
def test_ffmpeg_pid_matches_process_uses_exact_inherited_env_name(self) -> None:
|
||||
env_map = {
|
||||
"LIVE_PM2_PROCESS_NAME": "youtube2__lane1",
|
||||
"PM2_SERVICE_NAME": "",
|
||||
}
|
||||
|
||||
def fake_env(pid: int, key: str) -> str:
|
||||
return env_map.get(key, "")
|
||||
|
||||
with patch("src.web_process_backend._read_proc_env_value", side_effect=fake_env):
|
||||
self.assertTrue(_ffmpeg_pid_matches_process(1234, "youtube2__lane1"))
|
||||
self.assertFalse(_ffmpeg_pid_matches_process(1234, "youtube"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user