fix: validate optional Python resources and isolate downloads
This commit is contained in:
1 parent
305021fcd6
commit
4fd85be130
19 files changed
+335
-149
No files matched your search
@@ -136,7 +136,6 @@ def _run(arguments: list[str], options: OfficeOptions | None, result_type: type[
|
||||
process = run_office_process(
|
||||
[*deepseek_harness_runtime.resolve_office_launch_args(), *arguments,
|
||||
*(options._arguments() if options is not None else [])],
|
||||
capture_output=True,
|
||||
)
|
||||
stdout, stderr = process.stdout, process.stderr
|
||||
if process.returncode != 0:
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Office interruption terminates the owned process tree before returning to callers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ntpath
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
from deepseek_harness_runtime import _resources
|
||||
|
||||
|
||||
def wait_for_file(path: Path, process: subprocess.Popen, timeout: float = 30) -> None:
|
||||
deadline = time.monotonic() + timeout
|
||||
while not path.is_file():
|
||||
if process.poll() is not None:
|
||||
raise AssertionError(f"fixture exited before {path.name}: {process.returncode}")
|
||||
if time.monotonic() >= deadline:
|
||||
raise TimeoutError(f"fixture never produced {path}")
|
||||
time.sleep(0.01)
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="POSIX process groups and SIGINT delivery")
|
||||
def test_second_interrupt_waits_for_the_entire_office_process_tree(tmp_path: Path) -> None:
|
||||
fixture = tmp_path / "office.py"
|
||||
ready, interrupted, pidfile = (tmp_path / name for name in ("ready", "interrupted", "pid"))
|
||||
fixture.write_text("""import json,os,signal,subprocess,sys
|
||||
from pathlib import Path
|
||||
root=Path(sys.argv[1])
|
||||
(root/'pid').write_text(str(os.getpid()))
|
||||
signal.signal(signal.SIGINT,lambda *_:(root/'interrupted').touch())
|
||||
child=subprocess.Popen([sys.executable,'-c',
|
||||
"import signal,sys; from pathlib import Path; signal.signal(signal.SIGINT,signal.SIG_IGN); Path(sys.argv[1]).touch(); signal.pause()",str(root/'child-ready')])
|
||||
(root/'ready').touch()
|
||||
child.wait()
|
||||
""", encoding="utf-8")
|
||||
code = ("import sys; from deepseek_harness_runtime._resources import run_office_process; "
|
||||
f"args=[sys.executable,{str(fixture)!r},{str(tmp_path)!r}]\n"
|
||||
"try: run_office_process(args)\n"
|
||||
"except KeyboardInterrupt: print('tree terminated',flush=True); sys.exit(73)\n")
|
||||
process = subprocess.Popen([sys.executable, "-c", code], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
start_new_session=True, env={**os.environ,
|
||||
"PYTHONPATH": str(Path(_resources.__file__).resolve().parents[1])})
|
||||
try:
|
||||
wait_for_file(ready, process)
|
||||
wait_for_file(tmp_path / "child-ready", process)
|
||||
os.kill(process.pid, signal.SIGINT)
|
||||
wait_for_file(interrupted, process)
|
||||
os.kill(process.pid, signal.SIGINT)
|
||||
# The grandchild inherits the captured streams; communicate can finish only after it closes them.
|
||||
stdout, stderr = process.communicate(timeout=30)
|
||||
assert process.returncode == 73, stderr
|
||||
assert stdout == b"tree terminated\n"
|
||||
finally:
|
||||
if pidfile.is_file():
|
||||
try:
|
||||
os.killpg(int(pidfile.read_text()), signal.SIGKILL)
|
||||
except ProcessLookupError as error:
|
||||
# The successful interruption path already terminated this group.
|
||||
pass
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
process.communicate(timeout=30)
|
||||
|
||||
|
||||
def test_windows_second_interrupt_uses_absolute_taskkill_and_waits(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
calls = []
|
||||
|
||||
class Process:
|
||||
pid = 73
|
||||
returncode = -9
|
||||
attempts = 0
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *_):
|
||||
pass
|
||||
|
||||
def communicate(self):
|
||||
self.attempts += 1
|
||||
if self.attempts < 3:
|
||||
raise KeyboardInterrupt
|
||||
return b"", b""
|
||||
|
||||
process = Process()
|
||||
|
||||
def popen(arguments, **options):
|
||||
assert options == {"stdin": subprocess.DEVNULL, "stdout": subprocess.PIPE,
|
||||
"stderr": subprocess.PIPE, "start_new_session": False}
|
||||
return process
|
||||
|
||||
monkeypatch.setattr(_resources, "os", SimpleNamespace(name="nt", path=ntpath, environ={"SystemRoot": r"C:\Windows"}))
|
||||
monkeypatch.setattr(_resources, "subprocess", SimpleNamespace(
|
||||
Popen=popen, DEVNULL=subprocess.DEVNULL, PIPE=subprocess.PIPE,
|
||||
run=lambda arguments, **options: calls.append((arguments, options)),
|
||||
))
|
||||
with pytest.raises(KeyboardInterrupt):
|
||||
_resources.run_office_process(["node.exe", "cli.js"])
|
||||
assert process.attempts == 3
|
||||
assert calls == [([r"C:\Windows\System32\taskkill.exe", "/PID", "73", "/T", "/F"],
|
||||
{"stdin": subprocess.DEVNULL, "capture_output": True, "check": False})]
|
||||
@@ -332,24 +332,57 @@ def test_runtime_starts_independently_of_office_engine(
|
||||
assert runtime.bundled_runtime_path() == executable
|
||||
|
||||
|
||||
@pytest.mark.parametrize("function,flag", [
|
||||
(runtime.download_office, "--download-office"),
|
||||
(runtime.download_primary_runtime, "--download-primary-runtime"),
|
||||
@pytest.mark.parametrize("function,resource", [
|
||||
(runtime.download_office, "office"),
|
||||
(runtime.download_primary_runtime, "primary"),
|
||||
])
|
||||
def test_explicit_download_uses_runtime_without_a_harness_home(
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, function, flag: str,
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, function, resource: str,
|
||||
) -> None:
|
||||
root = tmp_path / "中文缓存"
|
||||
root.mkdir()
|
||||
_resource_sidecars(root / "deepseek-harness-sdk-runtime-linux-x64")
|
||||
monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
|
||||
script = tmp_path / "download.py"
|
||||
destination = tmp_path / "downloaded"
|
||||
script.write_text("import json,sys\n" + f"assert sys.argv[1] == {flag!r}\nprint(json.dumps({str(destination)!r}))\n")
|
||||
destination = root / "linux-x64/primary-runtime" if resource == "primary" else root
|
||||
script.write_text("import json,os,sys\n" + f"assert os.environ['DSH_RUNTIME_DOWNLOAD'] == {resource!r}\n"
|
||||
+ f"sys.stdout.buffer.write(json.dumps({str(destination)!r}, ensure_ascii=False).encode('utf-8'))\n", encoding="utf-8")
|
||||
monkeypatch.delenv("DSH_HOME", raising=False)
|
||||
monkeypatch.setattr(runtime, "resolve_bundled_launch_args", lambda: (sys.executable, str(script)))
|
||||
assert function() == destination
|
||||
script.write_text("import sys\nprint('checksum mismatch', file=sys.stderr)\nsys.exit(2)\n")
|
||||
with pytest.raises(RuntimeError, match="checksum mismatch"):
|
||||
script.write_text("import sys\nsys.stderr.buffer.write('下载失败'.encode('utf-8'))\nsys.exit(2)\n", encoding="utf-8")
|
||||
with pytest.raises(RuntimeError, match="下载失败"):
|
||||
function()
|
||||
|
||||
|
||||
@pytest.mark.skipif(os.name == "nt", reason="LC_ALL=C is a POSIX locale fixture")
|
||||
def test_download_decodes_utf8_in_an_ascii_locale(tmp_path: Path) -> None:
|
||||
script = tmp_path / "download.py"
|
||||
destination = tmp_path / "中文缓存"
|
||||
script.write_text("import sys\nsys.stdout.buffer.write(" + json.dumps(str(destination), ensure_ascii=False).encode('utf-8').__repr__() + ")\n", encoding="utf-8")
|
||||
code = ("import sys,locale,deepseek_harness_runtime as r; "
|
||||
"assert locale.getpreferredencoding(False).lower() not in ('utf-8','utf8'); "
|
||||
f"r.resolve_bundled_launch_args=lambda:(sys.executable,{str(script)!r}); "
|
||||
f"assert str(r.download_office())=={str(destination)!r}")
|
||||
result = subprocess.run([sys.executable, "-c", code], capture_output=True, encoding="utf-8", env={
|
||||
**os.environ, "LC_ALL": "C", "PYTHONUTF8": "0", "PYTHONCOERCECLOCALE": "0",
|
||||
"PYTHONPATH": str(Path(runtime.__file__).resolve().parents[1]),
|
||||
}, timeout=30)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
@pytest.mark.parametrize("missing", ["primary-runtime/runtime.json", "primary-runtime/dependencies/python/bin/python3",
|
||||
"primary-runtime/dependencies/node/bin/node", "office-skills/scripts/check_office.py"])
|
||||
def test_primary_download_rejects_an_incomplete_payload(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, missing: str) -> None:
|
||||
_resource_sidecars(tmp_path / "deepseek-harness-sdk-runtime-linux-x64")
|
||||
root = tmp_path / "linux-x64"
|
||||
(root / missing).unlink()
|
||||
monkeypatch.setattr(runtime, "_download", lambda resource: root / "primary-runtime")
|
||||
monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "linux-x64")
|
||||
with pytest.raises(FileNotFoundError):
|
||||
runtime.download_primary_runtime()
|
||||
|
||||
|
||||
def test_office_resolution_requires_explicit_download(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from deepseek_harness_runtime._resources import office_launch_args
|
||||
|
||||
@@ -366,4 +399,32 @@ def test_office_resolution_requires_explicit_download(tmp_path: Path, monkeypatc
|
||||
for path in (node, cli):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.touch()
|
||||
with pytest.raises(FileNotFoundError, match="incomplete"):
|
||||
office_launch_args(tmp_path / "linux-x64", "linux-x64")
|
||||
(office / "complete").write_text("wrong", encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="identity mismatch"):
|
||||
office_launch_args(tmp_path / "linux-x64", "linux-x64")
|
||||
(office / "complete").write_text("a" * 64, encoding="utf-8")
|
||||
assert office_launch_args(tmp_path / "linux-x64", "linux-x64") == (str(node), str(cli))
|
||||
(office / "complete").unlink()
|
||||
monkeypatch.setenv("DSH_OFFICE_SIDECAR", str(office))
|
||||
assert office_launch_args(tmp_path / "linux-x64", "linux-x64") == (str(node), str(cli))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("invalid", ["target", "identity", "office", "pnpm", "skills"])
|
||||
def test_download_manifest_rejects_invalid_wheel_resources(tmp_path: Path, invalid: str) -> None:
|
||||
from deepseek_harness_runtime._resources import validate_downloads
|
||||
|
||||
_resource_sidecars(tmp_path / "deepseek-harness-sdk-runtime-linux-x64")
|
||||
root = tmp_path / "linux-x64"
|
||||
path = root / "downloads.json"
|
||||
manifest = json.loads(path.read_text(encoding="utf-8"))
|
||||
if invalid == "skills":
|
||||
(root / "office-skills/scripts/check_office.py").unlink()
|
||||
with pytest.raises(FileNotFoundError, match="Office skill"):
|
||||
validate_downloads(root, "linux-x64")
|
||||
else:
|
||||
manifest[invalid] = {"target": "win-x64", "identity": "z" * 64, "office": [], "pnpm": []}[invalid]
|
||||
path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
with pytest.raises(ValueError, match="invalid runtime download manifest"):
|
||||
validate_downloads(root, "linux-x64")
|
||||
Reference in new issue
Block a user