"""Tests for repeated -r ranges: unit window intersection, plain item slicing.""" import asyncio import json from kanta.serialization import JsonSerializer from tests.support import Data, make_kanta # The CLI reads files with its own JSON Kanta instance unless -k is given. JSON = ("json", JsonSerializer) 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_filters_output_not_state(tmp_path, capsys): """Plain numbers slice the printed items; -o state is unaffected.""" 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 # Display filter only: the output state is the full replayed state. assert json.loads(out.out) == {"users": {}, "counter": 3} # 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 plain spec does not narrow the output state. assert json.loads(out.out) == {"users": {}, "counter": 2} def test_context_expands_after_grep(tmp_path, capsys): """Context records come from the in-window pool, 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 range. 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_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