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.
125 lines
4.6 KiB
Python
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}"
|