Files
Behavision/tests/test_model_download.py
Suriyakumarvijayanayagam 0558344dc2 urllib does not let SSLCertVerificationError out, and a fake said it did
The certificate fix shipped and failed on the machine it was written for, with
the exact traceback it was meant to prevent. The retry was written

    except ssl.SSLCertVerificationError:

and urllib never raises that from urlopen. It catches it and re-raises
urllib.error.URLError(err), carrying the original on .reason. So the except
matched nothing, ever, and the fallback could not fire.

The unit test passed throughout, because the stub it used raised the bare SSL
error - a shape real urllib never produces. That is the lesson: a fake that
agrees with the author is worse than no test, because it converts an untested
path into a tested-looking one. This file already says that about
UPDATE ... RETURNING and about the in-memory API fake, and it got written
again anyway.

_is_cert_failure checks the exception and its .reason, and the tests now raise
URLError(SSLCertVerificationError(...)) - what the traceback actually shows. A
plain URLError is re-raised untouched, and a test asserts no second attempt is
made for one.

Beside the stubs there is now a real reproduction, opt-in behind
BEHAVISION_NETWORK_TESTS=1. Python's default context honours SSL_CERT_FILE, so
an empty file gives a context that trusts nobody - the python.org condition
exactly - while certifi is loaded by path and is unaffected. It skips rather
than passes where it cannot reproduce that, and the difference is measured:

    macOS Command Line Tools  LibreSSL 2.8.3   128 CAs with an empty CA file
    python.org / pyenv build  OpenSSL 3.5.8      0 CAs -> reproduces it

Checked for teeth by putting the shipped except back: both the corrected stub
test and the live one fail, and pass again when it is restored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGcjxF1cNLcuwc3DAPcnfj
2026-09-30 17:36:53 +05:30

165 lines
6.8 KiB
Python

