mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 20:17:13 +08:00
869 lines
28 KiB
Python
869 lines
28 KiB
Python
"""PERF Docker harness 场景协议的无 Docker fake 测试。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import importlib.util
|
||
import json
|
||
import os
|
||
import sys
|
||
import threading
|
||
import time
|
||
from pathlib import Path
|
||
from types import ModuleType, SimpleNamespace
|
||
|
||
import pytest
|
||
|
||
|
||
PERF_DIR = Path(__file__).resolve().parent
|
||
|
||
|
||
def load_module(name: str, path: Path):
|
||
"""从脚本路径加载模块,避免要求 scripts 变成运行时 Python package。"""
|
||
spec = importlib.util.spec_from_file_location(name, path)
|
||
assert spec is not None and spec.loader is not None
|
||
module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(module)
|
||
return module
|
||
|
||
|
||
def snapshot(xvfb_count: int, xvfb_pss_kib: int = 0) -> dict:
|
||
"""构造只包含验收字段的进程快照。"""
|
||
return {
|
||
"processes": {
|
||
"xvfb": {"count": xvfb_count, "pss_kib": xvfb_pss_kib},
|
||
}
|
||
}
|
||
|
||
|
||
def agent_snapshot(prefix_counts: dict[str, int], xvfb_count: int = 0) -> dict:
|
||
"""构造包含 Agent 模块哨兵的场景边界快照。"""
|
||
return {
|
||
"engine": {
|
||
"working_set_bytes": 500 * 1024 * 1024,
|
||
"network_rx_bytes": 1024,
|
||
"network_tx_bytes": 512,
|
||
},
|
||
"processes": {
|
||
"main_python": {
|
||
"pss_kib": 400 * 1024,
|
||
"uss_kib": 390 * 1024,
|
||
"threads": 8,
|
||
},
|
||
"xvfb": {"count": xvfb_count, "pss_kib": 0},
|
||
},
|
||
"modules": {
|
||
"count": 3000,
|
||
"prefix_counts": prefix_counts,
|
||
},
|
||
}
|
||
|
||
|
||
def managed_resource(before_generation: int, after_generation: int) -> dict:
|
||
"""构造 host.display single-flight 观测。"""
|
||
observations = []
|
||
if after_generation > before_generation:
|
||
observations = [
|
||
{
|
||
"operation": "activate",
|
||
"outcome": "started",
|
||
"generation": after_generation,
|
||
"reason": "headed_browser_launch",
|
||
},
|
||
{
|
||
"operation": "activate",
|
||
"outcome": "succeeded",
|
||
"generation": after_generation,
|
||
},
|
||
]
|
||
return {
|
||
"before": {
|
||
"available": True,
|
||
"snapshot": {"generation": before_generation},
|
||
"observations": [],
|
||
},
|
||
"after": {
|
||
"available": True,
|
||
"snapshot": {"generation": after_generation},
|
||
"observations": observations,
|
||
},
|
||
}
|
||
|
||
|
||
class FakePage:
|
||
"""验证本地 data URL 的同步页面替身。"""
|
||
|
||
def __init__(self) -> None:
|
||
self.url = ""
|
||
|
||
def goto(self, url: str) -> None:
|
||
self.url = url
|
||
|
||
def title(self) -> str:
|
||
assert self.url.startswith("data:text/html,")
|
||
return "MoviePilot Browser Probe"
|
||
|
||
def close(self) -> None:
|
||
"""模拟 Playwright page 对称关闭。"""
|
||
|
||
|
||
class FakeContext:
|
||
"""浏览器上下文替身。"""
|
||
|
||
def new_page(self) -> FakePage:
|
||
return FakePage()
|
||
|
||
def close(self) -> None:
|
||
"""模拟 CloakBrowser context 对称关闭。"""
|
||
|
||
|
||
def test_default_cli_and_paths_keep_idle_contract(tmp_path: Path) -> None:
|
||
"""未指定 scenario 时保持既有 idle 命令和资源路径。"""
|
||
harness = load_module("moviepilot_perf_cli", PERF_DIR / "moviepilot_docker_ab.py")
|
||
args = harness.build_parser().parse_args(
|
||
[
|
||
"--campaign",
|
||
"fake",
|
||
"--output-dir",
|
||
str(tmp_path),
|
||
"sample",
|
||
"--variant",
|
||
"after",
|
||
"--index",
|
||
"1",
|
||
]
|
||
)
|
||
|
||
assert args.scenario == "idle-default"
|
||
assert harness.sample_volume_names(args, "after", 1) == (
|
||
"mpperf-fake-after-1-config",
|
||
"mpperf-fake-after-1-browser",
|
||
)
|
||
assert harness.sample_result_directory(args, "after", 1) == (
|
||
tmp_path / "fake" / "samples" / "after-1"
|
||
)
|
||
|
||
|
||
def test_browser_scenario_uses_isolated_resource_and_result_names(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
"""不同激活场景不会覆盖 idle 样本或彼此复用可写卷。"""
|
||
harness = load_module("moviepilot_perf_paths", PERF_DIR / "moviepilot_docker_ab.py")
|
||
args = argparse.Namespace(
|
||
campaign="fake",
|
||
output_dir=tmp_path,
|
||
scenario="browser-headed",
|
||
)
|
||
|
||
assert harness.sample_volume_names(args, "after", 2) == (
|
||
"mpperf-fake-browser-headed-after-2-config",
|
||
"mpperf-fake-browser-headed-after-2-browser",
|
||
)
|
||
assert harness.sample_result_directory(args, "after", 2) == (
|
||
tmp_path / "fake" / "samples" / "browser-headed" / "after-2"
|
||
)
|
||
|
||
|
||
def test_agent_scenarios_are_explicit_and_keep_idle_prefix_contract() -> None:
|
||
"""PERF-003 暴露完整哨兵,并把 Schema 基线排除在归零门禁外。"""
|
||
harness = load_module(
|
||
"moviepilot_perf_agent_cli",
|
||
PERF_DIR / "moviepilot_docker_ab.py",
|
||
)
|
||
expected_prefixes = {
|
||
"app.agent.orchestrator",
|
||
"app.agent.callback",
|
||
"app.agent.llm.helper",
|
||
"app.agent.tools.base",
|
||
"app.agent.tools.catalog",
|
||
"app.agent.tools.factory",
|
||
"app.agent.tools.impl",
|
||
"langgraph",
|
||
"langchain",
|
||
"langchain_core",
|
||
"openai",
|
||
"anthropic",
|
||
"google.genai",
|
||
"boto3",
|
||
"botocore",
|
||
}
|
||
|
||
assert set(harness.AGENT_SCENARIOS) == {
|
||
"agent-disabled-router",
|
||
"agent-tool-catalog",
|
||
}
|
||
assert set(harness.AGENT_HEAVY_MODULE_PREFIXES) == expected_prefixes
|
||
assert expected_prefixes.issubset(harness.MODULE_PREFIXES)
|
||
assert set(harness.AGENT_SCHEMA_BASELINE_PREFIXES) == {
|
||
"langchain",
|
||
"langchain_core",
|
||
}
|
||
assert not set(harness.AGENT_SCHEMA_BASELINE_PREFIXES).intersection(
|
||
harness.AGENT_NONMATERIALIZATION_PREFIXES
|
||
)
|
||
|
||
|
||
@pytest.mark.parametrize("scenario", ["browser-headless", "agent-disabled-router"])
|
||
def test_non_default_scenario_rejects_before_without_touching_docker(
|
||
scenario: str,
|
||
) -> None:
|
||
"""旧基线不具备候选公共合同,所有非默认场景只接受 After。"""
|
||
harness = load_module(
|
||
"moviepilot_perf_after_only", PERF_DIR / "moviepilot_docker_ab.py"
|
||
)
|
||
args = argparse.Namespace(scenario=scenario, variant="before")
|
||
|
||
with pytest.raises(harness.HarnessError, match="After"):
|
||
harness.command_sample(args)
|
||
|
||
|
||
def test_activation_validation_enforces_headless_and_headed_invariants() -> None:
|
||
"""headless 保持无 Xvfb,headed 两调用只能产生一个 Xvfb。"""
|
||
harness = load_module(
|
||
"moviepilot_perf_validation",
|
||
PERF_DIR / "moviepilot_docker_ab.py",
|
||
)
|
||
headless_marker = {
|
||
"scenario": "browser-headless",
|
||
"pid": 42,
|
||
"success": True,
|
||
"browser": {
|
||
"success": True,
|
||
"concurrency": 1,
|
||
"successes": 1,
|
||
"retained_contexts": 1,
|
||
"managed_resource": managed_resource(0, 0),
|
||
"single_flight_probe": {
|
||
"concurrent_callers": 0,
|
||
"successful_callers": 0,
|
||
},
|
||
},
|
||
}
|
||
headed_marker = {
|
||
"scenario": "browser-headed",
|
||
"pid": 42,
|
||
"success": True,
|
||
"browser": {
|
||
"success": True,
|
||
"concurrency": 2,
|
||
"successes": 2,
|
||
"retained_contexts": 1,
|
||
"managed_resource": managed_resource(0, 1),
|
||
"single_flight_probe": {
|
||
"concurrent_callers": 2,
|
||
"successful_callers": 2,
|
||
},
|
||
},
|
||
}
|
||
|
||
headless = harness.evaluate_browser_activation(
|
||
"browser-headless",
|
||
snapshot(0),
|
||
snapshot(0),
|
||
headless_marker,
|
||
expected_pid=42,
|
||
)
|
||
headed = harness.evaluate_browser_activation(
|
||
"browser-headed",
|
||
snapshot(0),
|
||
snapshot(1, 72 * 1024),
|
||
headed_marker,
|
||
expected_pid=42,
|
||
)
|
||
invalid = harness.evaluate_browser_activation(
|
||
"browser-headed",
|
||
snapshot(0),
|
||
snapshot(2, 144 * 1024),
|
||
headed_marker,
|
||
expected_pid=42,
|
||
)
|
||
|
||
assert headless["passed"] is True
|
||
assert headed["passed"] is True
|
||
assert headed["single_flight"]["passed"] is True
|
||
assert headed["single_flight"]["generation_after"] == 1
|
||
assert headed["single_flight"]["activation_start_count"] == 1
|
||
assert invalid["passed"] is False
|
||
assert invalid["single_flight"]["passed"] is False
|
||
|
||
|
||
def test_agent_activation_validation_enforces_lazy_boundaries() -> None:
|
||
"""禁用态延迟重 Agent 域,首次目录只允许工具域物化。"""
|
||
harness = load_module(
|
||
"moviepilot_perf_agent_validation",
|
||
PERF_DIR / "moviepilot_docker_ab.py",
|
||
)
|
||
zero = {prefix: 0 for prefix in harness.AGENT_HEAVY_MODULE_PREFIXES}
|
||
catalog_loaded = dict(zero)
|
||
catalog_loaded["app.agent.tools.base"] = 1
|
||
catalog_loaded["app.agent.tools.catalog"] = 1
|
||
catalog_loaded["app.agent.tools.factory"] = 1
|
||
catalog_loaded["app.agent.tools.impl"] = 82
|
||
schema_baseline = dict(zero)
|
||
schema_baseline["langchain_core"] = 5
|
||
router_marker = {
|
||
"scenario": "agent-disabled-router",
|
||
"pid": 42,
|
||
"success": True,
|
||
"agent": {
|
||
"success": True,
|
||
"observations": {
|
||
"before": {
|
||
"available": True,
|
||
"tool_factory_materialized": False,
|
||
},
|
||
"after": {
|
||
"available": True,
|
||
"tool_factory_materialized": False,
|
||
},
|
||
},
|
||
"router_openapi": {
|
||
"success": True,
|
||
"ai_agent_enable": False,
|
||
"missing_routes": [],
|
||
"missing_openapi_paths": [],
|
||
"route_count": 200,
|
||
"openapi_path_count": 180,
|
||
"openapi_sha256": "schema",
|
||
},
|
||
},
|
||
}
|
||
catalog_marker = {
|
||
"scenario": "agent-tool-catalog",
|
||
"pid": 42,
|
||
"success": True,
|
||
"agent": {
|
||
"success": True,
|
||
"observations": {
|
||
"before": {
|
||
"available": True,
|
||
"tool_factory_materialized": False,
|
||
},
|
||
"after": {
|
||
"available": True,
|
||
"tool_factory_materialized": True,
|
||
},
|
||
},
|
||
"tool_catalog": {
|
||
"success": True,
|
||
"tool_count": 82,
|
||
"schema_count": 82,
|
||
"catalog_entry_count": 82,
|
||
"collision_names": [],
|
||
"plugin_revision": 0,
|
||
"factory_revision": "factory-revision",
|
||
"schemas_sha256": "schemas",
|
||
"schema_digests_complete": True,
|
||
"repeat_tool_count": 82,
|
||
"repeat_stable": True,
|
||
},
|
||
},
|
||
}
|
||
|
||
router = harness.evaluate_agent_activation(
|
||
"agent-disabled-router",
|
||
agent_snapshot(schema_baseline),
|
||
agent_snapshot(schema_baseline),
|
||
router_marker,
|
||
expected_pid=42,
|
||
)
|
||
catalog = harness.evaluate_agent_activation(
|
||
"agent-tool-catalog",
|
||
agent_snapshot(zero),
|
||
agent_snapshot(catalog_loaded),
|
||
catalog_marker,
|
||
expected_pid=42,
|
||
)
|
||
invalid_loaded = dict(catalog_loaded)
|
||
invalid_loaded["app.agent.orchestrator"] = 1
|
||
invalid = harness.evaluate_agent_activation(
|
||
"agent-tool-catalog",
|
||
agent_snapshot(zero),
|
||
agent_snapshot(invalid_loaded),
|
||
catalog_marker,
|
||
expected_pid=42,
|
||
)
|
||
callback_loaded = dict(catalog_loaded)
|
||
callback_loaded["app.agent.callback"] = 1
|
||
invalid_callback = harness.evaluate_agent_activation(
|
||
"agent-tool-catalog",
|
||
agent_snapshot(zero),
|
||
agent_snapshot(callback_loaded),
|
||
catalog_marker,
|
||
expected_pid=42,
|
||
)
|
||
network_post = agent_snapshot(catalog_loaded)
|
||
network_post["engine"]["network_tx_bytes"] += 1
|
||
invalid_network = harness.evaluate_agent_activation(
|
||
"agent-tool-catalog",
|
||
agent_snapshot(zero),
|
||
network_post,
|
||
catalog_marker,
|
||
expected_pid=42,
|
||
)
|
||
|
||
assert router["passed"] is True
|
||
assert router["action"]["openapi_path_count"] == 180
|
||
assert catalog["passed"] is True
|
||
assert catalog["revision"]["factory"] == "factory-revision"
|
||
assert invalid["passed"] is False
|
||
assert any("非目录" in error for error in invalid["errors"])
|
||
assert invalid_callback["passed"] is False
|
||
assert invalid_network["passed"] is False
|
||
assert any("网络" in error for error in invalid_network["errors"])
|
||
|
||
|
||
def test_sitecustomize_acquires_headed_display_concurrently_in_same_process() -> None:
|
||
"""headed probe 并发走公开 SDK 冷启动,并只保留一个上下文。"""
|
||
probe = load_module(
|
||
"moviepilot_perf_sitecustomize",
|
||
PERF_DIR / "instrument" / "sitecustomize.py",
|
||
)
|
||
browser_calls: list[tuple[int, bool]] = []
|
||
closed_contexts: list[tuple[int, int]] = []
|
||
lock = threading.Lock()
|
||
|
||
class TrackedContext(FakeContext):
|
||
"""记录并发探针关闭的额外浏览器上下文。"""
|
||
|
||
def __init__(self, index: int) -> None:
|
||
self.index = index
|
||
|
||
def close(self) -> None:
|
||
closed_contexts.append((self.index, threading.get_ident()))
|
||
|
||
def launcher(*, headless: bool) -> FakeContext:
|
||
with lock:
|
||
browser_calls.append((threading.get_ident(), headless))
|
||
index = len(browser_calls) - 1
|
||
return TrackedContext(index)
|
||
|
||
result = probe._activate_browser_scenario(
|
||
"browser-headed",
|
||
launcher=launcher,
|
||
)
|
||
|
||
assert result["success"] is True
|
||
assert result["successes"] == 2
|
||
assert result["retained_contexts"] == 1
|
||
assert len(browser_calls) == 2
|
||
assert len({thread_id for thread_id, _headless in browser_calls}) == 2
|
||
assert all(headless is False for _thread_id, headless in browser_calls)
|
||
assert len(closed_contexts) == 1
|
||
closed_index, closed_thread_id = closed_contexts[0]
|
||
assert closed_thread_id == browser_calls[closed_index][0]
|
||
assert result["single_flight_probe"]["barrier_used"] is True
|
||
|
||
|
||
def test_sitecustomize_headless_uses_one_headless_context() -> None:
|
||
"""headless probe 只启动一个无显示上下文。"""
|
||
probe = load_module(
|
||
"moviepilot_perf_sitecustomize_headless",
|
||
PERF_DIR / "instrument" / "sitecustomize.py",
|
||
)
|
||
calls: list[bool] = []
|
||
|
||
def launcher(*, headless: bool) -> FakeContext:
|
||
calls.append(headless)
|
||
return FakeContext()
|
||
|
||
result = probe._activate_browser_scenario("browser-headless", launcher=launcher)
|
||
|
||
assert result["success"] is True
|
||
assert calls == [True]
|
||
assert result["single_flight_probe"]["requested"] is False
|
||
|
||
|
||
def test_sitecustomize_router_probe_generates_complete_openapi_without_http() -> None:
|
||
"""禁用态探针直接读取主进程 app,不发起 HTTP 或外部请求。"""
|
||
probe = load_module(
|
||
"moviepilot_perf_sitecustomize_router",
|
||
PERF_DIR / "instrument" / "sitecustomize.py",
|
||
)
|
||
required_paths = [
|
||
"/api/v1/message/agent/stream",
|
||
"/api/v1/message/agent/sessions",
|
||
"/api/v1/openai/v1/chat/completions",
|
||
"/api/v1/openai/v1/responses",
|
||
"/api/v1/anthropic/v1/messages",
|
||
"/api/v1/llm/manage",
|
||
"/api/v1/mcp",
|
||
"/api/v1/mcp/tools",
|
||
]
|
||
|
||
class FakeApp:
|
||
"""只实现 Router/OpenAPI 探针使用的 FastAPI 合同。"""
|
||
|
||
routes = [SimpleNamespace(path=path) for path in required_paths]
|
||
|
||
@staticmethod
|
||
def openapi() -> dict:
|
||
return {
|
||
"info": {"title": "MoviePilot", "version": "v3"},
|
||
"paths": {path: {"get": {}} for path in required_paths},
|
||
}
|
||
|
||
result = probe._probe_router_openapi(
|
||
app_instance=FakeApp(),
|
||
settings_object=SimpleNamespace(AI_AGENT_ENABLE=False),
|
||
)
|
||
|
||
assert result["success"] is True
|
||
assert result["route_count"] == len(required_paths)
|
||
assert result["openapi_path_count"] == len(required_paths)
|
||
assert result["missing_routes"] == []
|
||
assert result["missing_openapi_paths"] == []
|
||
|
||
|
||
def test_sitecustomize_tool_catalog_probe_records_schema_and_revisions() -> None:
|
||
"""首次目录探针保留工具数、Schema 摘要和双 revision。"""
|
||
probe = load_module(
|
||
"moviepilot_perf_sitecustomize_catalog",
|
||
PERF_DIR / "instrument" / "sitecustomize.py",
|
||
)
|
||
definitions = [
|
||
SimpleNamespace(
|
||
name="query_media",
|
||
input_schema={"type": "object", "properties": {}},
|
||
),
|
||
SimpleNamespace(
|
||
name="add_subscribe",
|
||
input_schema={"type": "object", "properties": {"title": {}}},
|
||
),
|
||
]
|
||
catalog = SimpleNamespace(
|
||
entries=(
|
||
SimpleNamespace(
|
||
name="query_media",
|
||
source="builtin",
|
||
schema_digest="a" * 64,
|
||
),
|
||
SimpleNamespace(
|
||
name="add_subscribe",
|
||
source="builtin",
|
||
schema_digest="b" * 64,
|
||
),
|
||
),
|
||
collisions={},
|
||
plugin_revision=7,
|
||
factory_revision="factory-revision",
|
||
)
|
||
|
||
class FakeManager:
|
||
"""按真实管理器合同在 list_tools 后发布 catalog。"""
|
||
|
||
def __init__(self) -> None:
|
||
self.catalog = None
|
||
|
||
def list_tools(self):
|
||
self.catalog = catalog
|
||
return definitions
|
||
|
||
result = probe._probe_tool_catalog(manager=FakeManager())
|
||
|
||
assert result["success"] is True
|
||
assert result["tool_count"] == 2
|
||
assert result["schema_count"] == 2
|
||
assert result["plugin_revision"] == 7
|
||
assert result["factory_revision"] == "factory-revision"
|
||
assert len(result["schemas_sha256"]) == 64
|
||
assert len(result["catalog_sha256"]) == 64
|
||
assert result["source_counts"] == {"builtin": 2}
|
||
assert result["schema_digests_complete"] is True
|
||
assert result["repeat_catalog_same_object"] is True
|
||
assert result["repeat_revision_unchanged"] is True
|
||
assert result["repeat_stable"] is True
|
||
|
||
|
||
def test_sitecustomize_agent_scenario_records_before_and_after_observations(
|
||
monkeypatch,
|
||
) -> None:
|
||
"""Agent 场景在同一目标解释器内记录模块与 materialization 边界。"""
|
||
probe = load_module(
|
||
"moviepilot_perf_sitecustomize_agent",
|
||
PERF_DIR / "instrument" / "sitecustomize.py",
|
||
)
|
||
module_observations = iter(
|
||
[
|
||
{"total_modules": 100, "prefix_counts": {}, "matching_modules": []},
|
||
{
|
||
"total_modules": 190,
|
||
"prefix_counts": {
|
||
"app.agent.tools.factory": 1,
|
||
"app.agent.tools.impl": 82,
|
||
},
|
||
"matching_modules": ["app.agent.tools.factory"],
|
||
},
|
||
]
|
||
)
|
||
runtime_observations = iter(
|
||
[
|
||
{"available": True, "tool_factory_materialized": False},
|
||
{"available": True, "tool_factory_materialized": True},
|
||
]
|
||
)
|
||
monkeypatch.setattr(
|
||
probe,
|
||
"_agent_module_observation",
|
||
lambda: next(module_observations),
|
||
)
|
||
monkeypatch.setattr(
|
||
probe,
|
||
"_probe_tool_catalog",
|
||
lambda manager=None: {
|
||
"success": True,
|
||
"tool_count": 82,
|
||
"schema_count": 82,
|
||
},
|
||
)
|
||
|
||
result = probe._activate_agent_scenario(
|
||
"agent-tool-catalog",
|
||
runtime_reader=lambda: next(runtime_observations),
|
||
)
|
||
|
||
assert result["success"] is True
|
||
assert result["observations"]["before"]["tool_factory_materialized"] is False
|
||
assert result["observations"]["after"]["tool_factory_materialized"] is True
|
||
assert result["modules"]["before"]["total_modules"] == 100
|
||
assert result["modules"]["after"]["total_modules"] == 190
|
||
|
||
|
||
def test_sitecustomize_serializes_managed_resource_facade(monkeypatch) -> None:
|
||
"""进程探针按公开只读 facade 记录 generation 与 activate observation。"""
|
||
observation = SimpleNamespace(
|
||
capability_id="host.display",
|
||
generation=1,
|
||
operation="activate",
|
||
outcome="started",
|
||
reason="fake",
|
||
materialization="materialized",
|
||
lifecycle="starting",
|
||
duration_ms=0.5,
|
||
error=None,
|
||
)
|
||
runtime_snapshot = SimpleNamespace(
|
||
capability_id="host.display",
|
||
materialization="materialized",
|
||
lifecycle="running",
|
||
generation=1,
|
||
visible=True,
|
||
error=None,
|
||
)
|
||
|
||
facade = ModuleType("app.runtime.managed_resources")
|
||
|
||
def managed_resource_snapshot(capability_id: str):
|
||
assert capability_id == "host.display"
|
||
return runtime_snapshot
|
||
|
||
def managed_resource_observations(capability_id=None):
|
||
assert capability_id == "host.display"
|
||
return (observation,)
|
||
|
||
facade.managed_resource_snapshot = managed_resource_snapshot
|
||
facade.managed_resource_observations = managed_resource_observations
|
||
monkeypatch.setitem(sys.modules, "app.runtime.managed_resources", facade)
|
||
probe = load_module(
|
||
"moviepilot_perf_sitecustomize_observation",
|
||
PERF_DIR / "instrument" / "sitecustomize.py",
|
||
)
|
||
|
||
result = probe._read_display_runtime()
|
||
|
||
assert result["available"] is True
|
||
assert result["snapshot"]["generation"] == 1
|
||
assert result["observations"][0]["operation"] == "activate"
|
||
assert result["observations"][0]["outcome"] == "started"
|
||
|
||
|
||
def test_sitecustomize_signal_worker_publishes_atomic_marker(tmp_path: Path) -> None:
|
||
"""信号回调只调度目标进程工作线程,并发布带 PID 的完成 marker。"""
|
||
probe = load_module(
|
||
"moviepilot_perf_sitecustomize_marker",
|
||
PERF_DIR / "instrument" / "sitecustomize.py",
|
||
)
|
||
probe._OUTPUT_DIR = str(tmp_path)
|
||
probe._SCENARIO = "browser-headless"
|
||
probe._activation_started = False
|
||
probe._activate_browser_scenario = lambda scenario: {
|
||
"success": scenario == "browser-headless"
|
||
}
|
||
|
||
probe._request_activation(None, None)
|
||
marker_path = tmp_path / f"activation-{os.getpid()}.json"
|
||
deadline = time.monotonic() + 2
|
||
while time.monotonic() < deadline and not marker_path.exists():
|
||
time.sleep(0.01)
|
||
|
||
payload = json.loads(marker_path.read_text(encoding="utf-8"))
|
||
assert payload["pid"] == os.getpid()
|
||
assert payload["scenario"] == "browser-headless"
|
||
assert payload["success"] is True
|
||
assert not list(tmp_path.glob("*.tmp"))
|
||
|
||
|
||
def test_sitecustomize_signal_worker_dispatches_agent_scenario(tmp_path: Path) -> None:
|
||
"""SIGUSR2 worker 对 Agent 场景也在当前 PID 发布完整 marker。"""
|
||
probe = load_module(
|
||
"moviepilot_perf_sitecustomize_agent_marker",
|
||
PERF_DIR / "instrument" / "sitecustomize.py",
|
||
)
|
||
probe._OUTPUT_DIR = str(tmp_path)
|
||
probe._SCENARIO = "agent-disabled-router"
|
||
probe._activate_agent_scenario = lambda scenario: {
|
||
"success": scenario == "agent-disabled-router"
|
||
}
|
||
|
||
probe._run_activation()
|
||
|
||
marker_path = tmp_path / f"activation-{os.getpid()}.json"
|
||
payload = json.loads(marker_path.read_text(encoding="utf-8"))
|
||
assert payload["pid"] == os.getpid()
|
||
assert payload["scenario"] == "agent-disabled-router"
|
||
assert payload["agent"]["success"] is True
|
||
assert "browser" not in payload
|
||
|
||
|
||
def test_markdown_reports_activation_and_keeps_scenario_medians_separate() -> None:
|
||
"""非默认场景报告包含激活证据,并按场景隔离中位数。"""
|
||
harness = load_module(
|
||
"moviepilot_perf_report", PERF_DIR / "moviepilot_docker_ab.py"
|
||
)
|
||
process_data = {
|
||
"main_python": {"pss_kib": 400 * 1024, "uss_kib": 390 * 1024, "threads": 8},
|
||
"xvfb": {"count": 0, "pss_kib": 0},
|
||
}
|
||
post_process_data = {
|
||
"main_python": {"pss_kib": 410 * 1024, "uss_kib": 400 * 1024, "threads": 10},
|
||
"xvfb": {"count": 1, "pss_kib": 72 * 1024},
|
||
}
|
||
activation = {
|
||
"worker_elapsed_seconds": 1.25,
|
||
"pre": {
|
||
"engine": {
|
||
"working_set_bytes": 500 * 1024 * 1024,
|
||
"network_rx_bytes": 1024,
|
||
"network_tx_bytes": 512,
|
||
},
|
||
"processes": process_data,
|
||
},
|
||
"post": {
|
||
"engine": {
|
||
"working_set_bytes": 600 * 1024 * 1024,
|
||
"network_rx_bytes": 3072,
|
||
"network_tx_bytes": 1536,
|
||
},
|
||
"processes": post_process_data,
|
||
},
|
||
"marker": {"success": True},
|
||
"validation": {
|
||
"passed": True,
|
||
"single_flight": {"passed": True},
|
||
},
|
||
}
|
||
sample = {
|
||
"scenario": "browser-headed",
|
||
"variant": "after",
|
||
"sample_index": 1,
|
||
"http_ready_seconds": 7.0,
|
||
"activation": activation,
|
||
"measurements": [
|
||
{
|
||
"target_minute": 1.0,
|
||
"engine": {
|
||
"working_set_bytes": 610 * 1024 * 1024,
|
||
"network_rx_bytes": 1024,
|
||
"network_tx_bytes": 512,
|
||
},
|
||
"processes": post_process_data,
|
||
"modules": {"count": 3000},
|
||
}
|
||
],
|
||
}
|
||
build = {
|
||
"campaign": "fake",
|
||
"platform": "linux/arm64",
|
||
"before_commit": "before",
|
||
"after_commit": "after",
|
||
"substrate": {"reference": "frozen"},
|
||
}
|
||
|
||
report = harness.build_markdown_report(build, None, [sample])
|
||
|
||
assert "## 场景激活" in report
|
||
assert "browser-headed" in report
|
||
assert "Single-flight" in report
|
||
assert "### `browser-headed`" in report
|
||
assert "1.25" in report
|
||
|
||
|
||
def test_markdown_reports_agent_observation_revision_and_sentinel() -> None:
|
||
"""Agent 场景报告展示物化边界、revision 与定时哨兵峰值。"""
|
||
harness = load_module(
|
||
"moviepilot_perf_agent_report",
|
||
PERF_DIR / "moviepilot_docker_ab.py",
|
||
)
|
||
zero = {prefix: 0 for prefix in harness.AGENT_HEAVY_MODULE_PREFIXES}
|
||
loaded = dict(zero)
|
||
loaded["app.agent.tools.factory"] = 1
|
||
loaded["app.agent.tools.impl"] = 82
|
||
pre = agent_snapshot(zero)
|
||
post = agent_snapshot(loaded)
|
||
activation = {
|
||
"worker_elapsed_seconds": 2.5,
|
||
"pre": pre,
|
||
"post": post,
|
||
"marker": {"success": True},
|
||
"validation": {
|
||
"passed": True,
|
||
"observed": {
|
||
"prefix_before": zero,
|
||
"prefix_after": loaded,
|
||
"tool_factory_materialized_before": False,
|
||
"tool_factory_materialized_after": True,
|
||
},
|
||
"action": {
|
||
"tool_count": 82,
|
||
"schema_count": 82,
|
||
"repeat_stable": True,
|
||
"collision_names": [],
|
||
},
|
||
"revision": {"plugin": 7, "factory": "1234567890abcdef"},
|
||
},
|
||
}
|
||
sample = {
|
||
"scenario": "agent-tool-catalog",
|
||
"variant": "after",
|
||
"sample_index": 1,
|
||
"http_ready_seconds": 7.0,
|
||
"activation": activation,
|
||
"measurements": [
|
||
{
|
||
"target_minute": 1.0,
|
||
"engine": post["engine"],
|
||
"processes": post["processes"],
|
||
"modules": {
|
||
"count": 3082,
|
||
"prefix_counts": loaded,
|
||
},
|
||
}
|
||
],
|
||
}
|
||
build = {
|
||
"campaign": "fake",
|
||
"platform": "linux/arm64",
|
||
"before_commit": "before",
|
||
"after_commit": "after",
|
||
"substrate": {"reference": "frozen"},
|
||
}
|
||
|
||
report = harness.build_markdown_report(build, None, [sample])
|
||
|
||
assert "## Agent 场景动作" in report
|
||
assert "False→True" in report
|
||
assert "82/82; repeat=Y; collision=0" in report
|
||
assert "7/1234567890ab" in report
|
||
assert "## Agent 模块哨兵" in report
|
||
assert "app.agent.tools.impl=82" in report
|