bit-backtest-lab / tests /test_extension.py
Bit-Trading-Company's picture
Backtest Lab v1.0.0
46f1a78 verified
Raw
History Blame Contribute Delete
11.5 kB
"""Phase 4 acceptance: extend flow, dedup, quota fallback, manifest atomicity.
Auth and GPU inference are both mocked, so these run offline and deterministically.
"""
from __future__ import annotations
import sys
import threading
from dataclasses import dataclass
from pathlib import Path
import numpy as np
import pandas as pd
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from src import config, extension, runtime
from src.extension import ExtensionError, add_model, estimate, extend_coverage
from src.store import SignalStore
@dataclass
class FakeProfile:
username: str
def ohlcv(n=900, start="2023-01-01", freq="D"):
rng = np.random.default_rng(4)
close = pd.Series(100 * np.exp(np.cumsum(rng.normal(0.0004, 0.02, n))))
ts = pd.date_range(start, periods=n, freq=freq, tz="UTC")
return pd.DataFrame({
"ts": ts, "open": close * 0.999, "high": close * 1.02,
"low": close * 0.98, "close": close, "volume": 1000.0, "source": "test",
})
@pytest.fixture
def wired(tmp_path, monkeypatch):
"""A fresh offline store wired into runtime, with GPU inference mocked."""
store = SignalStore(repo_id=None, local_root=tmp_path / "store", offline=True)
store.write_prices("BTC-USD", "1d", ohlcv())
monkeypatch.setattr(runtime, "_store", store, raising=False)
monkeypatch.setattr(runtime, "get_store", lambda: store)
monkeypatch.setattr(extension, "HAS_SPACES", True)
runtime.cache_clear()
calls = {"n": 0}
def fake_inference(model_id, family, values, ctx_len):
calls["n"] += 1
arr = np.asarray(values, dtype="float64")
last = arr[:, -1]
return {
"q10": (last * 0.97).tolist(), "q50": last.tolist(),
"q90": (last * 1.03).tolist(), "context_len": int(ctx_len),
"revision": "deadbeef", "inference_version": "1.0.0+test.deadbeef",
}
monkeypatch.setattr(extension, "run_inference", fake_inference)
return store, calls
# --------------------------------------------------------------------------
# Auth gating
# --------------------------------------------------------------------------
def test_anonymous_users_cannot_extend(wired):
html, _ = extension.extend_ui("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01", profile=None)
assert "Sign in with Hugging Face" in html
assert "spends your own GPU quota" in html
def test_anonymous_users_cannot_add_models(wired):
html, _ = extension.add_model_ui("chronos", "amazon/chronos-bolt-small", profile=None)
assert "Sign in" in html
def test_signed_in_user_can_extend(wired):
store, calls = wired
html, cov = extension.extend_ui("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01",
profile=FakeProfile("alice"))
assert "Coverage extended by <b>@alice</b>" in html
assert calls["n"] == 1
assert not cov.empty
def test_contribution_is_attributed_to_the_user(wired):
store, _ = wired
extend_coverage("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01", username="bob")
entries = store.load_manifest().find_signals(model_slug="chronos-bolt-small")
assert entries and entries[0].contributed_by == "bob"
# --------------------------------------------------------------------------
# Dedup — never recompute covered ranges
# --------------------------------------------------------------------------
def test_second_identical_request_recomputes_nothing(wired):
store, calls = wired
first = extend_coverage("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01", username="alice")
assert "Coverage extended" in first
assert calls["n"] == 1
second = extend_coverage("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01", username="alice")
assert "already covered" in second
assert calls["n"] == 1, "inference ran again for an already-covered range"
def test_estimate_reports_already_covered(wired):
extend_coverage("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01", username="alice")
est = estimate("chronos-bolt-small", "BTC-USD", "1d", "2024-02-01", "2024-05-01")
assert est.already_covered
def test_estimate_counts_steps_for_an_uncovered_range(wired):
est = estimate("chronos-bolt-small", "BTC-USD", "1d", "2024-01-01", "2024-06-01")
assert est.steps > 0 and not est.already_covered
# --------------------------------------------------------------------------
# Guardrails
# --------------------------------------------------------------------------
def test_range_is_capped_per_timeframe(wired):
est = estimate("chronos-bolt-small", "BTC-USD", "1d", "2015-01-01", "2025-01-01")
assert est.capped
assert (est.end - est.start).days <= config.CAPS.max_days["1d"]
assert "cap" in est.note
def test_steps_never_exceed_the_per_run_ceiling(wired):
est = estimate("chronos-bolt-small", "BTC-USD", "1d", "2023-01-01", "2025-01-01")
assert est.steps <= config.CAPS.max_steps_per_run
@pytest.mark.parametrize("bad", [
("nope-model", "BTC-USD", "1d"),
("chronos-bolt-small", "DOGE-USD", "1d"),
("chronos-bolt-small", "BTC-USD", "3y"),
])
def test_unknown_selections_are_rejected(wired, bad):
with pytest.raises(ExtensionError):
estimate(bad[0], bad[1], bad[2], "2024-01-01", "2024-06-01")
def test_inverted_range_is_rejected(wired):
with pytest.raises(ExtensionError, match="start must be before end"):
estimate("chronos-bolt-small", "BTC-USD", "1d", "2024-06-01", "2024-01-01")
def test_extension_requires_existing_price_coverage(wired):
with pytest.raises(ExtensionError, match="No cached prices"):
estimate("chronos-bolt-small", "ETH-USD", "1d", "2024-01-01", "2024-06-01")
def test_add_model_rejects_hostile_ids(wired):
for bad in ("../../etc/passwd", "no-slash", "owner/name;rm -rf /"):
html = add_model("chronos", bad, username="alice")
assert "not a valid Hub model id" in html or "not allowed" in html
def test_add_model_rejects_families_off_the_allow_list(wired):
html = add_model("evil", "someone/backdoor", username="alice")
assert "not on the allow-list" in html
def test_add_model_smoke_test_registers_on_success(wired):
store, calls = wired
html = add_model("chronos", "amazon/chronos-bolt-small", username="carol")
assert "Smoke test passed" in html
assert calls["n"] == 1
slugs = {e.model_slug for e in store.load_manifest().signals.values()}
assert "chronos-bolt-small" in slugs
# --------------------------------------------------------------------------
# Quota exhaustion
# --------------------------------------------------------------------------
def test_quota_exhaustion_renders_the_duplicate_space_fallback(wired, monkeypatch):
def boom(*a, **k):
raise RuntimeError("ZeroGPU quota exceeded for this account")
monkeypatch.setattr(extension, "run_inference", boom)
html = extend_coverage("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01", username="alice")
assert "quota is exhausted" in html
assert "duplicate=true" in html
@pytest.mark.parametrize("message", [
"GPU task aborted", "ZeroGPU quota exceeded", "No GPU available right now",
])
def test_quota_errors_are_recognised(message):
assert extension._is_quota_error(RuntimeError(message))
def test_ordinary_errors_are_not_mistaken_for_quota_errors(wired, monkeypatch):
monkeypatch.setattr(extension, "run_inference",
lambda *a, **k: (_ for _ in ()).throw(ValueError("bad tensor shape")))
html = extend_coverage("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01", username="alice")
assert "quota" not in html.lower()
assert "bad tensor shape" in html
# --------------------------------------------------------------------------
# Manifest atomicity under concurrent writers
# --------------------------------------------------------------------------
def test_parallel_extensions_do_not_corrupt_the_manifest(wired):
"""Simulated parallel writers must leave a valid, complete manifest."""
store, calls = wired
store.write_prices("ETH-USD", "1d", ohlcv())
store.write_prices("SOL-USD", "1d", ohlcv())
targets = [("BTC-USD", "alice"), ("ETH-USD", "bob"), ("SOL-USD", "carol")]
errors: list[Exception] = []
barrier = threading.Barrier(len(targets))
def worker(asset, user):
try:
barrier.wait(timeout=10) # maximise overlap
extend_coverage("chronos-bolt-small", asset, "1d",
"2024-01-01", "2024-06-01", username=user)
except Exception as e:
errors.append(e)
threads = [threading.Thread(target=worker, args=t) for t in targets]
for t in threads:
t.start()
for t in threads:
t.join(timeout=60)
assert not errors, f"concurrent extensions raised: {errors}"
# Manifest must still validate and hold every contribution.
m = store.load_manifest(force=True)
m.validate()
assets = {e.asset for e in m.signals.values()}
assert assets == {"BTC-USD", "ETH-USD", "SOL-USD"}
contributors = {e.contributed_by for e in m.signals.values()}
assert contributors == {"alice", "bob", "carol"}
# And every referenced slice must actually exist and be readable.
for e in m.signals.values():
df = store.get_signals(e.model_slug, e.asset, e.timeframe)
assert not df.empty, f"manifest references an empty slice for {e.key}"
def test_repeated_parallel_requests_for_the_same_slice_run_inference_once(wired):
store, calls = wired
barrier = threading.Barrier(4)
def worker():
barrier.wait(timeout=10)
extend_coverage("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01", username="alice")
threads = [threading.Thread(target=worker) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join(timeout=60)
m = store.load_manifest(force=True)
m.validate()
entries = [e for e in m.signals.values() if e.asset == "BTC-USD"]
assert len(entries) == 1, "duplicate manifest entries for the same slice"
def test_write_lock_serialises_commits():
assert isinstance(extension._WRITE_LOCK, type(threading.Lock()))
# --------------------------------------------------------------------------
# CPU-only degradation
# --------------------------------------------------------------------------
def test_cpu_only_space_disables_extension_with_an_explanation(wired, monkeypatch):
monkeypatch.setattr(extension, "HAS_SPACES", False)
html, _ = extension.extend_ui("chronos-bolt-small", "BTC-USD", "1d",
"2024-01-01", "2024-06-01",
profile=FakeProfile("alice"))
assert "running on CPU" in html
assert "works normally" in html
def test_status_html_states_which_mode_the_space_is_in(monkeypatch):
monkeypatch.setattr(extension, "HAS_SPACES", True)
assert "ZEROGPU AVAILABLE" in extension.status_html()
monkeypatch.setattr(extension, "HAS_SPACES", False)
assert "CPU" in extension.status_html()