Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"""Runtime install progress: the slow legs must tick, never hang silently.
|
||||
|
||||
The incident: 'Installing runtime…' sat frozen for minutes on a slow
|
||||
line — ensure_runtime_installed downloaded and extracted with no
|
||||
progress stream, so both the quickstart hero and the pane's install row
|
||||
showed a dead bar. Contract: _download and _extract tick per chunk /
|
||||
per member, ensure_runtime_installed forwards a staged stream, and the
|
||||
router's hook translates it into live job fields."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import hermes_cli.local_runtime.binaries as binaries
|
||||
from hermes_cli.web_routers.local_models import _job, _runtime_progress_hook
|
||||
|
||||
|
||||
def _make_zip(path: Path, names_sizes: dict[str, int]) -> None:
|
||||
with zipfile.ZipFile(path, "w") as z:
|
||||
for name, size in names_sizes.items():
|
||||
z.writestr(name, b"x" * size)
|
||||
|
||||
|
||||
def test_extract_ticks_per_member(tmp_path):
|
||||
archive = tmp_path / "runtime.zip"
|
||||
_make_zip(archive, {"a.bin": 1000, "b.bin": 3000, "c.bin": 500})
|
||||
ticks: list[tuple[int, int]] = []
|
||||
binaries._extract(archive, tmp_path / "out",
|
||||
progress=lambda d, t: ticks.append((d, t)))
|
||||
assert len(ticks) == 3
|
||||
total = 4500
|
||||
assert all(t == total for _, t in ticks)
|
||||
assert [d for d, _ in ticks] == sorted(d for d, _ in ticks)
|
||||
assert ticks[-1][0] == total
|
||||
assert (tmp_path / "out" / "b.bin").stat().st_size == 3000
|
||||
|
||||
|
||||
def test_download_ticks_with_content_length(tmp_path, monkeypatch):
|
||||
payload = b"y" * (3 << 20) # 3 MiB -> several 1 MiB chunks
|
||||
|
||||
class _Resp(io.BytesIO):
|
||||
headers = {"Content-Length": str(len(payload))}
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, *a):
|
||||
return False
|
||||
|
||||
monkeypatch.setattr(binaries.urllib.request, "urlopen",
|
||||
lambda url, timeout=0: _Resp(payload))
|
||||
ticks: list[tuple[int, int]] = []
|
||||
dest = tmp_path / "asset.zip"
|
||||
binaries._download("http://x/asset.zip", dest,
|
||||
progress=lambda d, t: ticks.append((d, t)))
|
||||
assert dest.read_bytes() == payload
|
||||
assert len(ticks) >= 3
|
||||
assert ticks[-1] == (len(payload), len(payload))
|
||||
|
||||
|
||||
def test_ensure_runtime_installed_forwards_staged_progress(tmp_path, monkeypatch):
|
||||
"""The full install path emits download -> verify -> extract stages
|
||||
(per asset) and a final verify, all through one callback."""
|
||||
monkeypatch.setattr(binaries, "runtimes_root", lambda: tmp_path)
|
||||
|
||||
class _Plan:
|
||||
assets = ["a.zip", "b.zip"]
|
||||
backend = "cuda"
|
||||
install_dir = tmp_path / "b1" / "cuda"
|
||||
|
||||
monkeypatch.setattr(binaries, "resolve_assets", lambda tag, backend: _Plan())
|
||||
monkeypatch.setattr(binaries, "verify_install", lambda d, t: "ok")
|
||||
|
||||
def _fake_download(url, dest, progress=None):
|
||||
_make_zip(dest, {"f.bin": 2048})
|
||||
if progress is not None:
|
||||
progress(1024, 2048)
|
||||
progress(2048, 2048)
|
||||
|
||||
monkeypatch.setattr(binaries, "_download", _fake_download)
|
||||
|
||||
events: list[tuple[str, str]] = []
|
||||
binaries.ensure_runtime_installed(
|
||||
"b1", "cuda",
|
||||
progress=lambda stage, d, t, label: events.append((stage, label)))
|
||||
|
||||
stages = [s for s, _ in events]
|
||||
assert "download" in stages and "extract" in stages and "verify" in stages
|
||||
# Two assets -> per-asset labels on the slow stages.
|
||||
assert ("download", "1/2") in events and ("download", "2/2") in events
|
||||
assert ("extract", "1/2") in events and ("extract", "2/2") in events
|
||||
# Stage order per asset: download before extract.
|
||||
assert stages.index("download") < stages.index("extract")
|
||||
|
||||
|
||||
def test_progress_hook_translates_stages_to_job_fields():
|
||||
job = _job("quickstart", "Test Model")
|
||||
hook = _runtime_progress_hook(job)
|
||||
|
||||
hook("download", 5 << 20, 100 << 20, "1/2")
|
||||
assert job["phase"] == "downloading-runtime"
|
||||
assert "1/2" in job["detail"]
|
||||
assert job["done_bytes"] == 5 << 20
|
||||
assert job["total_bytes"] == 100 << 20
|
||||
|
||||
# Rapid second tick inside the throttle window is dropped...
|
||||
hook("download", 6 << 20, 100 << 20, "1/2")
|
||||
assert job["done_bytes"] == 5 << 20
|
||||
# ...but a terminal tick (done == total) always lands.
|
||||
hook("download", 100 << 20, 100 << 20, "1/2")
|
||||
assert job["done_bytes"] == 100 << 20
|
||||
|
||||
hook("extract", 10, 100, "")
|
||||
assert job["phase"] in ("downloading-runtime", "unpacking-runtime")
|
||||
|
||||
job2 = _job("runtime-install", "x")
|
||||
hook2 = _runtime_progress_hook(job2)
|
||||
hook2("extract", 100, 100, "")
|
||||
assert job2["phase"] == "unpacking-runtime"
|
||||
hook2("verify", 0, 0, "")
|
||||
assert job2["phase"] == "verifying-runtime"
|
||||
assert job2["total_bytes"] is None # indeterminate bar, not a stuck 0%
|
||||
|
||||
|
||||
def test_progress_hook_accumulates_across_assets(monkeypatch):
|
||||
"""A two-asset engine reads as ONE growing download: the second asset's
|
||||
bytes stack on the first's instead of restarting the bar at zero, and
|
||||
unpack/verify leave the finished download's counters standing."""
|
||||
# Drive the throttle's clock so every tick lands (the real hook drops
|
||||
# sub-250ms non-terminal ticks; this test is about arithmetic, not
|
||||
# pacing — pacing has its own assertions above).
|
||||
from hermes_cli.web_routers import local_models as lm
|
||||
|
||||
clock = {"now": 0.0}
|
||||
|
||||
def fake_monotonic():
|
||||
clock["now"] += 1.0
|
||||
return clock["now"]
|
||||
|
||||
monkeypatch.setattr(lm.time, "monotonic", fake_monotonic)
|
||||
|
||||
job = _job("runtime-install", "engine")
|
||||
hook = _runtime_progress_hook(job)
|
||||
|
||||
hook("download", 40 << 20, 40 << 20, "1/2")
|
||||
assert job["done_bytes"] == 40 << 20
|
||||
assert job["total_bytes"] == 40 << 20
|
||||
|
||||
# Second asset starts: counters continue from the first asset's total.
|
||||
hook("download", 0, 60 << 20, "2/2")
|
||||
assert job["done_bytes"] == 40 << 20
|
||||
assert job["total_bytes"] == 100 << 20
|
||||
|
||||
hook("download", 60 << 20, 60 << 20, "2/2")
|
||||
assert job["done_bytes"] == 100 << 20
|
||||
assert job["total_bytes"] == 100 << 20
|
||||
|
||||
# Unpack and verify narrate without rewinding the finished bar.
|
||||
hook("extract", 1, 100, "2/2")
|
||||
assert job["phase"] == "unpacking-runtime"
|
||||
assert job["done_bytes"] == 100 << 20
|
||||
hook("verify", 0, 0, "")
|
||||
assert job["phase"] == "verifying-runtime"
|
||||
assert job["done_bytes"] == 100 << 20
|
||||
Reference in New Issue
Block a user