Files
hermes-agent/tests/pm/_range_server.py
ethernet bb824c381f merge: combine network retries with downloader safety
Keep the retry branch's bounded network policy under installation.
Preserve destination-local atomic publication, partial ownership,
representation checks, aggregate progress and operation-owned pause.
Keep the serial CDN fallback active across subsequent retry attempts.

Retain exact-origin authorization for index reads and safe redirects.
The retry policy does not retry hash, disk or certificate failures.

Verified with canonical native Windows ARM64 tests for both branches,
local-model download consumers, PM core and update resolution.
The commit-build draft remains uncommitted and unchanged.
2026-09-09 17:40:19 -04:00

125 lines
4.6 KiB
Python

"""Shared Range-honoring loopback server for pm downloader tests.
Both test_downloader.py and test_store_resume.py exercise real downloads
through this one server instead of each re-implementing it. Behaviour is
configured with class attributes on :class:`RangeHandler`; the ``dl_server``
fixture starts one and resets its state.
"""
from __future__ import annotations
import hashlib
import threading
import time
from http.server import BaseHTTPRequestHandler, HTTPServer
import pytest
class RangeHandler(BaseHTTPRequestHandler):
payloads: dict = {}
ranges_seen: list = [] # (path, start, end) from real Range requests
abort_after: int | None = None # refuse bytes beyond this payload offset
slow_per_chunk: float = 0.0 # sleep per served piece (pause tests)
no_range: bool = False # ignore Range, serve 200 full body
etags: bool = True
chunk: int = 1 << 20 # serve piece size
def log_message(self, *args): # noqa: A002 - silence request logging
pass
def end_headers(self):
payload = self.payloads.get(self.path)
if payload is not None and self.etags:
self.send_header("ETag", '"' + hashlib.sha256(payload).hexdigest() + '"')
super().end_headers()
def do_GET(self): # noqa: N802 - http.server API
payload = self.payloads.get(self.path)
if payload is None:
self.send_error(404)
return
if self.no_range:
# A server that ignores Range: 200 with the full body, even
# when the client asked for a byte range.
self.send_response(200)
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
served = 0
while served < len(payload):
if self.abort_after is not None and served >= self.abort_after:
self.connection.close()
return
if self.slow_per_chunk:
time.sleep(self.slow_per_chunk)
piece = payload[served:served + self.chunk]
self.wfile.write(piece)
self.wfile.flush()
served += len(piece)
return
rng = self.headers.get("Range")
if rng:
spec = rng.removeprefix("bytes=")
if spec == "0-0":
# probe: 206 with total from Content-Range, 1 byte body
total = len(payload)
self.send_response(206)
self.send_header("Content-Range", f"bytes 0-0/{total}")
self.send_header("Content-Length", "1")
self.end_headers()
self.wfile.write(payload[:1])
self.wfile.flush()
return
start_s, end_s = spec.split("-", 1)
start, end = int(start_s), int(end_s)
self.ranges_seen.append((self.path, start, end))
body = payload[start:end + 1]
self.send_response(206)
self.send_header("Content-Range",
f"bytes {start}-{end}/{len(payload)}")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
served = 0
while served < len(body):
if self.abort_after is not None and start + served >= self.abort_after:
self.connection.close()
return
if self.slow_per_chunk:
time.sleep(self.slow_per_chunk)
end = served + self.chunk
if self.abort_after is not None:
end = min(end, self.abort_after - start)
piece = body[served:end]
self.wfile.write(piece)
self.wfile.flush()
served += len(piece)
return
self.send_response(200)
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
self.wfile.flush()
@pytest.fixture
def dl_server():
RangeHandler.payloads = {}
RangeHandler.ranges_seen = []
RangeHandler.abort_after = None
RangeHandler.slow_per_chunk = 0.0
RangeHandler.no_range = False
RangeHandler.etags = True
RangeHandler.chunk = 1 << 20
server = HTTPServer(("127.0.0.1", 0), RangeHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield server
finally:
server.shutdown()
server.server_close()
def url(server, path: str) -> str:
return f"http://127.0.0.1:{server.server_port}{path}"