Files
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

202 lines
6.3 KiB
Python

"""Shared fixtures for device-broker tests.
The repo has no ``fakeredis`` dependency, so ``FakeRedis`` is a tiny
in-memory double covering only the commands ``DeviceBroker`` issues. It is
deliberately *not* faithful in several ways, so tests must not lean on them:
- blocking ops (``blpop`` / ``xread``) don't truly block — they return
immediately (or after a tiny sleep) so tests stay fast;
- TTL is not modeled — ``set(ex=)`` and ``expire`` are no-ops, so expiry must
be simulated by an explicit ``delete``;
- ``xadd`` trims to an EXACT ``maxlen`` and ignores ``approximate``, whereas
real Redis with ``approximate=True`` only trims past a slack.
One ``FakeRedis`` shared by two ``DeviceBroker`` instances models two
processes (e.g. a Celery worker and the web tier) talking through one Redis.
"""
from __future__ import annotations
import threading
import time
import pytest
from application.devices.broker import DeviceBroker
def _b(value) -> bytes:
if isinstance(value, bytes):
return value
return str(value).encode("utf-8")
def _seq_of(entry_id) -> int:
text = entry_id.decode() if isinstance(entry_id, bytes) else str(entry_id)
try:
return int(text.split("-", 1)[0])
except (ValueError, IndexError):
return 0
class FakeRedis:
"""In-memory stand-in for the redis-py commands the broker uses."""
def __init__(self) -> None:
self._lock = threading.RLock()
self.kv: dict = {}
self.lists: dict = {}
self.hashes: dict = {}
self.streams: dict = {}
self._seq = 0
# -- strings ------------------------------------------------------
def get(self, key):
with self._lock:
return self.kv.get(key)
def set(self, key, value, ex=None, nx=False):
with self._lock:
if nx and key in self.kv:
return None
self.kv[key] = _b(value)
return True
def delete(self, *keys):
removed = 0
with self._lock:
for key in keys:
for store in (self.kv, self.lists, self.hashes, self.streams):
if key in store:
del store[key]
removed += 1
return removed
def exists(self, key):
with self._lock:
present = (
key in self.kv
or key in self.hashes
or key in self.lists
or key in self.streams
)
return 1 if present else 0
def expire(self, key, ttl): # TTL is not simulated.
return True
# -- lists --------------------------------------------------------
def llen(self, key):
with self._lock:
return len(self.lists.get(key, []))
def rpush(self, key, *values):
with self._lock:
lst = self.lists.setdefault(key, [])
lst.extend(_b(v) for v in values)
return len(lst)
def blpop(self, key, timeout=0):
with self._lock:
lst = self.lists.get(key)
if lst:
value = lst.pop(0)
if not lst:
del self.lists[key]
return (_b(key), value)
return None
def lrem(self, key, count, value):
with self._lock:
lst = self.lists.get(key)
if not lst:
return 0
target = _b(value)
kept = [item for item in lst if item != target]
removed = len(lst) - len(kept)
if kept:
self.lists[key] = kept
else:
self.lists.pop(key, None)
return removed
# -- hashes -------------------------------------------------------
def hset(self, key, field=None, value=None, mapping=None):
with self._lock:
h = self.hashes.setdefault(key, {})
count = 0
if mapping:
for f, v in mapping.items():
h[f] = _b(v)
count += 1
if field is not None:
h[field] = _b(value)
count += 1
return count
def hsetnx(self, key, field, value):
with self._lock:
h = self.hashes.setdefault(key, {})
if field in h:
return 0
h[field] = _b(value)
return 1
def hget(self, key, field):
with self._lock:
return self.hashes.get(key, {}).get(field)
def hgetall(self, key):
with self._lock:
return {_b(f): v for f, v in self.hashes.get(key, {}).items()}
def hincrby(self, key, field, amount):
with self._lock:
h = self.hashes.setdefault(key, {})
current = int(h.get(field, b"0"))
current += int(amount)
h[field] = _b(str(current))
return current
# -- streams ------------------------------------------------------
def xadd(self, key, fields, maxlen=None, approximate=True):
with self._lock:
self._seq += 1
entry_id = f"{self._seq}-0"
entries = self.streams.setdefault(key, [])
entries.append((entry_id, {_b(f): _b(v) for f, v in fields.items()}))
if maxlen is not None and len(entries) > maxlen:
del entries[: len(entries) - maxlen]
return _b(entry_id)
def xread(self, streams, count=None, block=None):
if block:
time.sleep(min(block / 1000.0, 0.02))
out = []
with self._lock:
for key, last_id in streams.items():
entries = self.streams.get(key, [])
last_seq = _seq_of(last_id)
fresh = [
(eid, fields)
for (eid, fields) in entries
if _seq_of(eid) > last_seq
]
if count:
fresh = fresh[:count]
if fresh:
out.append([_b(key), [(_b(eid), f) for eid, f in fresh]])
return out or None
@pytest.fixture
def fake_redis() -> FakeRedis:
return FakeRedis()
@pytest.fixture
def broker_env(monkeypatch, fake_redis):
"""A ``DeviceBroker`` wired to a fresh ``FakeRedis``; returns (broker, fake)."""
monkeypatch.setattr(
"application.devices.broker.get_redis_instance", lambda: fake_redis
)
return DeviceBroker(), fake_redis