Skip to content

Commit 999c94e

Browse files
committed
Achieve 100% statement and branch coverage in test_render
1 parent 9e3ebb3 commit 999c94e

File tree

1 file changed

+208
-0
lines changed

1 file changed

+208
-0
lines changed

Lib/test/test_pyrepl/test_render.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
from unittest import TestCase
22
from _pyrepl.render import (
3+
LineUpdate,
4+
RenderCell,
35
RenderLine,
46
RenderedScreen,
7+
ScreenOverlay,
58
StyleRef,
69
diff_render_lines,
710
render_cells,
11+
requires_cursor_resync,
812
)
913

1014

@@ -38,6 +42,45 @@ def test_from_parts_accepts_style_refs(self):
3842
)
3943
self.assertEqual(line.text, "\x1b[1;34mdef\x1b[0m ")
4044

45+
def test_from_parts_without_styles(self):
46+
line = RenderLine.from_parts(["a", "b"], [1, 1])
47+
48+
self.assertEqual(line.text, "ab")
49+
self.assertEqual(line.width, 2)
50+
51+
def test_from_rendered_text_empty_string(self):
52+
line = RenderLine.from_rendered_text("")
53+
54+
self.assertEqual(line.cells, ())
55+
self.assertEqual(line.text, "")
56+
self.assertEqual(line.width, 0)
57+
58+
def test_from_rendered_text_with_non_sgr_controls(self):
59+
# \x1b[H is a cursor-home control (not SGR since it doesn't end with 'm')
60+
line = RenderLine.from_rendered_text("\x1b[Hx")
61+
62+
self.assertEqual(len(line.cells), 2)
63+
self.assertEqual(line.cells[0].controls, ("\x1b[H",))
64+
self.assertEqual(line.cells[0].text, "")
65+
self.assertEqual(line.cells[1].text, "x")
66+
67+
def test_from_rendered_text_trailing_non_sgr_control(self):
68+
line = RenderLine.from_rendered_text("a\x1b[K")
69+
70+
self.assertEqual(len(line.cells), 2)
71+
self.assertEqual(line.cells[0].text, "a")
72+
self.assertEqual(line.cells[1].controls, ("\x1b[K",))
73+
self.assertEqual(line.cells[1].text, "")
74+
75+
def test_from_rendered_text_non_sgr_before_text(self):
76+
# Non-SGR control immediately before text should produce a control cell
77+
# then text cells.
78+
line = RenderLine.from_rendered_text("\x1b[Kab")
79+
80+
texts = [c.text for c in line.cells]
81+
self.assertEqual(texts, ["", "a", "b"])
82+
self.assertEqual(line.cells[0].controls, ("\x1b[K",))
83+
4184

4285
class TestLineDiff(TestCase):
4386
def test_diff_render_lines_ignores_unchanged_ansi_prefix(self):
@@ -94,10 +137,175 @@ def test_keyword_space_inserts_only_space_after_reset(self):
94137
self.assertEqual(diff.start_x, 3)
95138
self.assertEqual(render_cells(diff.new_cells), " ")
96139

140+
def test_diff_render_lines_returns_none_for_identical(self):
141+
line = RenderLine.from_rendered_text("abc")
142+
self.assertIsNone(diff_render_lines(line, line))
143+
144+
def test_diff_render_lines_breaks_on_controls_in_prefix(self):
145+
old = RenderLine.from_cells([
146+
RenderCell("a", 1),
147+
RenderCell("", 0, controls=("\x1b[K",)),
148+
RenderCell("b", 1),
149+
])
150+
new = RenderLine.from_cells([
151+
RenderCell("a", 1),
152+
RenderCell("", 0, controls=("\x1b[K",)),
153+
RenderCell("c", 1),
154+
])
155+
156+
diff = diff_render_lines(old, new)
157+
158+
self.assertIsNotNone(diff)
159+
assert diff is not None
160+
# Prefix scan stops at control cell, so diff starts at cell 1
161+
self.assertEqual(diff.start_cell, 1)
162+
self.assertEqual(diff.start_x, 1)
163+
164+
def test_diff_render_lines_extends_past_combining_chars(self):
165+
# \u0301 is a combining acute accent (zero-width)
166+
old = RenderLine.from_parts(["a", "b", "\u0301"], [1, 1, 0])
167+
new = RenderLine.from_parts(["a", "c", "\u0301"], [1, 1, 0])
168+
169+
diff = diff_render_lines(old, new)
170+
171+
self.assertIsNotNone(diff)
172+
assert diff is not None
173+
# The combining char is included since it's zero-width
174+
self.assertEqual(len(diff.new_cells), 2)
175+
176+
def test_diff_old_and_new_changed_width(self):
177+
old = RenderLine.from_rendered_text("ab")
178+
new = RenderLine.from_rendered_text("acd")
179+
180+
diff = diff_render_lines(old, new)
181+
182+
self.assertIsNotNone(diff)
183+
assert diff is not None
184+
self.assertEqual(diff.old_changed_width, 1)
185+
self.assertEqual(diff.new_changed_width, 2)
186+
97187
def test_rendered_screen_round_trips_screen_lines(self):
98188
screen = RenderedScreen.from_screen_lines(
99189
["a", "\x1b[31mb\x1b[0m"],
100190
(0, 1),
101191
)
102192

