''' 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