Files
kanta/tests/test_range.py
T
LeoVasanko b8d2222905 Repeatable -r: plain numbers slice filtered output, unit windows intersect; add --context
-r is now repeatable and restrictive: unit specs (l/s/v/r) intersect
their database line windows, while plain-number specs index into the
filtered output items left after unit windows and --grep, acting as a
pure display filter (negatives from the end, single index errors with
exit 2, ranges truncate).  --context N (default 0) then expands each
surviving item GNU grep-style by N surrounding in-window records, free
of the grep/plain filters, merging overlaps.
2026-09-28 01:05:12 +00:00

194 lines
6.1 KiB
Python

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