103193
self.assertEqual(screen.screen_lines, ("a", "\x1b[31mb\x1b[0m"))
194+
195+
196+
class TestRenderedScreen(TestCase):
197+
def test_empty(self):
198+
screen = RenderedScreen.empty()
199+
200+
self.assertEqual(screen.lines, ())
201+
self.assertEqual(screen.cursor, (0, 0))
202+
self.assertEqual(screen.overlays, ())
203+
self.assertEqual(screen.composed_lines, ())
204+
205+
def test_with_overlay(self):
206+
screen = RenderedScreen.from_screen_lines(["aaa", "bbb"], (0, 0))
207+
overlay_line = RenderLine.from_rendered_text("xxx")
208+
result = screen.with_overlay(1, [overlay_line])
209+
210+
self.assertEqual(len(result.overlays), 1)
211+
self.assertEqual(result.composed_lines[0].text, "aaa")
212+
self.assertEqual(result.composed_lines[1].text, "xxx")
213+
214+
def test_compose_replace_overlay(self):
215+
base = RenderedScreen.from_screen_lines(["aa", "bb", "cc"], (0, 0))
216+
overlay_line = RenderLine.from_rendered_text("XX")
217+
screen = RenderedScreen(
218+
base.lines,
219+
(0, 0),
220+
(ScreenOverlay(y=1, lines=(overlay_line,)),),
221+
)
222+
223+
self.assertEqual(screen.composed_lines[0].text, "aa")
224+
self.assertEqual(screen.composed_lines[1].text, "XX")
225+
self.assertEqual(screen.composed_lines[2].text, "cc")
226+
227+
def test_compose_insert_overlay(self):
228+
base = RenderedScreen.from_screen_lines(["aa", "bb"], (0, 0))
229+
overlay_line = RenderLine.from_rendered_text("INS")
230+
screen = RenderedScreen(
231+
base.lines,
232+
(0, 0),
233+
(ScreenOverlay(y=1, lines=(overlay_line,), insert=True),),
234+
)
235+
236+
self.assertEqual(len(screen.composed_lines), 3)
237+
self.assertEqual(screen.composed_lines[0].text, "aa")
238+
self.assertEqual(screen.composed_lines[1].text, "INS")
239+
self.assertEqual(screen.composed_lines[2].text, "bb")
240+
241+
def test_compose_replace_extends_beyond_lines(self):
242+
base = RenderedScreen.from_screen_lines(["aa"], (0, 0))
243+
overlay_line = RenderLine.from_rendered_text("XX")
244+
screen = RenderedScreen(
245+
base.lines,
246+
(0, 0),
247+
(ScreenOverlay(y=1, lines=(overlay_line,)),),
248+
)
249+
250+
self.assertEqual(len(screen.composed_lines), 2)
251+
self.assertEqual(screen.composed_lines[1].text, "XX")
252+
253+
254+
class TestRenderCell(TestCase):
255+
def test_terminal_text(self):
256+
cell = RenderCell("x", 1, style=StyleRef.from_sgr("\x1b[32m"))
257+
self.assertEqual(cell.terminal_text, "\x1b[32mx\x1b[0m")
258+
259+
def test_terminal_text_plain(self):
260+
cell = RenderCell("y", 1)
261+
self.assertEqual(cell.terminal_text, "y")
262+
263+
264+
class TestRenderCells(TestCase):
265+
def test_render_cells_with_controls(self):
266+
cells = [
267+
RenderCell("", 0, controls=("\x1b[K",)),
268+
RenderCell("a", 1),
269+
]
270+
result = render_cells(cells)
271+
self.assertEqual(result, "\x1b[Ka")
272+
273+
def test_render_cells_skips_empty_text(self):
274+
cells = [
275+
RenderCell("", 0),
276+
RenderCell("a", 1),
277+
]
278+
result = render_cells(cells)
279+
self.assertEqual(result, "a")
280+
281+
def test_render_cells_with_visual_style(self):
282+
cells = [RenderCell("a", 1)]
283+
result = render_cells(cells, visual_style="\x1b[7m")
284+
self.assertEqual(result, "\x1b[7ma\x1b[0m")
285+
286+
287+
class TestLineUpdate(TestCase):
288+
def test_post_init_renders_text(self):
289+
cells = (RenderCell("a", 1), RenderCell("b", 1))
290+
update = LineUpdate(
291+
kind="insert_char",
292+
y=0,
293+
start_cell=0,
294+
start_x=0,
295+
cells=cells,
296+
)
297+
self.assertEqual(update.text, "ab")
298+
299+
300+
class TestRequiresCursorResync(TestCase):
301+
def test_no_controls(self):
302+
cells = [RenderCell("a", 1)]
303+
self.assertFalse(requires_cursor_resync(cells))
304+
305+
def test_sgr_only_does_not_require_resync(self):
306+
cells = [RenderCell("", 0, controls=("\x1b[31m",))]
307+
self.assertFalse(requires_cursor_resync(cells))
308+
309+
def test_non_sgr_requires_resync(self):
310+
cells = [RenderCell("", 0, controls=("\x1b[H",))]
311+
self.assertTrue(requires_cursor_resync(cells))

0 commit comments

Comments
 (0)