"""Downloading the models is the last step of every fresh install, and it ran
into the one macOS trap nothing else here does.
A python.org macOS build ships its own OpenSSL with NO trust store, and
populates one only when somebody double-clicks `Install Certificates.command`
in the Python folder. Measured on a colleague's Mac: the engine installed
perfectly - numpy, onnxruntime, faiss, all of it - and then could not fetch a
230 KB model file, ending setup in forty lines of traceback about `_ssl.c`.
"""
import io
import logging
import os
import ssl
import urllib.request
import pytest
from behavision import model_assets
class _Resp(io.BytesIO):
"""Enough of an http response for _fetch: read() and .headers."""
def __init__(self, payload: bytes):
super().__init__(payload)
self.headers = {"Content-Length": str(len(payload))}
def __enter__(self):
return self
def __exit__(self, *exc):
self.close()
return False
def _verify_error():
"""What urlopen ACTUALLY raises, which is not what it looks like.
urllib catches ssl.SSLCertVerificationError and re-raises
urllib.error.URLError(err), carrying the original on `.reason`. The first
version of this test raised the bare SSL error - a shape real urllib never
produces - so it passed against a fallback that could never fire, and the
fix shipped and failed on the machine it was written for with the exact
traceback it was meant to prevent.
"""
return urllib.error.URLError(ssl.SSLCertVerificationError(
"[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: "
"unable to get local issuer certificate"))
def test_a_machine_with_no_trust_store_falls_back_to_the_bundled_one(monkeypatch):
calls = []
def fake(url, timeout=None, context=None):
calls.append(context)
if context is None:
raise _verify_error()
return _Resp(b"ok")
monkeypatch.setattr(urllib.request, "urlopen", fake)
with model_assets._urlopen("https://example.invalid/m.onnx") as resp:
assert resp.read() == b"ok"
assert len(calls) == 2, f"expected a retry, got {calls}"
# Default FIRST, and that order is the point. On Windows and on a system
# Python the default context reads the machine's own certificate store,
# which is what makes a corporate proxy with its own root CA work.
# Replacing it unconditionally would break every site that has one in
# order to fix a different platform.
assert calls[0] is None
assert isinstance(calls[1], ssl.SSLContext)
def test_a_working_trust_store_is_used_as_is(monkeypatch):
calls = []
def fake(url, timeout=None, context=None):
calls.append(context)
return _Resp(b"ok")
monkeypatch.setattr(urllib.request, "urlopen", fake)
model_assets._urlopen("https://example.invalid/m.onnx").close()
assert calls == [None], "the machine's own certificate store was bypassed"
def test_a_real_network_failure_is_not_disguised_as_a_certificate_problem(monkeypatch):
"""A URLError is not automatically a certificate problem.
"No route to host" and "connection refused" arrive as URLError too, and
retrying those with a different CA list changes nothing except how long
the operator waits for the real message.
"""
calls = []
def fake(url, timeout=None, context=None):
calls.append(context)
raise urllib.error.URLError("no route to host")
monkeypatch.setattr(urllib.request, "urlopen", fake)
with pytest.raises(urllib.error.URLError):
model_assets._urlopen("https://example.invalid/m.onnx")
assert calls == [None], f"a dead network was retried as a CA problem: {calls}"
def test_progress_lines_survive_the_rewrite(tmp_path, monkeypatch, caplog):
"""`download: <label> <n>%` is a CONTRACT, not logging.
The supervisor parses it (progressRe) to put first-run progress in the
tray and the window, because the API is not up yet and a shop PC showing
a stopped engine for five minutes after install looks broken. Switching
off urlretrieve - needed because it offers no way to pass an SSL context -
is exactly the kind of change that drops it silently.
"""
payload = b"x" * (64 * 1024 * 8)
monkeypatch.setattr(urllib.request, "urlopen",
lambda url, timeout=None, context=None: _Resp(payload))
dest = tmp_path / "model.onnx"
with caplog.at_level(logging.INFO, logger="behavision.model_assets"):
model_assets._fetch("https://example.invalid/m.onnx", dest, "face detector")
assert dest.read_bytes() == payload
lines = [r.getMessage() for r in caplog.records]
pct = [ln for ln in lines if ln.startswith("download: face detector ")]
assert pct, f"no progress lines at all: {lines}"
assert "download: face detector 100%" in pct, pct
@pytest.mark.skipif(not os.environ.get("BEHAVISION_NETWORK_TESTS"),
reason="set BEHAVISION_NETWORK_TESTS=1 to reach github.com")
def test_against_a_real_machine_with_no_trust_store(tmp_path, monkeypatch):
"""The whole thing, over the real network, with the real failure.
Every stub above is a statement about what I believe urllib does, and the
first version of this file proved how much that is worth. Python's default
context honours SSL_CERT_FILE, so pointing it at an EMPTY file reproduces
a python.org macOS build exactly: a default context that trusts nobody.
certifi is loaded by path and is unaffected by the variable, so if the
fallback works here it works there.
"""
empty = tmp_path / "no-cas.pem"
empty.write_text("")
monkeypatch.setenv("SSL_CERT_FILE", str(empty))
monkeypatch.setenv("SSL_CERT_DIR", str(tmp_path / "nothing"))
# Not every interpreter can be put into that state, and pretending
# otherwise would turn this into a test that passes by not running. A
# Python linked against LibreSSL - which is what macOS Command Line Tools
# ships - reads the system keychain and ignores SSL_CERT_FILE entirely:
# measured, 128 CAs loaded with the variable pointing at an empty file. A
# python.org build links OpenSSL and honours it: 0 CAs, which is the
# condition being reproduced.
if ssl.create_default_context().get_ca_certs():
pytest.skip("this interpreter ignores SSL_CERT_FILE (%s), so the "
"no-trust-store condition cannot be reproduced here"
% ssl.OPENSSL_VERSION)
# The premise: the default context really is broken in this process.
with pytest.raises(urllib.error.URLError) as caught:
urllib.request.urlopen(model_assets.YUNET_URL, timeout=30)
assert model_assets._is_cert_failure(caught.value), caught.value
dest = tmp_path / "yunet.onnx"
model_assets._fetch(model_assets.YUNET_URL, dest, "face detector")
assert dest.stat().st_size > 200_000