"""run_job 研究子进程入口的平台/配置分支单测。 覆盖点(Review 补测): - `_apply_memory_limit` 成功路径确实施加了请求的上限; - 环境变量缺失时回退 `Settings.job_memory_limit_gb`; - setrlimit 失败(如 macOS 上限额低于进程虚拟地址空间基线 → EINVAL/ValueError) **不得抛出**,否则研究子进程启动即失败; - `_subprocess_log_target` 落盘失败时退回 DEVNULL —— 记日志失败不能拖垮 Job。 """ from __future__ import annotations import resource import pytest from app.application.services import job_executor as je from app.cli import run_job def test_apply_memory_limit_sets_requested_value(monkeypatch) -> None: captured: dict = {} monkeypatch.setenv("QLIB_JOB_MEM_LIMIT_GB", "3") monkeypatch.setattr(resource, "setrlimit", lambda which, lim: captured.update(w=which, lim=lim)) run_job._apply_memory_limit() assert captured["w"] == resource.RLIMIT_AS assert captured["lim"] == (3 * 1024**3, 3 * 1024**3) def test_apply_memory_limit_falls_back_to_settings(monkeypatch) -> None: """未设 QLIB_JOB_MEM_LIMIT_GB 时用 Settings 的上限(config.yaml job.max_memory_gb)。""" from app.core.config import get_settings captured: dict = {} monkeypatch.delenv("QLIB_JOB_MEM_LIMIT_GB", raising=False) monkeypatch.setattr(resource, "setrlimit", lambda which, lim: captured.update(lim=lim)) run_job._apply_memory_limit() gb = get_settings().job_memory_limit_gb assert captured["lim"] == (gb * 1024**3, gb * 1024**3) def test_apply_memory_limit_failure_does_not_raise(monkeypatch, capsys) -> None: """setrlimit 失败(macOS 限额低于地址空间基线的典型情形)只提示、不抛出。""" def _boom(which, lim): # noqa: ANN001 raise ValueError("current limit exceeds maximum limit") monkeypatch.setenv("QLIB_JOB_MEM_LIMIT_GB", "6") monkeypatch.setattr(resource, "setrlimit", _boom) run_job._apply_memory_limit() # 不应抛异常 err = capsys.readouterr().err assert "内存上限 6GB 设置失败" in err assert "无内存隔离保护" in err def test_subprocess_log_target_falls_back_to_devnull(tmp_path, monkeypatch) -> None: """日志目录不可创建时退回 DEVNULL,不影响 Job 执行。""" blocker = tmp_path / "not-a-dir" blocker.write_text("x", encoding="utf-8") monkeypatch.setattr(je, "BACKEND_ROOT", blocker / "sub") assert je._subprocess_log_target() is je.subprocess.DEVNULL def test_subprocess_log_target_writes_under_project_logs(tmp_path, monkeypatch) -> None: """正常路径:在 <项目根>/logs/job-subprocess.log 追加写入。""" monkeypatch.setattr(je, "BACKEND_ROOT", tmp_path / "backend") target = je._subprocess_log_target() try: target.write("hello-subprocess\n") finally: target.close() assert (tmp_path / "logs" / "job-subprocess.log").read_text(encoding="utf-8") == ( "hello-subprocess\n" ) if __name__ == "__main__": # pragma: no cover raise SystemExit(pytest.main([__file__, "-q"]))