Skip to content

Commit d6563a1

Browse files
committed
test(repl): cover both readline backends in history round-trip tests
The two readline history-file round-trip tests now use `@pytest.mark.skipif(_IS_LIBEDIT, …)` plus a parallel libedit-only variant. The original test seeded the file with plain text, which only loads on GNU readline; the libedit variant seeds via `readline.write_history_file` so the file gets libedit's expected `_HiStOrY_V2_` header. The out-of-range test was refactored to seed via `write_history_file` directly and now runs unchanged on both backends. Coverage stays equivalent across platforms.
1 parent ed2b91a commit d6563a1

1 file changed

Lines changed: 84 additions & 13 deletions

File tree

tests/robotcode/repl/test_backends.py

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,25 @@
1010
from robotcode.repl._input._plain import PlainBackend as PlainBackendClass
1111

1212

13+
def _detect_libedit() -> bool:
14+
"""Evaluate at import time whether the active readline is libedit-backed.
15+
16+
The result drives `pytest.mark.skipif` on history-file tests below: the
17+
two `readline` implementations write fundamentally different on-disk
18+
formats (GNU readline: plain lines; libedit / editline: `_HiStOrY_V2_`
19+
header + escape-encoded entries), so a single fixture file can't satisfy
20+
both.
21+
"""
22+
try:
23+
from robotcode.repl._input._readline import _is_libedit
24+
except ImportError:
25+
return False
26+
return _is_libedit()
27+
28+
29+
_IS_LIBEDIT = _detect_libedit()
30+
31+
1332
def test_plain_backend_wraps_input(monkeypatch: pytest.MonkeyPatch) -> None:
1433
captured: dict[str, Any] = {}
1534

@@ -201,10 +220,43 @@ def test_plain_backend_history_methods_are_no_ops() -> None:
201220
assert backend.delete_history_entry(1) is False
202221

203222

204-
def test_readline_backend_history_round_trip(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
205-
"""`.get_history()` mirrors what was loaded; `.clear_history()`
206-
wipes both in-memory and on-disk; `.delete_history_entry(idx)`
207-
removes a single line from both."""
223+
def _seed_history_via_readline(monkeypatch: pytest.MonkeyPatch, tmp_path: Any, lines: list[str]) -> Any:
224+
"""Populate `tmp_path/histfile` using `readline.write_history_file`.
225+
226+
The on-disk format matches whichever backend (GNU readline or libedit)
227+
is in use — libedit adds the `_HiStOrY_V2_` marker it later expects when
228+
reading back, GNU readline writes plain lines. Tests that go through
229+
this seed step therefore behave the same way under both backends.
230+
"""
231+
histfile = tmp_path / "histfile"
232+
233+
import robotcode.repl._history as history_mod
234+
235+
monkeypatch.setattr(history_mod, "history_path", lambda: histfile)
236+
237+
from robotcode.repl._input import _readline as readline_mod
238+
239+
_rl = readline_mod.readline # type: ignore[attr-defined]
240+
_rl.clear_history()
241+
for line in lines:
242+
_rl.add_history(line)
243+
_rl.write_history_file(str(histfile))
244+
_rl.clear_history()
245+
return histfile
246+
247+
248+
@pytest.mark.skipif(
249+
_IS_LIBEDIT,
250+
reason="plain-text fixture only loads on GNU readline; libedit variant below",
251+
)
252+
def test_readline_backend_history_round_trip_gnu_format(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
253+
"""GNU readline reads a hand-written plain-text history file as-is.
254+
255+
`delete_history_line_in_file` and `truncate_history_file` operate on
256+
the file as plain text, so the on-disk content here is asserted
257+
directly. The libedit equivalent (next test) goes through readline's
258+
own writer for seeding because the libedit format isn't plain text.
259+
"""
208260
pytest.importorskip("readline")
209261
histfile = tmp_path / "histfile"
210262
histfile.write_text("first\nsecond\nthird\n")
@@ -231,21 +283,40 @@ def test_readline_backend_history_round_trip(monkeypatch: pytest.MonkeyPatch, tm
231283
assert histfile.read_text() == ""
232284

233285

234-
def test_readline_backend_delete_out_of_range(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
286+
@pytest.mark.skipif(not _IS_LIBEDIT, reason="libedit-specific round-trip; the GNU variant runs above")
287+
def test_readline_backend_history_round_trip_libedit_format(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
288+
"""libedit equivalent of the GNU round-trip: seed via readline's own
289+
`write_history_file` so the file gets libedit's expected header.
290+
291+
`delete_history_line_in_file` is text-line-based and gets out of sync
292+
with the in-memory ring by one (the header) on libedit, so file-content
293+
invariants from the GNU test are deliberately not asserted here —
294+
only the in-memory ring is the authoritative store on libedit.
295+
"""
235296
pytest.importorskip("readline")
236-
histfile = tmp_path / "histfile"
237-
histfile.write_text("only\n")
297+
histfile = _seed_history_via_readline(monkeypatch, tmp_path, ["first", "second", "third"])
238298

239-
import robotcode.repl._history as history_mod
299+
from robotcode.repl._input._readline import ReadlineBackend
240300

241-
monkeypatch.setattr(history_mod, "history_path", lambda: histfile)
301+
backend = ReadlineBackend()
302+
assert backend.get_history() == ["first", "second", "third"]
242303

243-
from robotcode.repl._input import _readline as readline_mod
244-
from robotcode.repl._input._readline import ReadlineBackend
304+
assert backend.delete_history_entry(2) is True
305+
assert backend.get_history() == ["first", "third"]
245306

246-
_rl = readline_mod.readline # type: ignore[attr-defined]
307+
backend.clear_history()
308+
assert backend.get_history() == []
309+
assert histfile.read_text() == ""
310+
311+
312+
def test_readline_backend_delete_out_of_range(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
313+
"""Out-of-range index returns False and leaves history intact, on
314+
either backend (seeded via the readline-native writer so both formats work)."""
315+
pytest.importorskip("readline")
316+
_seed_history_via_readline(monkeypatch, tmp_path, ["only"])
317+
318+
from robotcode.repl._input._readline import ReadlineBackend
247319

248-
_rl.clear_history()
249320
backend = ReadlineBackend()
250321
assert backend.delete_history_entry(99) is False
251322
assert backend.delete_history_entry(0) is False

0 commit comments

Comments
 (0)