236 lines
6.0 KiB
Python
236 lines
6.0 KiB
Python
'''
|
|
Shared-memory persistence regressions.
|
|
|
|
'''
|
|
from collections.abc import (
|
|
AsyncIterator,
|
|
Iterator,
|
|
)
|
|
from contextlib import asynccontextmanager
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from typing import Any
|
|
|
|
import numpy as np
|
|
import polars as pl
|
|
import pytest
|
|
|
|
from piker import tsp
|
|
from piker.storage import cli as storage_cli
|
|
from piker.storage.cli import (
|
|
_shm_period_and_invalid_count,
|
|
ldshm,
|
|
)
|
|
|
|
|
|
def test_ldshm_detects_nulls_without_skewing_period() -> None:
|
|
'''
|
|
Null SHM slots must stop persistence without skewing cadence.
|
|
|
|
A live QQQ buffer contained an interior run of zero-time rows.
|
|
``ldshm`` included them in period inference and then sent them to
|
|
NativeDB, which correctly rejected the replacement. Build a
|
|
60-second series with the same interior hole and enough
|
|
bars around the gap. Prove inspection counts every null while
|
|
inferring the published series' 60-second period; the
|
|
command uses that count to skip both persistence and SHM reload.
|
|
|
|
'''
|
|
times = np.array([
|
|
60,
|
|
120,
|
|
0,
|
|
0,
|
|
300,
|
|
360,
|
|
420,
|
|
])
|
|
|
|
period_s, invalid_count = _shm_period_and_invalid_count(times)
|
|
|
|
assert period_s == 60
|
|
assert invalid_count == 2
|
|
|
|
|
|
def test_ldshm_period_is_an_observed_step() -> None:
|
|
'''
|
|
Sparse timestamps must not invent a storage timeframe.
|
|
|
|
The arithmetic median of two steps can produce a cadence that
|
|
occurs nowhere in the source series. Such a value can select the
|
|
wrong NativeDB timeframe and hide the gap. Arrange tied 60 and
|
|
120-second steps and prove the deterministic tie-break chooses
|
|
the smaller observed period.
|
|
|
|
'''
|
|
times = np.array([60, 120, 240])
|
|
|
|
period_s, invalid_count = _shm_period_and_invalid_count(times)
|
|
|
|
assert period_s == 60
|
|
assert invalid_count == 0
|
|
|
|
|
|
def test_ldshm_counts_every_invalid_timestamp() -> None:
|
|
'''
|
|
Corrupt timestamps stop persistence just like null SHM slots.
|
|
|
|
Exact zero denotes an unpublished slot, while negative, NaN, and
|
|
infinite values indicate corruption. NativeDB rejects them all,
|
|
but allowing a replacement attempt would still abort ``ldshm``.
|
|
Mix each invalid class into a regular series and prove the same
|
|
snapshot inspection counts all four while preserving cadence.
|
|
|
|
'''
|
|
times = np.array([
|
|
60,
|
|
120,
|
|
0,
|
|
-1,
|
|
np.nan,
|
|
np.inf,
|
|
180,
|
|
])
|
|
|
|
period_s, invalid_count = _shm_period_and_invalid_count(times)
|
|
|
|
assert period_s == 60
|
|
assert invalid_count == 4
|
|
|
|
|
|
def test_ldshm_invalid_snapshot_never_reaches_storage(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
) -> None:
|
|
'''
|
|
Invalid snapshots stop before analyzers and storage mutation.
|
|
|
|
The original QQQ failure passed zero-time rows through dedupe and
|
|
into NativeDB. An intermediate fix called null-segment analysis
|
|
before its guard, allowing corrupt indexes to abort the command.
|
|
Supply one immutable Polars snapshot containing that combination,
|
|
then replace dedupe and writes with failing spies. Completing the
|
|
command proves its guard runs first, preventing persistence or
|
|
SHM reload without relying on task timing.
|
|
|
|
'''
|
|
shm_df = pl.DataFrame({
|
|
'index': [100, 500, 104],
|
|
'time': [60, 0, 180],
|
|
})
|
|
|
|
@asynccontextmanager
|
|
async def open_runtime(
|
|
*args: Any,
|
|
**kwargs: Any,
|
|
) -> AsyncIterator[None]:
|
|
'''
|
|
Yield a runtime-free command context.
|
|
|
|
'''
|
|
yield
|
|
|
|
async def fail_write(
|
|
*args: Any,
|
|
**kwargs: Any,
|
|
) -> None:
|
|
'''
|
|
Fail if invalid SHM reaches NativeDB.
|
|
|
|
'''
|
|
raise AssertionError('invalid SHM reached NativeDB')
|
|
|
|
client = SimpleNamespace(write_ohlcv=fail_write)
|
|
|
|
@asynccontextmanager
|
|
async def open_storage(
|
|
*args: Any,
|
|
**kwargs: Any,
|
|
) -> AsyncIterator[tuple[SimpleNamespace, SimpleNamespace]]:
|
|
'''
|
|
Yield storage objects with a failing writer.
|
|
|
|
'''
|
|
yield SimpleNamespace(), client
|
|
|
|
@asynccontextmanager
|
|
async def open_annotations(
|
|
*args: Any,
|
|
**kwargs: Any,
|
|
) -> AsyncIterator[SimpleNamespace]:
|
|
'''
|
|
Yield an inert annotation controller.
|
|
|
|
'''
|
|
yield SimpleNamespace()
|
|
|
|
def iter_shms(
|
|
fqme: str,
|
|
) -> Iterator[tuple[Path, SimpleNamespace, pl.DataFrame]]:
|
|
'''
|
|
Yield the single captured invalid snapshot.
|
|
|
|
'''
|
|
yield Path('qqq.rt'), SimpleNamespace(), shm_df
|
|
|
|
def dedupe(*args: Any, **kwargs: Any) -> None:
|
|
'''
|
|
Fail if invalid SHM reaches deduplication.
|
|
|
|
'''
|
|
raise AssertionError('invalid SHM reached dedupe')
|
|
|
|
async def pause() -> None:
|
|
'''
|
|
Replace the command's final interactive pause.
|
|
|
|
'''
|
|
|
|
monkeypatch.setattr(
|
|
storage_cli,
|
|
'open_piker_runtime',
|
|
open_runtime,
|
|
)
|
|
monkeypatch.setattr(
|
|
storage_cli,
|
|
'open_storage_client',
|
|
open_storage,
|
|
)
|
|
monkeypatch.setattr(tsp, 'iter_dfs_from_shms', iter_shms)
|
|
monkeypatch.setattr(tsp, 'dedupe_ohlcv_smart', dedupe)
|
|
monkeypatch.setattr(storage_cli.tractor, 'pause', pause)
|
|
|
|
from piker.ui import _remote_ctl
|
|
monkeypatch.setattr(
|
|
_remote_ctl,
|
|
'open_annot_ctl',
|
|
open_annotations,
|
|
)
|
|
|
|
ldshm('qqq.nasdaq.ib')
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
'times',
|
|
[
|
|
np.array([0, 0]),
|
|
np.array([60]),
|
|
np.array([60, 60]),
|
|
],
|
|
)
|
|
def test_ldshm_skips_frame_without_positive_cadence(
|
|
times: np.ndarray,
|
|
) -> None:
|
|
'''
|
|
Degenerate buffers are skipped before indexing or persistence.
|
|
|
|
The old inference indexed final timestamps and left its median
|
|
undefined for short buffers. Fully unpublished, one-row, and
|
|
duplicate-only frames could therefore fail before the command's
|
|
guard. Exercise each shape and prove inspection
|
|
returns no cadence, which directs ``ldshm`` to skip the buffer.
|
|
|
|
'''
|
|
period_s, _ = _shm_period_and_invalid_count(times)
|
|
|
|
assert period_s is None
|