mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-28 19:06:36 +08:00
179 lines
5.7 KiB
Python
179 lines
5.7 KiB
Python
"""
|
||
原生并发事实门禁回归测试。
|
||
"""
|
||
|
||
import textwrap
|
||
from pathlib import Path
|
||
|
||
from scripts.architecture.concurrency import collect_concurrency, compare_concurrency
|
||
|
||
|
||
def _write_source(root: Path, relative: str, source: str) -> None:
|
||
"""
|
||
写入临时宿主源码,供 AST collector 做结构性验证。
|
||
"""
|
||
source_path = root / relative
|
||
source_path.parent.mkdir(parents=True, exist_ok=True)
|
||
source_path.write_text(textwrap.dedent(source), encoding="utf-8")
|
||
|
||
|
||
def test_concurrency_gate_resolves_aliases_and_runtime_sources(tmp_path: Path) -> None:
|
||
"""
|
||
canonical 别名、TaskGroup、事件循环和 Executor 来源必须完整枚举。
|
||
"""
|
||
_write_source(
|
||
tmp_path,
|
||
"app/doctor/sample.py",
|
||
"""
|
||
import asyncio as aio
|
||
import concurrent.futures as cf
|
||
from asyncio import AbstractEventLoop, TaskGroup as TG, create_task as spawn
|
||
from concurrent.futures import ThreadPoolExecutor as Pool
|
||
from threading import Thread as WorkerThread
|
||
|
||
async def run(job, typed_loop: AbstractEventLoop):
|
||
loop = aio.get_running_loop()
|
||
spawn(job())
|
||
aio.create_task(job())
|
||
loop.create_task(job())
|
||
typed_loop.run_in_executor(None, job)
|
||
async with TG() as group:
|
||
group.create_task(job())
|
||
with cf.ThreadPoolExecutor() as executor:
|
||
executor.submit(job)
|
||
with Pool() as executor:
|
||
executor.map(job, [])
|
||
WorkerThread(target=job)
|
||
""",
|
||
)
|
||
|
||
facts = collect_concurrency(tmp_path)
|
||
|
||
assert facts["app/doctor/sample.py:asyncio.create_task"]["owners"] == {"run": 2}
|
||
assert facts["app/doctor/sample.py:asyncio.AbstractEventLoop.create_task"]["owners"] == {
|
||
"run": 1
|
||
}
|
||
assert facts["app/doctor/sample.py:asyncio.AbstractEventLoop.run_in_executor"][
|
||
"owners"
|
||
] == {"run": 1}
|
||
assert facts["app/doctor/sample.py:asyncio.TaskGroup"]["owners"] == {"run": 1}
|
||
assert facts["app/doctor/sample.py:asyncio.TaskGroup.create_task"]["owners"] == {
|
||
"run": 1
|
||
}
|
||
assert facts["app/doctor/sample.py:concurrent.futures.ThreadPoolExecutor"][
|
||
"owners"
|
||
] == {"run": 2}
|
||
assert facts["app/doctor/sample.py:concurrent.futures.Executor.submit"]["owners"] == {
|
||
"run": 1
|
||
}
|
||
assert facts["app/doctor/sample.py:concurrent.futures.Executor.map"]["owners"] == {
|
||
"run": 1
|
||
}
|
||
assert facts["app/doctor/sample.py:threading.Thread"]["owners"] == {"run": 1}
|
||
|
||
|
||
def test_concurrency_gate_excludes_unproven_same_named_calls(tmp_path: Path) -> None:
|
||
"""
|
||
普通同名函数和对象方法没有 canonical 来源时不得误报。
|
||
"""
|
||
_write_source(
|
||
tmp_path,
|
||
"app/testing/fakes.py",
|
||
"""
|
||
def create_task():
|
||
return None
|
||
|
||
def run_in_threadpool():
|
||
return None
|
||
|
||
def run(obj):
|
||
create_task()
|
||
run_in_threadpool()
|
||
obj.create_task()
|
||
obj.submit()
|
||
""",
|
||
)
|
||
|
||
assert collect_concurrency(tmp_path) == {}
|
||
|
||
|
||
def test_concurrency_facts_ignore_line_drift_and_preserve_counts(tmp_path: Path) -> None:
|
||
"""
|
||
空行漂移不得改变事实键,同一行多个调用必须保留精确计数。
|
||
"""
|
||
relative = "app/runtime/sample.py"
|
||
_write_source(
|
||
tmp_path,
|
||
relative,
|
||
"""
|
||
import asyncio
|
||
async def run(first, second):
|
||
asyncio.create_task(first()); asyncio.create_task(second())
|
||
""",
|
||
)
|
||
baseline = collect_concurrency(tmp_path)
|
||
_write_source(
|
||
tmp_path,
|
||
relative,
|
||
"""
|
||
|
||
|
||
import asyncio
|
||
async def run(first, second):
|
||
asyncio.create_task(first()); asyncio.create_task(second())
|
||
""",
|
||
)
|
||
|
||
current = collect_concurrency(tmp_path)
|
||
assert current == baseline
|
||
assert current["app/runtime/sample.py:asyncio.create_task"]["owners"] == {"run": 2}
|
||
|
||
|
||
def test_concurrency_gate_scans_complete_app_and_keeps_exact_exclusions(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
"""
|
||
默认扫描必须覆盖完整 app,同时排除 Plugin、SDK 和 Compat 表面。
|
||
"""
|
||
source = """
|
||
import asyncio
|
||
asyncio.create_task(job())
|
||
"""
|
||
_write_source(tmp_path, "app/doctor/check.py", source)
|
||
_write_source(tmp_path, "app/plugins/copy.py", source)
|
||
_write_source(tmp_path, "app/sdk/facade.py", source)
|
||
_write_source(tmp_path, "app/runtime/compat/legacy.py", source)
|
||
|
||
facts = collect_concurrency(tmp_path)
|
||
assert list(facts) == ["app/doctor/check.py:asyncio.create_task"]
|
||
assert facts["app/doctor/check.py:asyncio.create_task"]["owners"] == {"<module>": 1}
|
||
|
||
|
||
def test_concurrency_ratchet_rejects_growth_and_stale_low_water() -> None:
|
||
"""
|
||
新增 owner、调用增长和未刷新的下降事实都必须阻断 ratchet。
|
||
"""
|
||
baseline = {
|
||
"app/runtime/tasks.py:asyncio.create_task": {
|
||
"target": "asyncio.create_task",
|
||
"owners": {"Runtime.run": 2, "Runtime.old": 1},
|
||
},
|
||
}
|
||
current = {
|
||
"app/runtime/tasks.py:asyncio.create_task": {
|
||
"target": "asyncio.create_task",
|
||
"owners": {"Runtime.run": 3, "Runtime.new": 1},
|
||
},
|
||
"app/api/system.py:threading.Thread": {
|
||
"target": "threading.Thread",
|
||
"owners": {"system": 1},
|
||
},
|
||
}
|
||
|
||
problems = compare_concurrency(baseline, current)
|
||
|
||
assert any("新增原生并发事实" in problem for problem in problems)
|
||
assert any("Runtime.run 3>2" in problem for problem in problems)
|
||
assert any("Runtime.new 1>0" in problem for problem in problems)
|
||
assert any("Runtime.old 0<1" in problem for problem in problems)
|