Skip to content

Commit b2367fd

Browse files
committed
new stub class for ci
1 parent e226abd commit b2367fd

2 files changed

Lines changed: 69 additions & 6 deletions

File tree

tests/conftest.py

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import importlib
6+
import importlib.machinery
67
import os
78
import sys
89
import tempfile
@@ -59,20 +60,80 @@
5960
#: is listed here.
6061
STUBBED_MODULES: set[str] = set()
6162

63+
# Probes that should return False on the stub (so call sites like
64+
# `torch.cuda.is_available()` take the "no GPU" / "not available" branch
65+
# instead of seeing a truthy MagicMock and crashing later on a `>` comparison).
66+
_STUB_FALSE_PROBES = frozenset(
67+
{"is_available", "is_initialized", "is_built"}
68+
)
69+
70+
71+
def _make_stub_class(name: str):
72+
"""Build a stub class whose instantiation returns a fresh MagicMock.
73+
74+
The kwargs to `stub_cls(...)` are set as attributes on the returned mock,
75+
so `VADIterator(model, threshold=0.5, min_silence_duration_ms=1500)` gives
76+
back a mock you can read `.threshold` / `.min_silence_duration_ms` from.
77+
Each call returns a NEW mock — critical for tests that compare two
78+
separately-constructed objects (e.g. `VadEndpointer._iterator` vs
79+
`_soft_iterator`); MagicMock's `return_value` reuse would alias them and
80+
make per-iterator updates clobber each other.
81+
82+
Names in `_STUB_FALSE_PROBES` return `False` instead of a mock, so probe
83+
calls like `torch.cuda.is_available()` are falsy and call sites skip the
84+
GPU/load branch.
85+
86+
A metaclass on the returned class routes `cls.SubName` through
87+
`_make_stub_class` again, so chains like `torch.cuda.is_available()`
88+
resolve (each link is a fresh stub class; the `is_available` one returns
89+
False per the probe list).
90+
"""
91+
92+
def __new__(cls, *args, **kwargs):
93+
if name in _STUB_FALSE_PROBES:
94+
return False
95+
m = MagicMock()
96+
for k, v in kwargs.items():
97+
setattr(m, k, v)
98+
return m
99+
100+
return _StubClassMeta(name, (), {"__new__": __new__})
101+
102+
103+
class _StubClassMeta(type):
104+
def __getattr__(cls, name):
105+
return _make_stub_class(name)
106+
62107

63108
class _StubModule(types.ModuleType):
64-
"""A module whose every attribute access returns a fresh MagicMock, so
65-
`import x`, `from x import Y`, and `x.Y(...)` all succeed harmlessly."""
109+
"""Stand-in for a heavy module that isn't installed (CI without GPU stack).
110+
111+
`import x` returns this; `from x import Y` resolves Y via `__getattr__`
112+
to a stub class (see `_make_stub_class`). `x.Y(...)` instantiates that
113+
class and returns a fresh MagicMock with kwargs as attributes.
114+
"""
66115

67116
def __getattr__(self, name): # noqa: D401 - simple delegation
68-
return MagicMock(name=f"{self.__name__}.{name}")
117+
# `__path__` is iterated by the import machinery when treating a
118+
# module as a package; returning a non-iterable stub class here
119+
# would break transitively-imported real packages that touch torch
120+
# (e.g. thinc via spacy via misaki). An empty list is what the
121+
# machinery would default to anyway.
122+
if name == "__path__":
123+
return []
124+
return _make_stub_class(name)
69125

70126

71127
for _name in _HEAVY_MODULES:
72128
try:
73129
importlib.import_module(_name)
74130
except Exception:
75-
sys.modules[_name] = _StubModule(_name)
131+
stub = _StubModule(_name)
132+
# Set a real __spec__ — the default ModuleType has __spec__ = None
133+
# which makes importlib.util.find_spec raise ValueError and breaks
134+
# tests that probe the dep via find_spec (e.g. test_cpu_imports).
135+
stub.__spec__ = importlib.machinery.ModuleSpec(name=_name, loader=None)
136+
sys.modules[_name] = stub
76137
STUBBED_MODULES.add(_name)
77138

78139

tests/test_llm_openai.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,15 @@ def behavior(kwargs):
175175

176176

177177
def test_malformed_json_still_attempts_repair():
178-
# Truncated/wrapped JSON (has a '{') is worth one repair round-trip.
178+
# Genuinely malformed JSON (has a '{' but isn't fixable by appending
179+
# closing brackets) is worth one repair round-trip. A truncated-but-
180+
# closable response is handled by the local fast-path and skips repair.
179181
calls = []
180182

181183
def behavior(kwargs):
182184
calls.append(kwargs)
183185
if len(calls) == 1:
184-
return iter([_chunk('{"reply": "hi"')]) # truncated -> has '{'
186+
return iter([_chunk('{"reply": hi}')]) # unquoted value -> not closeable
185187
return types.SimpleNamespace(
186188
choices=[types.SimpleNamespace(message=types.SimpleNamespace(content='{"reply":"hi"}'))]
187189
)

0 commit comments

Comments
 (0)