"""Tests for repeated -r ranges: unit window intersection, plain item slicing.""" import asyncio import json from datetime import UTC, datetime from kanta.serialization import JsonSerializer from kanta.structs import META_FORMAT, ChangeRecord, Snapshot from tests.support import Data, make_kanta # The CLI reads files with its own JSON Kanta instance unless -k is given. JSON = ("json", JsonSerializer) T0 = datetime(2026, 1, 1, tzinfo=UTC) def _run_cli(path, *args): from kanta.__main__ import main return main([str(path), *args]) def _build(path, increments=3): async def build(): kanta = make_kanta(path, Data, JSON) await kanta.open(log=False) for i in range(increments): with kanta.transaction(f"inc{i}", log=False) as data: data.counter += 1 await kanta.close() asyncio.run(build()) def test_plain_range_restricts_dump_to_last_printed(tmp_path, capsys): """The JSON dump holds the state after the last printed item.""" path = tmp_path / "data.kantadb" _build(path) capsys.readouterr() # Items are bootstrap, inc0, inc1, inc2: 1:3 prints inc0 and inc1. assert _run_cli(path, "-r", "1:3", "-o", "-") == 0 out = capsys.readouterr() assert "bootstrap" not in out.err assert "inc0" in out.err assert "inc1" in out.err assert "inc2" not in out.err # The dump is the state after inc1, the last printed record. assert json.loads(out.out) == {"users": {}, "counter": 2} # Inclusive end. assert _run_cli(path, "-r", "0..1") == 0 err = capsys.readouterr().err assert "bootstrap" in err assert "inc0" in err assert "inc1" not in err def test_plain_single_index(tmp_path, capsys): path = tmp_path / "data.kantadb" _build(path) capsys.readouterr() assert _run_cli(path, "-r", "0") == 0 err = capsys.readouterr().err assert "bootstrap" in err assert "inc0" not in err # Negative index counts from the end of the filtered output. assert _run_cli(path, "-r", "-1") == 0 err = capsys.readouterr().err assert "inc2" in err assert "inc1" not in err # A single index must exist; out of range is exit 2. assert _run_cli(path, "-r", "99") == 2 assert "99" in capsys.readouterr().err def test_plain_ranges_intersect(tmp_path, capsys): path = tmp_path / "data.kantadb" _build(path) capsys.readouterr() # [1:) and [:3) intersect to items 1 and 2. assert _run_cli(path, "-r", "1:", "-r", ":3") == 0 err = capsys.readouterr().err assert "bootstrap" not in err assert "inc0" in err assert "inc1" in err assert "inc2" not in err # An empty intersection prints nothing, exit 0 (truncation, not error). assert _run_cli(path, "-r", ":1", "-r", "3:") == 0 err = capsys.readouterr().err assert "inc" not in err assert "bootstrap" not in err def test_plain_applies_after_grep(tmp_path, capsys): """Plain numbers index into what survives --grep, order-independently.""" path = tmp_path / "data.kantadb" _build(path) capsys.readouterr() # Grep leaves inc0..inc2; plain 0 picks the first of those. assert _run_cli(path, "--grep", "inc", "-r", "0") == 0 first = capsys.readouterr().err assert "inc0" in first assert "inc1" not in first assert "bootstrap" not in first assert _run_cli(path, "-r", "0", "--grep", "inc") == 0 assert capsys.readouterr().err == first # Negative index from the filtered end. assert _run_cli(path, "--grep", "inc", "-r", "-1") == 0 err = capsys.readouterr().err assert "inc2" in err assert "inc1" not in err def test_unit_specs_intersect(tmp_path, capsys): path = tmp_path / "data.kantadb" _build(path) capsys.readouterr() # r1:r4 = bootstrap..inc1, r2:r5 = inc0..inc2; intersection is inc0, inc1. assert _run_cli(path, "-r", "r1:r4", "-r", "r2:r5", "-o", "-") == 0 out = capsys.readouterr() assert "bootstrap" not in out.err assert "inc0" in out.err assert "inc1" in out.err assert "inc2" not in out.err # The replay end is the intersected window end (state after inc1). assert json.loads(out.out) == {"users": {}, "counter": 2} def test_unit_and_plain_combine(tmp_path, capsys): """Unit window first, then plain slicing over the window's items.""" path = tmp_path / "data.kantadb" _build(path) capsys.readouterr() # Window r2:r4 yields inc0, inc1; plain item 0 prints only inc0. assert _run_cli(path, "-r", "r2:r4", "-r", "0", "-o", "-") == 0 out = capsys.readouterr() assert "inc0" in out.err assert "inc1" not in out.err # The dump is the state after the last printed item (inc0). assert json.loads(out.out) == {"users": {}, "counter": 1} def test_context_expands_after_grep(tmp_path, capsys): """Context records come from the whole file, free of --grep.""" path = tmp_path / "data.kantadb" _build(path, increments=5) capsys.readouterr() assert _run_cli(path, "--grep", "inc2", "--context", "1") == 0 err = capsys.readouterr().err assert "inc1" in err assert "inc2" in err assert "inc3" in err assert "inc0" not in err assert "inc4" not in err assert "bootstrap" not in err # Context 0 (default) changes nothing. assert _run_cli(path, "--grep", "inc2") == 0 err = capsys.readouterr().err assert "inc1" not in err assert "inc2" in err # Context is clamped at the start of the file. assert _run_cli(path, "--grep", "bootstrap", "--context", "2") == 0 err = capsys.readouterr().err assert "bootstrap" in err assert "inc0" in err assert "inc1" in err assert "inc2" not in err def test_context_expands_past_unit_window(tmp_path, capsys): """Context expands whatever a unit selection would show, past the window.""" path = tmp_path / "data.kantadb" _build(path) capsys.readouterr() # -r r2 alone shows only inc0 (the record producing state 2); context 1 # expands past the window to bootstrap and inc1, and the dump follows # the last printed record. assert _run_cli(path, "-r", "r2", "--context", "1", "-o", "-") == 0 out = capsys.readouterr() assert "bootstrap" in out.err assert "inc0" in out.err assert "inc1" in out.err assert "inc2" not in out.err assert json.loads(out.out) == {"users": {}, "counter": 2} def test_context_expands_snapshot_selection(tmp_path, capsys): """-r sN --context N shows the snapshot plus its neighbors.""" path = tmp_path / "data.kantadb" serializer = JsonSerializer() framer = serializer.framer_cls() out_bytes = bytearray() out_bytes += framer.frame_change( serializer.encode(ChangeRecord(ts=T0, a="inc0", v=0, diff={"counter": 1})), record_offset=len(out_bytes), ) snap = Snapshot(ts=T0, v=0, state={"counter": 1}, r=1, k=META_FORMAT) out_bytes += framer.frame_snapshot( serializer.encode(snap), record_offset=len(out_bytes) ) out_bytes += framer.frame_change( serializer.encode(ChangeRecord(ts=T0, a="inc1", v=0, diff={"counter": 2})), record_offset=len(out_bytes), ) path.write_bytes(bytes(out_bytes)) capsys.readouterr() # Without context, s1 (the file starts with a change, so s0 is l0) # shows just the snapshot and its state. assert _run_cli(path, "-r", "s1", "-o", "-") == 0 out = capsys.readouterr() assert "snapshot s1" in out.err assert "inc0" not in out.err assert json.loads(out.out) == {"counter": 1} # With context, the selection expands past the snapshot line. assert _run_cli(path, "-r", "s1", "--context", "1", "-o", "-") == 0 out = capsys.readouterr() assert "inc0" in out.err assert "snapshot s1" in out.err assert "inc1" in out.err assert json.loads(out.out) == {"counter": 2} def test_context_merges_overlaps(tmp_path, capsys): """Nearby matches share context instead of duplicating records.""" path = tmp_path / "data.kantadb" _build(path, increments=5) capsys.readouterr() # Grep leaves inc0..inc4; plain 1:3 selects inc1, inc2; context 1 # expands to inc0..inc3, each printed exactly once (overlaps merged). assert _run_cli(path, "--grep", "inc", "-r", "1:3", "--context", "1") == 0 err = capsys.readouterr().err for action in ["inc0", "inc1", "inc2", "inc3"]: assert err.count(action) == 1 assert "bootstrap" not in err assert "inc4" not in err def test_grep_and_context_shift_dump_to_last_printed(tmp_path, capsys): """Grep and --context are display filters, but the dump follows the last printed record, so they move it too.""" path = tmp_path / "data.kantadb" _build(path) capsys.readouterr() # Only inc1 prints; the dump is the state after inc1. assert _run_cli(path, "--grep", "inc1", "-o", "-") == 0 out = capsys.readouterr() assert "inc0" not in out.err assert json.loads(out.out) == {"users": {}, "counter": 2} # Context extends the printout past the match; the dump follows it. assert _run_cli(path, "--grep", "inc1", "--context", "1", "-o", "-") == 0 out = capsys.readouterr() assert "inc2" in out.err assert json.loads(out.out) == {"users": {}, "counter": 3} # Nothing printed: the dump falls back to the range-end state. assert _run_cli(path, "--grep", "zzz", "-o", "-") == 0 out = capsys.readouterr() assert json.loads(out.out) == {"users": {}, "counter": 3}