piker/tests/test_store_cli.py

327 lines
8.5 KiB
Python
Raw Normal View History

'''
Storage command UX regressions.
'''
from collections.abc import (
AsyncIterator,
Iterator,
)
from contextlib import asynccontextmanager
import json
from pathlib import Path
from types import SimpleNamespace
from typing import Any
import numpy as np
import pytest
from typer.testing import CliRunner
from piker import tsp
from piker import config
from piker.data import def_iohlcv_fields
from piker.storage import cli as storage_cli
from piker.storage.cli import store
def test_store_group_shows_help_without_arguments() -> None:
'''
Bare ``piker store`` must present its command map immediately.
The Typer group emitted only a missing-command error and
required another ``--help`` invocation. Invoke the group
without arguments and prove complete help replaces that error.
'''
result = CliRunner().invoke(store, [])
assert result.exit_code == 2
assert 'Usage:' in result.output
assert 'Missing command' not in result.output
for command in (
'anal',
'audit',
'delete',
'ls',
'series',
'shm',
):
assert command in result.output
def test_store_commands_show_help_without_arguments() -> None:
'''
Bare endpoints must not open runtimes or report missing args.
Every endpoint is discoverable by typing its name once. Exercise
required-input commands and prove help rendering exits before any
callback can run.
'''
runner = CliRunner()
for command in (
'anal',
'audit',
'delete',
'shm',
):
result = runner.invoke(store, [command])
assert result.exit_code == 2
assert 'Usage:' in result.output
assert 'Missing argument' not in result.output
def test_series_lists_exact_native_periods(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
Durable listing must expose separate 1s and 60s identities.
The old FQME-only index hid one timeframe. Create canonical files
without storage or actor services, invoke the JSON endpoint,
and prove both exact paths and periods are returned.
'''
nativedb: Path = tmp_path / 'nativedb'
nativedb.mkdir()
one = nativedb / 'qqq.nasdaq.ib.ohlcv1s.parquet'
sixty = nativedb / 'qqq.nasdaq.ib.ohlcv60s.parquet'
three_hundred = nativedb / (
'qqq.nasdaq.ib.ohlcv300s.parquet'
)
one.write_bytes(b'1')
sixty.write_bytes(b'60')
three_hundred.write_bytes(b'300')
monkeypatch.setattr(config, 'get_conf_dir', lambda: tmp_path)
result = CliRunner().invoke(
store,
['series', '--json'],
)
assert result.exit_code == 0
payload = json.loads(result.output)
assert [item['period_s'] for item in payload] == [1, 60, 300]
assert [item['path'] for item in payload] == [
str(one),
str(sixty),
str(three_hundred),
]
compact = CliRunner().invoke(store, ['ls'])
assert compact.exit_code == 0
assert 'qqq.nasdaq.ib' in compact.output
assert '1s, 60s, 300s' in compact.output
def test_shm_reload_requires_explicit_persistence() -> None:
'''
SHM reload must not silently opt into durable persistence.
Reload reads back the Parquet produced by the repair path, so the
reload flag alone has no valid input and must not open runtime or
storage services. Invoke that invalid combination and prove CLI
validation rejects it before inspecting an FQME.
'''
result = CliRunner().invoke(
store,
['shm', 'qqq.nasdaq.ib', '--reload-parquet-to-shm'],
)
assert result.exit_code == 2
assert 'requires --write-parquet' in result.output
@pytest.mark.parametrize(
'option',
[
['--json'],
['--max-gaps', '10'],
],
)
def test_shm_repair_rejects_read_only_formatting(
option: list[str],
) -> None:
'''
Repair mode must not silently ignore read-only output options.
Combining the old repair workflow with the new inspector made
``--json`` and an explicitly default-valued ``--max-gaps``
appeared accepted even though repair produced logs and entered an
interactive pause. Invoke each conflicting option and prove
validation stops before runtime or storage can be opened.
'''
result = CliRunner().invoke(
store,
[
'shm',
'qqq.nasdaq.ib',
'--write-parquet',
*option,
],
)
assert result.exit_code == 2
assert 'apply only to read-only output' in result.output
def test_ls_has_description_in_store_help() -> None:
'''
Compact NativeDB discovery must explain itself in group help.
The backend-oriented ``ls`` callback had no docstring, leaving
its command-map description blank. Render the parent help and
prove the compact series purpose is visible without opening the
command.
'''
result = CliRunner().invoke(store, [])
assert result.exit_code == 2
assert 'List NativeDB FQMEs' in result.output
def test_shm_endpoint_reports_immutable_snapshot(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
SHM diagnostics report exact buffers without storage mutation.
Build one RT identity and snapshot, then replace runtime
attachment with fakes. Invoke JSON output and prove it carries
generation, cadence, and invalid-slot
evidence without opening a storage client or writer.
'''
path = Path(
'piker.datad[aaaaaaaa-aaaa-aa].qqq.nasdaq.ib.rt'
)
ref = tsp.ShmBufferRef(
service='datad',
generation='aaaaaaaa-aaaa-aa',
fqme='qqq.nasdaq.ib',
kind='rt',
path=path,
)
frame = np.zeros(
4,
dtype=np.dtype(def_iohlcv_fields),
)
frame['index'] = np.arange(4)
frame['time'] = [1, 0, 3, 4]
@asynccontextmanager
async def open_runtime(
*args: Any,
**kwargs: Any,
) -> AsyncIterator[None]:
'''
Yield a runtime-free diagnostic context.
'''
yield
def iter_refs(
fqme: str|None = None,
) -> Iterator[tsp.ShmBufferRef]:
'''
Yield the selected exact SHM identity.
'''
yield ref
def iter_frames(
fqme: str,
shm_name: str|None = None,
refs: list[tsp.ShmBufferRef]|None = None,
) -> Iterator[tuple[Path, SimpleNamespace, None]]:
'''
Yield an immutable snapshot through the attachment interface.
'''
shm = SimpleNamespace(array=frame)
yield path, shm, None
def fail_storage(*args: Any, **kwargs: Any) -> None:
'''
Fail if read-only SHM inspection reaches storage.
'''
raise AssertionError('SHM diagnostics opened storage')
monkeypatch.setattr(
storage_cli,
'open_piker_runtime',
open_runtime,
)
monkeypatch.setattr(
storage_cli,
'open_storage_client',
fail_storage,
)
monkeypatch.setattr(tsp, 'iter_shm_buffer_refs', iter_refs)
monkeypatch.setattr(tsp, 'iter_dfs_from_shms', iter_frames)
result = CliRunner().invoke(
store,
['shm', 'qqq.nasdaq.ib', '--json'],
)
assert result.exit_code == 0
payload = json.loads(result.output)
assert len(payload) == 1
assert payload[0]['generation'] == 'aaaaaaaa-aaaa-aa'
assert payload[0]['period_s'] == 1
assert payload[0]['invalid_count'] == 1
def test_shm_rejects_unknown_exact_name_before_runtime(
monkeypatch: pytest.MonkeyPatch,
) -> None:
'''
A stale exact SHM selection must fail before opening services.
The old no-match path paused after opening runtime and storage
setup. Return no identity, install a runtime fail-spy,
and prove exact selection exits immediately
with a deterministic error.
'''
def iter_refs(
fqme: str|None = None,
) -> Iterator[tsp.ShmBufferRef]:
'''
Yield no matching SHM identities.
'''
return iter(())
def fail_runtime(*args: Any, **kwargs: Any) -> None:
'''
Fail if no-match handling opens a runtime.
'''
raise AssertionError('unknown SHM opened runtime')
monkeypatch.setattr(tsp, 'iter_shm_buffer_refs', iter_refs)
monkeypatch.setattr(
storage_cli,
'open_piker_runtime',
fail_runtime,
)
result = CliRunner().invoke(
store,
[
'shm',
'qqq.nasdaq.ib',
'--shm-name',
'missing',
],
)
assert result.exit_code == 2
assert 'No exact OHLCV SHM buffer' in result.output