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
165 lines
6.8 KiB
Python
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
|