's'
This commit is contained in:
1
d2ypp2/tests/__init__.py
Normal file
1
d2ypp2/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
47
d2ypp2/tests/test_android_redroid.py
Normal file
47
d2ypp2/tests/test_android_redroid.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from src.android_control import enrich_device_row
|
||||
from src.redroid_control import load_redroid_env, redroid_instance_name, redroid_instance_port
|
||||
|
||||
|
||||
class AndroidRedroidTests(unittest.TestCase):
|
||||
def test_enrich_device_row_marks_usb_physical(self) -> None:
|
||||
row = enrich_device_row({"serial": "ABC123", "state": "device", "model": "Pixel"})
|
||||
self.assertEqual(row["transport"], "usb")
|
||||
self.assertEqual(row["kind"], "physical")
|
||||
|
||||
def test_enrich_device_row_marks_redroid(self) -> None:
|
||||
row = enrich_device_row(
|
||||
{
|
||||
"serial": "127.0.0.1:5555",
|
||||
"state": "device",
|
||||
"model": "redroid",
|
||||
"product": "redroid",
|
||||
}
|
||||
)
|
||||
self.assertEqual(row["transport"], "tcp")
|
||||
self.assertEqual(row["kind"], "redroid")
|
||||
|
||||
@patch("src.redroid_control.platform.machine", return_value="x86_64")
|
||||
def test_redroid_env_prefers_x86_alt_image(self, _machine) -> None:
|
||||
env = load_redroid_env()
|
||||
self.assertIn("redroid:", env["REDROID_IMAGE"])
|
||||
self.assertEqual(env["REDROID_IMAGE"], env["REDROID_IMAGE_X86_ALT"])
|
||||
|
||||
@patch("src.redroid_control.platform.machine", return_value="aarch64")
|
||||
def test_redroid_env_prefers_arm_image_and_guest_gpu_bootarg(self, _machine) -> None:
|
||||
env = load_redroid_env()
|
||||
self.assertEqual(env["REDROID_IMAGE"], "fahaddz/redroid:13-arm-pi5")
|
||||
self.assertIn("androidboot.redroid_gpu_mode=guest", env["REDROID_BOOT_ARGS"])
|
||||
|
||||
def test_redroid_default_instance_layout(self) -> None:
|
||||
self.assertEqual(redroid_instance_name(1), "redroid13-1")
|
||||
self.assertEqual(redroid_instance_port(1), 5555)
|
||||
self.assertEqual(redroid_instance_port(3), 5557)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
137
d2ypp2/tests/test_config_governance.py
Normal file
137
d2ypp2/tests/test_config_governance.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from src.config_governance import (
|
||||
analyze_content,
|
||||
backup_managed_file,
|
||||
optimize_content,
|
||||
save_managed_file,
|
||||
)
|
||||
from src.control_plane import ManagedRoot
|
||||
|
||||
|
||||
class ConfigGovernanceTests(unittest.TestCase):
|
||||
def test_url_config_detects_duplicates(self) -> None:
|
||||
sample = "\n".join(
|
||||
[
|
||||
"https://live.douyin.com/123456,主播A",
|
||||
"https://live.douyin.com/123456,主播A重复",
|
||||
"# https://live.douyin.com/999999",
|
||||
"https://www.tiktok.com/@demo/live",
|
||||
]
|
||||
)
|
||||
result = analyze_content("app-config", "URL_config.ini", sample, source="draft")
|
||||
codes = {item["code"] for item in result["issues"]}
|
||||
self.assertIn("duplicate_urls", codes)
|
||||
self.assertTrue(any(item["id"] == "dedupe_urls" for item in result["actions"]))
|
||||
|
||||
def test_dedupe_urls_keeps_single_active_copy(self) -> None:
|
||||
sample = "\n".join(
|
||||
[
|
||||
"# keep comment",
|
||||
"https://live.douyin.com/123456",
|
||||
"https://live.douyin.com/123456",
|
||||
"https://live.douyin.com/654321",
|
||||
]
|
||||
)
|
||||
result = optimize_content("app-config", "URL_config.ini", "dedupe_urls", sample)
|
||||
content = result["content"]
|
||||
self.assertEqual(content.count("https://live.douyin.com/123456"), 1)
|
||||
self.assertIn("# keep comment", content)
|
||||
self.assertIn("https://live.douyin.com/654321", content)
|
||||
|
||||
def test_youtube_preset_preserves_key_and_enables_balanced_defaults(self) -> None:
|
||||
sample = "[youtube]\nkey = demo-key\nrtmps = 否\nfast_audio = 0\n"
|
||||
result = optimize_content("app-config", "youtube.ini", "youtube_balanced_preset", sample)
|
||||
content = result["content"]
|
||||
self.assertIn("key = demo-key", content)
|
||||
self.assertIn("rtmps = 是", content)
|
||||
self.assertIn("bitrate = 4500", content)
|
||||
self.assertIn("fast_audio = 1", content)
|
||||
|
||||
def test_backup_managed_file_reclaims_history_when_disk_full(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
root_path = tmp_path / "config"
|
||||
root_path.mkdir(parents=True, exist_ok=True)
|
||||
target = root_path / "youtube.demo.ini"
|
||||
target.write_text("[youtube]\nkey = old\n", encoding="utf-8")
|
||||
root = ManagedRoot(
|
||||
root_id="app-config",
|
||||
label="App Config",
|
||||
description="test",
|
||||
path=root_path,
|
||||
explicit_names=("youtube.demo.ini",),
|
||||
)
|
||||
hist_dir = tmp_path / "backup_config" / "__web__" / "app-config" / "youtube.demo.ini.history"
|
||||
hist_dir.mkdir(parents=True, exist_ok=True)
|
||||
(hist_dir / "stale-1.ini").write_text("1", encoding="utf-8")
|
||||
(hist_dir / "stale-2.ini").write_text("2", encoding="utf-8")
|
||||
|
||||
calls = {"n": 0}
|
||||
real_copy2 = shutil.copy2
|
||||
|
||||
def flaky_copy2(src, dst, *args, **kwargs):
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 1:
|
||||
raise OSError(errno.ENOSPC, "No space left on device")
|
||||
return real_copy2(src, dst, *args, **kwargs)
|
||||
|
||||
with mock.patch("src.config_governance.resolve_managed_file", return_value=(root, Path("youtube.demo.ini"), target)):
|
||||
with mock.patch("src.config_governance.WEB_BACKUP_DIR", tmp_path / "backup_config" / "__web__"):
|
||||
with mock.patch("src.config_governance.shutil.copy2", side_effect=flaky_copy2):
|
||||
backup = backup_managed_file("app-config", "youtube.demo.ini", reason="save")
|
||||
|
||||
self.assertIsNotNone(backup)
|
||||
self.assertGreaterEqual(calls["n"], 2)
|
||||
remaining = sorted(p.name for p in hist_dir.iterdir() if p.is_file())
|
||||
self.assertEqual(len(remaining), 1)
|
||||
self.assertTrue(remaining[0].endswith(".ini"))
|
||||
|
||||
def test_save_managed_file_retries_after_enospc(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
root_path = tmp_path / "config"
|
||||
root_path.mkdir(parents=True, exist_ok=True)
|
||||
target = root_path / "youtube.demo.ini"
|
||||
target.write_text("[youtube]\nkey = old\n", encoding="utf-8")
|
||||
root = ManagedRoot(
|
||||
root_id="app-config",
|
||||
label="App Config",
|
||||
description="test",
|
||||
path=root_path,
|
||||
explicit_names=("youtube.demo.ini",),
|
||||
)
|
||||
hist_dir = tmp_path / "backup_config" / "__web__" / "app-config" / "youtube.demo.ini.history"
|
||||
hist_dir.mkdir(parents=True, exist_ok=True)
|
||||
(hist_dir / "stale.ini").write_text("stale", encoding="utf-8")
|
||||
|
||||
calls = {"n": 0}
|
||||
real_write_text = Path.write_text
|
||||
|
||||
def flaky_write_text(self, *args, **kwargs):
|
||||
if self == target:
|
||||
calls["n"] += 1
|
||||
if calls["n"] == 1:
|
||||
raise OSError(errno.ENOSPC, "No space left on device")
|
||||
return real_write_text(self, *args, **kwargs)
|
||||
|
||||
with mock.patch("src.config_governance.resolve_managed_file", return_value=(root, Path("youtube.demo.ini"), target)):
|
||||
with mock.patch("src.config_governance.WEB_BACKUP_DIR", tmp_path / "backup_config" / "__web__"):
|
||||
with mock.patch("pathlib.Path.write_text", new=flaky_write_text):
|
||||
result = save_managed_file("app-config", "youtube.demo.ini", "[youtube]\nkey = new\n")
|
||||
|
||||
self.assertEqual(result["message"], "Saved successfully")
|
||||
self.assertEqual(target.read_text(encoding="utf-8"), "[youtube]\nkey = new\n")
|
||||
self.assertGreaterEqual(calls["n"], 2)
|
||||
self.assertFalse(hist_dir.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
35
d2ypp2/tests/test_control_plane_services.py
Normal file
35
d2ypp2/tests/test_control_plane_services.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from src.control_plane import SERVICE_SPECS, stack_summary
|
||||
|
||||
|
||||
class ControlPlaneServiceTests(unittest.TestCase):
|
||||
def test_service_registry_includes_required_modules(self) -> None:
|
||||
ids = {spec.service_id for spec in SERVICE_SPECS}
|
||||
self.assertTrue(
|
||||
{
|
||||
"mitmproxy",
|
||||
"vscode_server",
|
||||
"uptime_kuma",
|
||||
"pulseaudio",
|
||||
"kmscube",
|
||||
"openclaw",
|
||||
}.issubset(ids)
|
||||
)
|
||||
|
||||
def test_stack_summary_exposes_new_quick_links(self) -> None:
|
||||
links = stack_summary()["quick_links"]
|
||||
for key in (
|
||||
"mitmproxy",
|
||||
"vscode_server",
|
||||
"uptime_kuma",
|
||||
"pulseaudio_tcp",
|
||||
"openclaw",
|
||||
):
|
||||
self.assertIn(key, links)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
40
d2ypp2/tests/test_hdmi_control.py
Normal file
40
d2ypp2/tests/test_hdmi_control.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from src.hdmi_control import _parse_v4l2_devices, _recommend_capture_device
|
||||
|
||||
|
||||
class HdmiControlTests(unittest.TestCase):
|
||||
def test_parse_v4l2_devices_extracts_nodes(self) -> None:
|
||||
text = (
|
||||
"USB Video:\n"
|
||||
"\t/dev/video0\n"
|
||||
"\t/dev/video1\n"
|
||||
"\n"
|
||||
"bcm2835-codec:\n"
|
||||
"\t/dev/video10\n"
|
||||
)
|
||||
items = _parse_v4l2_devices(text)
|
||||
self.assertEqual(items[0]["name"], "USB Video")
|
||||
self.assertEqual(items[0]["nodes"], ["/dev/video0", "/dev/video1"])
|
||||
self.assertTrue(items[0]["is_capture_candidate"])
|
||||
|
||||
def test_parse_v4l2_devices_handles_empty(self) -> None:
|
||||
self.assertEqual(_parse_v4l2_devices(""), [])
|
||||
|
||||
def test_recommend_capture_device_skips_codec_only_nodes(self) -> None:
|
||||
text = (
|
||||
"bcm2835-codec:\n"
|
||||
"\t/dev/video10\n"
|
||||
"\n"
|
||||
"USB Video:\n"
|
||||
"\t/dev/video0\n"
|
||||
)
|
||||
items = _parse_v4l2_devices(text)
|
||||
self.assertFalse(items[0]["is_capture_candidate"])
|
||||
self.assertEqual(_recommend_capture_device(["/dev/video0", "/dev/video10"], items), "/dev/video0")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
58
d2ypp2/tests/test_hub_routes.py
Normal file
58
d2ypp2/tests/test_hub_routes.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from src.hub_routes import configure_hub, fallback_live_event_lines, hub_dashboard
|
||||
|
||||
|
||||
class HubRoutesTests(unittest.TestCase):
|
||||
def test_fallback_live_event_lines_reads_local_process_logs(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
out_path = Path(tmp) / "youtube-out.log"
|
||||
err_path = Path(tmp) / "youtube-error.log"
|
||||
out_path.write_text("booting\nstream ready\n", encoding="utf-8")
|
||||
err_path.write_text("warning line\ntraceback line\n", encoding="utf-8")
|
||||
rows = [
|
||||
{
|
||||
"label": "YouTube",
|
||||
"pm2": "youtube",
|
||||
"out_path": str(out_path),
|
||||
"err_path": str(err_path),
|
||||
}
|
||||
]
|
||||
lines = fallback_live_event_lines(rows, max_lines=4)
|
||||
self.assertEqual(len(lines), 4)
|
||||
self.assertTrue(lines[0].startswith("[YouTube stderr]"))
|
||||
self.assertTrue(lines[-1].endswith("stream ready"))
|
||||
|
||||
def test_hub_dashboard_marks_local_declared_process_as_configured(self) -> None:
|
||||
backend = type("StubBackend", (), {})()
|
||||
backend.mode = "local"
|
||||
backend.ensure_mode = AsyncMock()
|
||||
backend.parse_pm2_describe = AsyncMock(return_value={"process_status": "not_found", "out_path": None, "err_path": None})
|
||||
backend.live_ffmpeg_pids = AsyncMock(return_value=[])
|
||||
configure_hub(backend, [{"pm2": "youtube", "label": "YouTube", "script": "youtube.py"}])
|
||||
|
||||
with patch("src.hub_routes.system_snapshot", return_value={}):
|
||||
with patch("src.hub_routes.query_services", new=AsyncMock(return_value=[])):
|
||||
with patch("src.hub_routes.adb_available", return_value=False):
|
||||
with patch("src.hub_routes.stack_summary", return_value={}):
|
||||
with patch("src.hub_routes.stack_stream_probes", new=AsyncMock(return_value={})):
|
||||
with patch("src.hub_routes.recent_pm2_log_lines", return_value=[]):
|
||||
with patch("src.hub_routes.network_iface_stats", return_value={}):
|
||||
with patch("src.hub_routes.load_hardware_profile", new=AsyncMock(return_value={})):
|
||||
with patch("src.hub_routes.redroid_runtime_summary", new=AsyncMock(return_value={})):
|
||||
with patch("src.hub_routes.hdmi_capture_summary", new=AsyncMock(return_value={})):
|
||||
payload = asyncio.run(hub_dashboard())
|
||||
|
||||
row = payload["live"]["processes"][0]
|
||||
self.assertEqual(row["process_status"], "configured")
|
||||
self.assertEqual(row["business_status"], "ready")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
87
d2ypp2/tests/test_neko_config.py
Normal file
87
d2ypp2/tests/test_neko_config.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class NekoConfigTests(unittest.TestCase):
|
||||
def test_browser_launch_cleanup_script_still_prunes_profile_bloat(self) -> None:
|
||||
script = (ROOT / "services" / "neko" / "browser-launch.sh").read_text(encoding="utf-8")
|
||||
self.assertIn("arch=\"$(uname -m", script)
|
||||
self.assertIn("--disable-software-rasterizer", script)
|
||||
self.assertIn("--use-gl=swiftshader", script)
|
||||
self.assertIn("--bwsi", script)
|
||||
self.assertIn("NEKO_BROWSER_RENDER_MODE", script)
|
||||
self.assertIn('prune_path "$profile_dir/BrowserMetrics"', script)
|
||||
self.assertIn('prune_path "$profile_dir/Crash Reports"', script)
|
||||
self.assertIn('prune_path "$profile_dir/SingletonLock"', script)
|
||||
self.assertIn('prune_path "$profile_dir/Default/Code Cache"', script)
|
||||
|
||||
def test_browser_policies_default_to_allowed_permissions(self) -> None:
|
||||
policies = json.loads((ROOT / "services" / "neko" / "policies" / "policies.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(policies["DefaultNotificationsSetting"], 1)
|
||||
self.assertEqual(policies["DefaultGeolocationSetting"], 1)
|
||||
self.assertTrue(policies["VideoCaptureAllowed"])
|
||||
self.assertTrue(policies["AudioCaptureAllowed"])
|
||||
|
||||
def test_google_chrome_supervisor_uses_direct_stable_command(self) -> None:
|
||||
conf = (ROOT / "services" / "neko" / "google-chrome.conf").read_text(encoding="utf-8")
|
||||
self.assertIn("command=/usr/bin/google-chrome", conf)
|
||||
self.assertNotIn("browser-launch.sh", conf)
|
||||
self.assertIn("--user-data-dir=/home/neko/.config/google-chrome", conf)
|
||||
self.assertIn("--disable-software-rasterizer", conf)
|
||||
self.assertIn("autorestart=true", conf)
|
||||
self.assertIn("[program:openbox]", conf)
|
||||
self.assertIn("/usr/bin/openbox --config-file /etc/neko/openbox.xml", conf)
|
||||
|
||||
def test_chromium_supervisor_uses_direct_stable_command(self) -> None:
|
||||
conf = (ROOT / "services" / "neko" / "chromium.conf").read_text(encoding="utf-8")
|
||||
self.assertIn("command=/usr/bin/chromium", conf)
|
||||
self.assertNotIn("browser-launch.sh", conf)
|
||||
self.assertIn("--user-data-dir=/home/neko/.config/chromium", conf)
|
||||
self.assertIn("--disable-software-rasterizer", conf)
|
||||
self.assertIn("autorestart=true", conf)
|
||||
self.assertIn("[program:openbox]", conf)
|
||||
|
||||
def test_neko_compose_mounts_only_selected_supervisor_conf(self) -> None:
|
||||
compose = (ROOT / "services" / "neko" / "docker-compose.yml").read_text(encoding="utf-8")
|
||||
self.assertIn("./${NEKO_BROWSER_SUPERVISOR_CONF}:/etc/neko/supervisord/browser.conf:ro", compose)
|
||||
self.assertIn("container_name: live-neko-1", compose)
|
||||
self.assertIn("container_name: live-neko-2", compose)
|
||||
self.assertIn("container_name: live-neko-3", compose)
|
||||
self.assertIn("${NEKO_HTTP_PORT_1}:8080", compose)
|
||||
self.assertIn("${NEKO_HTTP_PORT_2}:8080", compose)
|
||||
self.assertIn("${NEKO_HTTP_PORT_3}:8080", compose)
|
||||
self.assertNotIn("./chromium.conf:/etc/neko/supervisord/chromium.conf:ro", compose)
|
||||
self.assertNotIn("./google-chrome.conf:/etc/neko/supervisord/google-chrome.conf:ro", compose)
|
||||
self.assertNotIn("./browser-launch.sh:/etc/neko/browser-launch.sh:ro", compose)
|
||||
|
||||
def test_neko_scripts_use_family_specific_profile_dirs(self) -> None:
|
||||
service_ctl = (ROOT / "scripts" / "linux" / "service_ctl.sh").read_text(encoding="utf-8")
|
||||
common = (ROOT / "scripts" / "linux" / "lib" / "common.sh").read_text(encoding="utf-8")
|
||||
self.assertIn("resolve_neko_profile_dir()", service_ctl)
|
||||
self.assertIn('${family}-profile-v2', service_ctl)
|
||||
self.assertIn("resolve_neko_profile_dir()", common)
|
||||
self.assertIn('${family}-profile-v2', common)
|
||||
self.assertIn("resolve_neko_profile_dir_for_instance()", service_ctl)
|
||||
self.assertIn("NEKO_HTTP_PORT_2", service_ctl)
|
||||
self.assertIn("NEKO_WEBRTC_UDP_RANGE_3", service_ctl)
|
||||
self.assertIn("resolve_neko_profile_dir_for_instance()", common)
|
||||
self.assertIn("NEKO_HTTP_PORT_2", common)
|
||||
self.assertIn("NEKO_WEBRTC_UDP_RANGE_3", common)
|
||||
|
||||
def test_local_browser_prefers_chrome_on_x86_and_chromium_elsewhere(self) -> None:
|
||||
script = (ROOT / "scripts" / "linux" / "launch_browser.sh").read_text(encoding="utf-8")
|
||||
self.assertIn("x86_64|amd64", script)
|
||||
x86_branch = script.split("x86_64|amd64)", 1)[1].split(";;", 1)[0]
|
||||
other_branch = script.split("*)", 1)[1].split(";;", 1)[0]
|
||||
self.assertLess(x86_branch.find("google-chrome"), x86_branch.find("chromium"))
|
||||
self.assertLess(other_branch.find("chromium"), other_branch.find("google-chrome"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
84
d2ypp2/tests/test_process_runtime_status.py
Normal file
84
d2ypp2/tests/test_process_runtime_status.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from src.process_runtime_status import (
|
||||
coerce_declared_process_status,
|
||||
normalize_log_text,
|
||||
summarize_process_activity,
|
||||
tail_log_text,
|
||||
)
|
||||
|
||||
|
||||
class ProcessRuntimeStatusTests(unittest.TestCase):
|
||||
def test_normalize_and_tail_log_text_handles_carriage_returns(self) -> None:
|
||||
raw = "line1\rline2\r\nline3\nline4"
|
||||
self.assertEqual(normalize_log_text(raw), "line1\nline2\nline3\nline4")
|
||||
self.assertEqual(tail_log_text(raw, 2), "line3\nline4\n")
|
||||
|
||||
def test_summarize_process_activity_marks_streaming_when_ffmpeg_is_active(self) -> None:
|
||||
state = summarize_process_activity(
|
||||
"youtube",
|
||||
"online",
|
||||
"监测 1 | 格式 YOUTUBE",
|
||||
"",
|
||||
log_age_seconds=3.0,
|
||||
ffmpeg_pids=[1234],
|
||||
)
|
||||
self.assertEqual(state["business_status"], "streaming")
|
||||
self.assertTrue(state["is_pushing"])
|
||||
|
||||
def test_summarize_process_activity_marks_waiting_source(self) -> None:
|
||||
state = summarize_process_activity(
|
||||
"youtube",
|
||||
"online",
|
||||
"等待直播...\n检测直播间中...",
|
||||
"",
|
||||
log_age_seconds=4.0,
|
||||
ffmpeg_pids=[],
|
||||
)
|
||||
self.assertEqual(state["business_status"], "waiting_source")
|
||||
self.assertFalse(state["is_pushing"])
|
||||
|
||||
def test_summarize_process_activity_marks_misconfigured(self) -> None:
|
||||
state = summarize_process_activity(
|
||||
"youtube",
|
||||
"online",
|
||||
"",
|
||||
"请在 config/youtube.ini 中配置 key=你的stream_key",
|
||||
log_age_seconds=2.0,
|
||||
ffmpeg_pids=[],
|
||||
)
|
||||
self.assertEqual(state["business_status"], "misconfigured")
|
||||
|
||||
def test_summarize_process_activity_marks_stale_when_heartbeat_is_old(self) -> None:
|
||||
state = summarize_process_activity(
|
||||
"youtube",
|
||||
"online",
|
||||
"监测 1 | 格式 YOUTUBE",
|
||||
"",
|
||||
log_age_seconds=360.0,
|
||||
ffmpeg_pids=[],
|
||||
)
|
||||
self.assertEqual(state["business_status"], "stale")
|
||||
|
||||
def test_summarize_process_activity_marks_configured_process_as_ready(self) -> None:
|
||||
state = summarize_process_activity(
|
||||
"youtube",
|
||||
"configured",
|
||||
"",
|
||||
"",
|
||||
log_age_seconds=None,
|
||||
ffmpeg_pids=[],
|
||||
)
|
||||
self.assertEqual(state["business_status"], "ready")
|
||||
self.assertEqual(state["business_note"], "Process is configured but not running")
|
||||
|
||||
def test_coerce_declared_process_status_marks_local_missing_process_as_configured(self) -> None:
|
||||
self.assertEqual(coerce_declared_process_status("local", "not_found", True), "configured")
|
||||
self.assertEqual(coerce_declared_process_status("pm2", "not_found", True), "not_found")
|
||||
self.assertEqual(coerce_declared_process_status("local", "online", True), "online")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
23
d2ypp2/tests/test_runtime_bootstrap.py
Normal file
23
d2ypp2/tests/test_runtime_bootstrap.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from src.runtime_bootstrap import compute_nofile_target, load_msg_push_exports
|
||||
|
||||
|
||||
class RuntimeBootstrapTests(unittest.TestCase):
|
||||
def test_compute_nofile_target_respects_minimum_and_hard_limit(self) -> None:
|
||||
self.assertEqual(compute_nofile_target(1024, 524288), 65536)
|
||||
self.assertEqual(compute_nofile_target(131072, 65535), 65535)
|
||||
|
||||
def test_load_msg_push_exports_falls_back_to_noop_when_backup_missing(self) -> None:
|
||||
with patch("builtins.__import__", side_effect=ModuleNotFoundError("backup")):
|
||||
funcs = load_msg_push_exports()
|
||||
self.assertEqual(len(funcs), 7)
|
||||
result = funcs[0]("demo")
|
||||
self.assertTrue(result["disabled"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
102
d2ypp2/tests/test_web_dynamic_processes.py
Normal file
102
d2ypp2/tests/test_web_dynamic_processes.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import asyncio
|
||||
import json
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import web
|
||||
|
||||
|
||||
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"}]
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
entries = web.process_monitor_entries()
|
||||
|
||||
pm2_names = {row["pm2"] for row in entries}
|
||||
self.assertIn("youtube2__lane1", pm2_names)
|
||||
|
||||
def test_process_monitor_entries_default_pm2_name_when_missing(self) -> None:
|
||||
channels = [{"id": "lane2", "name": "Lane 2"}]
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
entries = web.process_monitor_entries()
|
||||
|
||||
pm2_names = {row["pm2"] for row in entries}
|
||||
self.assertIn("youtube2__lane2", pm2_names)
|
||||
|
||||
def test_script_for_pm2_or_pro_accepts_custom_saved_pm2_name(self) -> None:
|
||||
channels = [{"id": "lane3", "name": "Lane 3", "pm2Name": "custom-lane-three"}]
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
resolved = web.script_for_pm2_or_pro("custom-lane-three")
|
||||
|
||||
self.assertEqual(resolved, "youtube.py")
|
||||
|
||||
def test_process_monitor_entries_overlay_static_youtube_label_for_lane_one(self) -> None:
|
||||
channels = [{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube"}]
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
entries = web.process_monitor_entries()
|
||||
|
||||
youtube_row = next(row for row in entries if row["pm2"] == "youtube")
|
||||
self.assertEqual(youtube_row["label"], "YouTube Pro \u00b7 Lane 1")
|
||||
|
||||
def test_process_monitor_entries_do_not_duplicate_static_pm2_rows(self) -> None:
|
||||
channels = [
|
||||
{"id": "lane1", "name": "Lane 1", "pm2Name": "youtube"},
|
||||
{"id": "lane2", "name": "Lane 2", "pm2Name": "youtube2__lane2"},
|
||||
]
|
||||
with patch("web.get_youtube_pro_channels", return_value=channels):
|
||||
entries = web.process_monitor_entries()
|
||||
|
||||
self.assertEqual(sum(1 for row in entries if row["pm2"] == "youtube"), 1)
|
||||
self.assertEqual(sum(1 for row in entries if row["pm2"] == "youtube2__lane2"), 1)
|
||||
|
||||
def test_web_module_does_not_repeat_process_entry_function_defs(self) -> None:
|
||||
tree = ast.parse((web.BASE_DIR / "web.py").read_text(encoding="utf-8"))
|
||||
defs: dict[str, list[int]] = {}
|
||||
for node in tree.body:
|
||||
if isinstance(node, ast.FunctionDef):
|
||||
defs.setdefault(node.name, []).append(node.lineno)
|
||||
|
||||
self.assertEqual(len(defs.get("youtube_pro_process_entries", [])), 1)
|
||||
self.assertEqual(len(defs.get("process_monitor_entries", [])), 1)
|
||||
|
||||
def test_status_marks_local_saved_process_as_configured_when_not_running(self) -> None:
|
||||
original_use_pm2 = web.process_backend._use_pm2
|
||||
web.process_backend._use_pm2 = False
|
||||
try:
|
||||
with patch.object(web.process_backend, "full_status_raw", AsyncMock(return_value="local table")):
|
||||
with patch("web.get_process_info", AsyncMock(return_value={"process_status": "not_found", "out_path": None, "err_path": None})):
|
||||
with patch("web.read_log_path_normalized", AsyncMock(return_value="")):
|
||||
with patch.object(web.process_backend, "live_ffmpeg_pids", AsyncMock(return_value=[])):
|
||||
response = asyncio.run(web.status("youtube"))
|
||||
finally:
|
||||
web.process_backend._use_pm2 = original_use_pm2
|
||||
|
||||
payload = json.loads(response.body)
|
||||
self.assertEqual(payload["process_status"], "configured")
|
||||
self.assertEqual(payload["business_status"], "ready")
|
||||
self.assertIn("已在控制台配置", payload["recent_log"])
|
||||
self.assertTrue(payload["raw_status"].startswith("进程「youtube」已在控制台配置"))
|
||||
|
||||
def test_process_monitor_includes_runtime_snapshot_for_local_configured_process(self) -> None:
|
||||
original_use_pm2 = web.process_backend._use_pm2
|
||||
web.process_backend._use_pm2 = False
|
||||
try:
|
||||
with patch.object(web.process_backend, "ensure_mode", AsyncMock()):
|
||||
with patch("web.get_process_info", AsyncMock(return_value={"process_status": "not_found", "out_path": None, "err_path": None})):
|
||||
with patch("web.read_log_path_normalized", AsyncMock(return_value="")):
|
||||
with patch.object(web.process_backend, "live_ffmpeg_pids", AsyncMock(return_value=[])):
|
||||
payload = asyncio.run(web.process_monitor())
|
||||
finally:
|
||||
web.process_backend._use_pm2 = original_use_pm2
|
||||
|
||||
youtube_row = next(row for row in payload["entries"] if row["pm2"] == "youtube")
|
||||
self.assertEqual(youtube_row["process_status"], "configured")
|
||||
self.assertEqual(youtube_row["business_status"], "ready")
|
||||
self.assertFalse(youtube_row["is_pushing"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
109
d2ypp2/tests/test_web_process_backend.py
Normal file
109
d2ypp2/tests/test_web_process_backend.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from src.web_process_backend import WebProcessBackend, build_pm2_start_command
|
||||
|
||||
|
||||
class WebProcessBackendTests(unittest.TestCase):
|
||||
def test_build_pm2_start_command_for_python_script_uses_repo_context(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
script = base_dir / "youtube.py"
|
||||
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")
|
||||
script.write_text("print('ok')\n", encoding="utf-8")
|
||||
|
||||
cmd = build_pm2_start_command(base_dir, "youtube2__lane1", "youtube.py")
|
||||
|
||||
self.assertIsNotNone(cmd)
|
||||
assert cmd is not None
|
||||
self.assertIn("LIVE_PM2_PROCESS_NAME=youtube2__lane1", cmd)
|
||||
self.assertIn("--name youtube2__lane1", cmd)
|
||||
self.assertIn("--cwd", cmd)
|
||||
self.assertIn(str(script), cmd)
|
||||
self.assertIn(str(venv_python), cmd)
|
||||
|
||||
def test_parse_pm2_describe_marks_unknown_unregistered_process_as_not_found(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
backend = WebProcessBackend(base_dir, base_dir / ".pm2" / "logs")
|
||||
backend._use_pm2 = True
|
||||
backend.ensure_mode = AsyncMock()
|
||||
backend._pm2_registered_names = AsyncMock(return_value=set())
|
||||
warn = "[PM2][WARN] Current process list is not synchronized with saved list. App youtube differs."
|
||||
with patch("src.web_process_backend.run_pm2", new=AsyncMock(return_value=warn)):
|
||||
info = asyncio.run(backend.parse_pm2_describe("youtube"))
|
||||
self.assertEqual(info["process_status"], "not_found")
|
||||
self.assertIsNone(info["out_path"])
|
||||
self.assertIsNone(info["err_path"])
|
||||
|
||||
def test_parse_pm2_describe_supports_spaced_log_path_keys(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
backend = WebProcessBackend(base_dir, base_dir / ".pm2" / "logs")
|
||||
backend._use_pm2 = True
|
||||
backend.ensure_mode = AsyncMock()
|
||||
backend._pm2_registered_names = AsyncMock(return_value={"youtube"})
|
||||
describe = "\n".join(
|
||||
[
|
||||
"│ status │ online │",
|
||||
"│ out log path │ /home/live/.pm2/logs/youtube-out.log │",
|
||||
"│ error log path │ /home/live/.pm2/logs/youtube-error.log │",
|
||||
]
|
||||
)
|
||||
with patch("src.web_process_backend.run_pm2", new=AsyncMock(return_value=describe)):
|
||||
info = asyncio.run(backend.parse_pm2_describe("youtube"))
|
||||
self.assertEqual(info["process_status"], "online")
|
||||
self.assertEqual(info["out_path"], "/home/live/.pm2/logs/youtube-out.log")
|
||||
self.assertEqual(info["err_path"], "/home/live/.pm2/logs/youtube-error.log")
|
||||
|
||||
def test_ensure_mode_resurrects_pm2_when_dump_exists_and_jlist_is_empty(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
pm2_home = base_dir / ".pm2-home"
|
||||
pm2_home.mkdir(parents=True, exist_ok=True)
|
||||
(pm2_home / "pm2.pid").write_text("1234\n", encoding="utf-8")
|
||||
(pm2_home / "dump.pm2").write_text('[{"name":"youtube"}]\n', encoding="utf-8")
|
||||
|
||||
backend = WebProcessBackend(base_dir, base_dir / ".pm2" / "logs")
|
||||
backend._use_pm2 = True
|
||||
|
||||
shell_out = AsyncMock(return_value=(0, "[]"))
|
||||
resurrect = AsyncMock(return_value="ok")
|
||||
with patch.dict("os.environ", {"PM2_HOME": str(pm2_home)}, clear=False):
|
||||
with patch("src.web_process_backend.resolve_pm2_bin", return_value="/usr/bin/pm2"):
|
||||
with patch("src.web_process_backend._shell_out", new=shell_out):
|
||||
with patch("src.web_process_backend.run_pm2", new=resurrect):
|
||||
asyncio.run(backend.ensure_mode())
|
||||
|
||||
resurrect.assert_awaited_once_with("resurrect", timeout=45.0)
|
||||
|
||||
def test_ensure_mode_skips_resurrect_without_saved_dump(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
base_dir = Path(tmp)
|
||||
pm2_home = base_dir / ".pm2-home"
|
||||
pm2_home.mkdir(parents=True, exist_ok=True)
|
||||
(pm2_home / "pm2.pid").write_text("1234\n", encoding="utf-8")
|
||||
|
||||
backend = WebProcessBackend(base_dir, base_dir / ".pm2" / "logs")
|
||||
backend._use_pm2 = True
|
||||
|
||||
shell_out = AsyncMock(return_value=(0, "[]"))
|
||||
resurrect = AsyncMock(return_value="ok")
|
||||
with patch.dict("os.environ", {"PM2_HOME": str(pm2_home)}, clear=False):
|
||||
with patch("src.web_process_backend.resolve_pm2_bin", return_value="/usr/bin/pm2"):
|
||||
with patch("src.web_process_backend._shell_out", new=shell_out):
|
||||
with patch("src.web_process_backend.run_pm2", new=resurrect):
|
||||
asyncio.run(backend.ensure_mode())
|
||||
|
||||
resurrect.assert_not_awaited()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
81
d2ypp2/tests/test_youtube_pro_runtime.py
Normal file
81
d2ypp2/tests/test_youtube_pro_runtime.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from src.youtube_pro_runtime import (
|
||||
ensure_youtube_pro_channel_runtime_files,
|
||||
managed_url_config_relpath_for_pm2,
|
||||
managed_youtube_ini_relpath_for_pm2,
|
||||
normalize_youtube_pro_channels,
|
||||
)
|
||||
|
||||
|
||||
class YoutubeProRuntimeTests(unittest.TestCase):
|
||||
def test_normalize_channels_assigns_default_pm2_and_rejects_duplicate_names(self) -> None:
|
||||
rows = normalize_youtube_pro_channels([{"id": "lane2", "name": "Lane 2"}])
|
||||
self.assertEqual(rows[0]["pm2Name"], "youtube2__lane2")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
normalize_youtube_pro_channels(
|
||||
[
|
||||
{"id": "lane1", "pm2Name": "custom"},
|
||||
{"id": "lane2", "pm2Name": "custom"},
|
||||
]
|
||||
)
|
||||
|
||||
def test_materialize_runtime_files_from_saved_channel_payload(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config_dir = Path(tmp)
|
||||
handled, errors = ensure_youtube_pro_channel_runtime_files(
|
||||
config_dir,
|
||||
"youtube2__lane2",
|
||||
[
|
||||
{
|
||||
"id": "lane2",
|
||||
"name": "Lane 2",
|
||||
"pm2Name": "youtube2__lane2",
|
||||
"streamKey": "abcd-efgh-ijkl-mnop",
|
||||
"urlLines": "https://live.douyin.com/123456789\n",
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(errors, [])
|
||||
youtube_ini = (config_dir / managed_youtube_ini_relpath_for_pm2("youtube2__lane2")).read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
url_ini = (config_dir / managed_url_config_relpath_for_pm2("youtube2__lane2")).read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("key = abcd-efgh-ijkl-mnop", youtube_ini)
|
||||
self.assertIn("https://live.douyin.com/123456789", url_ini)
|
||||
|
||||
def test_materialize_runtime_files_uses_existing_seed_when_channel_fields_blank(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
config_dir = Path(tmp)
|
||||
(config_dir / "youtube.ini").write_text("[youtube]\nkey = legacy-key\nrtmps = 是\n", encoding="utf-8")
|
||||
(config_dir / "URL_config.ini").write_text("https://live.douyin.com/88888888\n", encoding="utf-8")
|
||||
|
||||
handled, errors = ensure_youtube_pro_channel_runtime_files(
|
||||
config_dir,
|
||||
"youtube2__lane3",
|
||||
[{"id": "lane3", "pm2Name": "youtube2__lane3"}],
|
||||
)
|
||||
|
||||
self.assertTrue(handled)
|
||||
self.assertEqual(errors, [])
|
||||
youtube_ini = (config_dir / managed_youtube_ini_relpath_for_pm2("youtube2__lane3")).read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
url_ini = (config_dir / managed_url_config_relpath_for_pm2("youtube2__lane3")).read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
self.assertIn("legacy-key", youtube_ini)
|
||||
self.assertIn("https://live.douyin.com/88888888", url_ini)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
96
d2ypp2/tests/test_youtube_push_config.py
Normal file
96
d2ypp2/tests/test_youtube_push_config.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from douyin_youtube_ffplay import _load_youtube_push_settings, _resolve_youtube_push_config_file
|
||||
from src.youtube_ini_sync import write_youtube_ini_stream_key
|
||||
|
||||
|
||||
class YoutubePushConfigTests(unittest.TestCase):
|
||||
def test_resolve_youtube_push_config_prefers_managed_pm2_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
cfg_dir = root / "config"
|
||||
cfg_dir.mkdir(parents=True, exist_ok=True)
|
||||
(cfg_dir / "youtube.ini").write_text("[youtube]\nkey = legacy\n", encoding="utf-8")
|
||||
managed = cfg_dir / "youtube.youtube.ini"
|
||||
managed.write_text("[youtube]\nkey = managed\n", encoding="utf-8")
|
||||
|
||||
with patch.dict("os.environ", {"LIVE_PM2_PROCESS_NAME": "youtube"}, clear=False):
|
||||
resolved = _resolve_youtube_push_config_file(base_dir=str(root))
|
||||
|
||||
self.assertEqual(resolved, str(managed))
|
||||
|
||||
def test_resolve_youtube_push_config_falls_back_to_legacy_file(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
cfg_dir = root / "config"
|
||||
cfg_dir.mkdir(parents=True, exist_ok=True)
|
||||
legacy = cfg_dir / "youtube.ini"
|
||||
legacy.write_text("[youtube]\nkey = legacy\n", encoding="utf-8")
|
||||
|
||||
with patch.dict("os.environ", {"LIVE_PM2_PROCESS_NAME": "youtube"}, clear=False):
|
||||
resolved = _resolve_youtube_push_config_file(base_dir=str(root))
|
||||
|
||||
self.assertEqual(resolved, str(legacy))
|
||||
|
||||
def test_write_youtube_ini_stream_key_updates_default_managed_file_when_present(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
cfg_dir = root / "config"
|
||||
cfg_dir.mkdir(parents=True, exist_ok=True)
|
||||
(cfg_dir / "youtube.ini").write_text("[youtube]\nkey = old\n", encoding="utf-8")
|
||||
(cfg_dir / "youtube.youtube.ini").write_text("[youtube]\nkey = old2\n", encoding="utf-8")
|
||||
|
||||
writes: list[tuple[str, str, str]] = []
|
||||
|
||||
def fake_save(root_id: str, relative_path: str, content: str) -> dict[str, str]:
|
||||
writes.append((root_id, relative_path, content))
|
||||
return {"root_id": root_id, "path": relative_path, "message": "Saved successfully"}
|
||||
|
||||
with patch("src.youtube_ini_sync.save_managed_file", side_effect=fake_save):
|
||||
msg = write_youtube_ini_stream_key(root, "demo-key")
|
||||
|
||||
self.assertIn("youtube.ini", msg)
|
||||
self.assertIn("youtube.youtube.ini", msg)
|
||||
self.assertEqual([w[1] for w in writes], ["youtube.ini", "youtube.youtube.ini"])
|
||||
self.assertTrue(all("demo-key" in w[2] for w in writes))
|
||||
|
||||
def test_load_youtube_push_settings_accepts_bom_prefixed_ini(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
cfg = Path(tmp) / "youtube.youtube.ini"
|
||||
cfg.write_text("\ufeff[youtube]\nkey = bom-key\nbitrate = 5500\nfast_audio = 1\n", encoding="utf-8")
|
||||
|
||||
parsed = _load_youtube_push_settings(str(cfg))
|
||||
|
||||
self.assertEqual(parsed["stream_key"], "bom-key")
|
||||
self.assertEqual(parsed["b_v"], "5500k")
|
||||
self.assertEqual(parsed["maxrate"], "6600k")
|
||||
self.assertEqual(parsed["bufsize"], "11000k")
|
||||
self.assertTrue(parsed["fast_audio"])
|
||||
|
||||
def test_write_youtube_ini_stream_key_reads_existing_bom_prefixed_ini(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
cfg_dir = root / "config"
|
||||
cfg_dir.mkdir(parents=True, exist_ok=True)
|
||||
(cfg_dir / "youtube.youtube.ini").write_text("\ufeff[youtube]\nkey = old\n", encoding="utf-8")
|
||||
|
||||
writes: list[tuple[str, str, str]] = []
|
||||
|
||||
def fake_save(root_id: str, relative_path: str, content: str) -> dict[str, str]:
|
||||
writes.append((root_id, relative_path, content))
|
||||
return {"root_id": root_id, "path": relative_path, "message": "Saved successfully"}
|
||||
|
||||
with patch("src.youtube_ini_sync.save_managed_file", side_effect=fake_save):
|
||||
msg = write_youtube_ini_stream_key(root, "fresh-key")
|
||||
|
||||
self.assertIn("youtube.youtube.ini", msg)
|
||||
self.assertTrue(any(path == "youtube.youtube.ini" and "fresh-key" in content for _, path, content in writes))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
70
d2ypp2/tests/test_youtube_runtime.py
Normal file
70
d2ypp2/tests/test_youtube_runtime.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from src.youtube_runtime import normalize_youtube_video_save_type, resolve_live_video_encoder
|
||||
|
||||
|
||||
class YoutubeRuntimeTests(unittest.TestCase):
|
||||
def test_youtube_process_defaults_ts_setting_to_push_mode(self) -> None:
|
||||
self.assertEqual(normalize_youtube_video_save_type("ts", "youtube"), "YOUTUBE")
|
||||
self.assertEqual(normalize_youtube_video_save_type("", "youtube2__lane1"), "YOUTUBE")
|
||||
|
||||
def test_explicit_local_record_format_is_preserved(self) -> None:
|
||||
self.assertEqual(normalize_youtube_video_save_type("mp4", "youtube"), "MP4")
|
||||
self.assertEqual(normalize_youtube_video_save_type("mkv", "douyin_youtube"), "MKV")
|
||||
|
||||
def test_non_youtube_process_falls_back_to_ts(self) -> None:
|
||||
self.assertEqual(normalize_youtube_video_save_type("ts", "tiktok"), "TS")
|
||||
self.assertEqual(normalize_youtube_video_save_type("unknown", "tiktok"), "TS")
|
||||
|
||||
def test_arm_falls_back_from_vaapi_to_v4l2m2m(self) -> None:
|
||||
enc, note = resolve_live_video_encoder(
|
||||
"h264_vaapi",
|
||||
"h264_v4l2m2m\nh264_vaapi\n",
|
||||
"aarch64",
|
||||
has_mpp_service=False,
|
||||
has_render_node=True,
|
||||
has_v4l2_device=True,
|
||||
)
|
||||
self.assertEqual(enc, "h264_v4l2m2m")
|
||||
self.assertIn("fallback", note)
|
||||
|
||||
def test_arm_auto_selects_v4l2m2m_when_env_missing(self) -> None:
|
||||
enc, note = resolve_live_video_encoder(
|
||||
"",
|
||||
"h264_v4l2m2m\n",
|
||||
"aarch64",
|
||||
has_mpp_service=False,
|
||||
has_render_node=False,
|
||||
has_v4l2_device=True,
|
||||
)
|
||||
self.assertEqual(enc, "h264_v4l2m2m")
|
||||
self.assertEqual(note, "")
|
||||
|
||||
def test_x86_requires_render_node_for_vaapi(self) -> None:
|
||||
enc, note = resolve_live_video_encoder(
|
||||
"h264_vaapi",
|
||||
"h264_vaapi\n",
|
||||
"x86_64",
|
||||
has_mpp_service=False,
|
||||
has_render_node=False,
|
||||
)
|
||||
self.assertEqual(enc, "")
|
||||
self.assertIn("renderD", note)
|
||||
|
||||
def test_arm_rejects_v4l2m2m_without_video_nodes(self) -> None:
|
||||
enc, note = resolve_live_video_encoder(
|
||||
"h264_v4l2m2m",
|
||||
"h264_v4l2m2m\n",
|
||||
"aarch64",
|
||||
has_mpp_service=False,
|
||||
has_render_node=False,
|
||||
has_v4l2_device=False,
|
||||
)
|
||||
self.assertEqual(enc, "")
|
||||
self.assertIn("/dev/video", note)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user