1010from 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+
1332def 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\n second\n third\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