xref: /aosp_15_r20/external/tensorflow/tensorflow/python/debug/cli/curses_ui_test.py (revision b6fb3261f9314811a0f4371741dbb8839866f948)
1# Copyright 2016 The TensorFlow Authors. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ==============================================================================
15"""Tests of the curses-based CLI."""
16import argparse
17import curses
18import os
19import queue
20import tempfile
21import threading
22
23import numpy as np
24
25from tensorflow.python.debug.cli import cli_config
26from tensorflow.python.debug.cli import cli_test_utils
27from tensorflow.python.debug.cli import curses_ui
28from tensorflow.python.debug.cli import debugger_cli_common
29from tensorflow.python.debug.cli import tensor_format
30from tensorflow.python.framework import test_util
31from tensorflow.python.platform import gfile
32from tensorflow.python.platform import googletest
33
34
35def string_to_codes(cmd):
36  return [ord(c) for c in cmd]
37
38
39def codes_to_string(cmd_code):
40  # Omit non-ASCII key codes.
41  return "".join(chr(code) for code in cmd_code if code < 256)
42
43
44class MockCursesUI(curses_ui.CursesUI):
45  """Mock subclass of CursesUI that bypasses actual terminal manipulations."""
46
47  def __init__(self,
48               height,
49               width,
50               command_sequence=None):
51    self._height = height
52    self._width = width
53
54    self._command_sequence = command_sequence
55    self._command_counter = 0
56
57    # The mock class has no actual textbox. So use this variable to keep
58    # track of what's entered in the textbox on creation.
59    self._curr_existing_command = ""
60
61    # Observers for test.
62    # Observers of screen output.
63    self.unwrapped_outputs = []
64    self.wrapped_outputs = []
65    self.scroll_messages = []
66    self.output_array_pointer_indices = []
67
68    self.output_pad_rows = []
69
70    # Observers of command textbox.
71    self.existing_commands = []
72
73    # Observer for tab-completion candidates.
74    self.candidates_lists = []
75
76    # Observer for the main menu.
77    self.main_menu_list = []
78
79    # Observer for toast messages.
80    self.toasts = []
81
82    curses_ui.CursesUI.__init__(
83        self,
84        config=cli_config.CLIConfig(
85            config_file_path=os.path.join(tempfile.mkdtemp(), ".tfdbg_config")))
86
87    # Override the default path to the command history file to avoid test
88    # concurrency issues.
89    _, history_file_path = tempfile.mkstemp()  # safe to ignore fd
90    self._command_history_store = debugger_cli_common.CommandHistory(
91        history_file_path=history_file_path)
92
93  # Below, override the _screen_ prefixed member methods that interact with the
94  # actual terminal, so that the mock can run in a terminal-less environment.
95
96  # TODO(cais): Search for a way to have a mock terminal object that behaves
97  # like the actual terminal, so that we can test the terminal interaction
98  # parts of the CursesUI class.
99
100  def _screen_init(self):
101    pass
102
103  def _screen_refresh_size(self):
104    self._max_y = self._height
105    self._max_x = self._width
106
107  def _screen_launch(self, enable_mouse_on_start):
108    self._mouse_enabled = enable_mouse_on_start
109
110  def _screen_terminate(self):
111    pass
112
113  def _screen_refresh(self):
114    pass
115
116  def _screen_create_command_window(self):
117    pass
118
119  def _screen_create_command_textbox(self, existing_command=None):
120    """Override to insert observer of existing commands.
121
122    Used in testing of history navigation and tab completion.
123
124    Args:
125      existing_command: Command string entered to the textbox at textbox
126        creation time. Note that the textbox does not actually exist in this
127        mock subclass. This method only keeps track of and records the state.
128    """
129
130    self.existing_commands.append(existing_command)
131    self._curr_existing_command = existing_command
132
133  def _screen_new_output_pad(self, rows, cols):
134    return "mock_pad"
135
136  def _screen_add_line_to_output_pad(self, pad, row, txt, color_segments=None):
137    pass
138
139  def _screen_draw_text_line(self, row, line, attr=curses.A_NORMAL, color=None):
140    pass
141
142  def _screen_scroll_output_pad(self, pad, viewport_top, viewport_left,
143                                screen_location_top, screen_location_left,
144                                screen_location_bottom, screen_location_right):
145    pass
146
147  def _screen_get_user_command(self):
148    command = self._command_sequence[self._command_counter]
149
150    self._command_key_counter = 0
151    for c in command:
152      if c == curses.KEY_RESIZE:
153        # Special case for simulating a terminal resize event in curses.
154        self._height = command[1]
155        self._width = command[2]
156        self._on_textbox_keypress(c)
157        self._command_counter += 1
158        return ""
159      elif c == curses.KEY_MOUSE:
160        mouse_x = command[1]
161        mouse_y = command[2]
162        self._command_counter += 1
163        self._textbox_curr_terminator = c
164        return self._fetch_hyperlink_command(mouse_x, mouse_y)
165      else:
166        y = self._on_textbox_keypress(c)
167
168        self._command_key_counter += 1
169        if y == curses_ui.CursesUI.CLI_TERMINATOR_KEY:
170          break
171
172    self._command_counter += 1
173
174    # Take into account pre-existing string automatically entered on textbox
175    # creation.
176    return self._curr_existing_command + codes_to_string(command)
177
178  def _screen_getmouse(self):
179    output = (0, self._mouse_xy_sequence[self._mouse_counter][0],
180              self._mouse_xy_sequence[self._mouse_counter][1], 0,
181              curses.BUTTON1_CLICKED)
182    self._mouse_counter += 1
183    return output
184
185  def _screen_gather_textbox_str(self):
186    return codes_to_string(self._command_sequence[self._command_counter]
187                           [:self._command_key_counter])
188
189  def _scroll_output(self, direction, line_index=None):
190    """Override to observe screen output.
191
192    This method is invoked after every command that generates a new screen
193    output and after every keyboard triggered screen scrolling. Therefore
194    it is a good place to insert the observer.
195
196    Args:
197      direction: which direction to scroll.
198      line_index: (int or None) Optional line index to scroll to. See doc string
199        of the overridden method for more information.
200    """
201
202    curses_ui.CursesUI._scroll_output(self, direction, line_index=line_index)
203
204    self.unwrapped_outputs.append(self._curr_unwrapped_output)
205    self.wrapped_outputs.append(self._curr_wrapped_output)
206    self.scroll_messages.append(self._scroll_info)
207    self.output_array_pointer_indices.append(self._output_array_pointer_indices)
208    self.output_pad_rows.append(self._output_pad_row)
209
210  def _display_main_menu(self, output):
211    curses_ui.CursesUI._display_main_menu(self, output)
212
213    self.main_menu_list.append(self._main_menu)
214
215  def _screen_render_nav_bar(self):
216    pass
217
218  def _screen_render_menu_pad(self):
219    pass
220
221  def _display_candidates(self, candidates):
222    curses_ui.CursesUI._display_candidates(self, candidates)
223
224    self.candidates_lists.append(candidates)
225
226  def _toast(self, message, color=None, line_index=None):
227    curses_ui.CursesUI._toast(self, message, color=color, line_index=line_index)
228
229    self.toasts.append(message)
230
231
232class CursesTest(test_util.TensorFlowTestCase):
233
234  _EXIT = string_to_codes("exit\n")
235
236  def _babble(self, args, screen_info=None):
237    ap = argparse.ArgumentParser(
238        description="Do babble.", usage=argparse.SUPPRESS)
239    ap.add_argument(
240        "-n",
241        "--num_times",
242        dest="num_times",
243        type=int,
244        default=60,
245        help="How many times to babble")
246    ap.add_argument(
247        "-l",
248        "--line",
249        dest="line",
250        type=str,
251        default="bar",
252        help="The content of each line")
253    ap.add_argument(
254        "-k",
255        "--link",
256        dest="link",
257        action="store_true",
258        help="Create a command link on each line")
259    ap.add_argument(
260        "-m",
261        "--menu",
262        dest="menu",
263        action="store_true",
264        help="Create a menu for testing")
265
266    parsed = ap.parse_args(args)
267
268    lines = [parsed.line] * parsed.num_times
269    font_attr_segs = {}
270    if parsed.link:
271      for i in range(len(lines)):
272        font_attr_segs[i] = [(
273            0,
274            len(lines[i]),
275            debugger_cli_common.MenuItem("", "babble"),)]
276
277    annotations = {}
278    if parsed.menu:
279      menu = debugger_cli_common.Menu()
280      menu.append(
281          debugger_cli_common.MenuItem("babble again", "babble"))
282      menu.append(
283          debugger_cli_common.MenuItem("ahoy", "ahoy", enabled=False))
284      annotations[debugger_cli_common.MAIN_MENU_KEY] = menu
285
286    output = debugger_cli_common.RichTextLines(
287        lines, font_attr_segs=font_attr_segs, annotations=annotations)
288    return output
289
290  def _print_ones(self, args, screen_info=None):
291    ap = argparse.ArgumentParser(
292        description="Print all-one matrix.", usage=argparse.SUPPRESS)
293    ap.add_argument(
294        "-s",
295        "--size",
296        dest="size",
297        type=int,
298        default=3,
299        help="Size of the matrix. For example, of the value is 3, "
300        "the matrix will have shape (3, 3)")
301
302    parsed = ap.parse_args(args)
303
304    m = np.ones([parsed.size, parsed.size])
305
306    return tensor_format.format_tensor(m, "m")
307
308  def testInitialization(self):
309    ui = MockCursesUI(40, 80)
310
311    self.assertEqual(0, ui._command_pointer)
312    self.assertEqual([], ui._active_command_history)
313    self.assertEqual("", ui._pending_command)
314
315  def testCursesUiInChildThreadStartsWithoutException(self):
316    result = queue.Queue()
317    def child_thread():
318      try:
319        MockCursesUI(40, 80)
320      except ValueError as e:
321        result.put(e)
322    t = threading.Thread(target=child_thread)
323    t.start()
324    t.join()
325    self.assertTrue(result.empty())
326
327  def testRunUIExitImmediately(self):
328    """Make sure that the UI can exit properly after launch."""
329
330    ui = MockCursesUI(40, 80, command_sequence=[self._EXIT])
331    ui.run_ui()
332
333    # No screen output should have happened.
334    self.assertEqual(0, len(ui.unwrapped_outputs))
335
336  def testRunUIEmptyCommand(self):
337    """Issue an empty command then exit."""
338
339    ui = MockCursesUI(40, 80, command_sequence=[[], self._EXIT])
340    ui.run_ui()
341
342    # Empty command should not lead to any screen output.
343    self.assertEqual(0, len(ui.unwrapped_outputs))
344
345  def testRunUIInvalidCommandPrefix(self):
346    """Handle an unregistered command prefix."""
347
348    ui = MockCursesUI(
349        40,
350        80,
351        command_sequence=[string_to_codes("foo\n"), self._EXIT])
352    ui.run_ui()
353
354    # Screen output/scrolling should have happened exactly once.
355    self.assertEqual(1, len(ui.unwrapped_outputs))
356    self.assertEqual(1, len(ui.wrapped_outputs))
357    self.assertEqual(1, len(ui.scroll_messages))
358
359    self.assertEqual(["ERROR: Invalid command prefix \"foo\""],
360                     ui.unwrapped_outputs[0].lines)
361    # TODO(cais): Add explanation for the 35 extra lines.
362    self.assertEqual(["ERROR: Invalid command prefix \"foo\""],
363                     ui.wrapped_outputs[0].lines[:1])
364    # A single line of output should not have caused scrolling.
365    self.assertNotIn("Scroll", ui.scroll_messages[0])
366    self.assertIn("Mouse:", ui.scroll_messages[0])
367
368  def testRunUIInvalidCommandSyntax(self):
369    """Handle a command with invalid syntax."""
370
371    ui = MockCursesUI(
372        40,
373        80,
374        command_sequence=[string_to_codes("babble -z\n"), self._EXIT])
375
376    ui.register_command_handler("babble", self._babble, "")
377    ui.run_ui()
378
379    # Screen output/scrolling should have happened exactly once.
380    self.assertEqual(1, len(ui.unwrapped_outputs))
381    self.assertEqual(1, len(ui.wrapped_outputs))
382    self.assertEqual(1, len(ui.scroll_messages))
383    self.assertIn("Mouse:", ui.scroll_messages[0])
384    self.assertEqual(
385        ["Syntax error for command: babble", "For help, do \"help babble\""],
386        ui.unwrapped_outputs[0].lines)
387
388  def testRunUIScrollTallOutputPageDownUp(self):
389    """Scroll tall output with PageDown and PageUp."""
390
391    # Use PageDown and PageUp to scroll back and forth a little before exiting.
392    ui = MockCursesUI(
393        40,
394        80,
395        command_sequence=[string_to_codes("babble\n"), [curses.KEY_NPAGE] * 2 +
396                          [curses.KEY_PPAGE] + self._EXIT])
397
398    ui.register_command_handler("babble", self._babble, "")
399    ui.run_ui()
400
401    # Screen output/scrolling should have happened exactly once.
402    self.assertEqual(4, len(ui.unwrapped_outputs))
403    self.assertEqual(4, len(ui.wrapped_outputs))
404    self.assertEqual(4, len(ui.scroll_messages))
405
406    # Before scrolling.
407    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[0].lines)
408    self.assertEqual(["bar"] * 60, ui.wrapped_outputs[0].lines[:60])
409
410    # Initial scroll: At the top.
411    self.assertIn("Scroll (PgDn): 0.00%", ui.scroll_messages[0])
412    self.assertIn("Mouse:", ui.scroll_messages[0])
413
414    # After 1st scrolling (PageDown).
415    # The screen output shouldn't have changed. Only the viewport should.
416    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[0].lines)
417    self.assertEqual(["bar"] * 60, ui.wrapped_outputs[0].lines[:60])
418    self.assertIn("Scroll (PgDn/PgUp): 1.69%", ui.scroll_messages[1])
419    self.assertIn("Mouse:", ui.scroll_messages[1])
420
421    # After 2nd scrolling (PageDown).
422    self.assertIn("Scroll (PgDn/PgUp): 3.39%", ui.scroll_messages[2])
423    self.assertIn("Mouse:", ui.scroll_messages[2])
424
425    # After 3rd scrolling (PageUp).
426    self.assertIn("Scroll (PgDn/PgUp): 1.69%", ui.scroll_messages[3])
427    self.assertIn("Mouse:", ui.scroll_messages[3])
428
429  def testCutOffTooManyOutputLines(self):
430    ui = MockCursesUI(
431        40,
432        80,
433        command_sequence=[string_to_codes("babble -n 20\n"), self._EXIT])
434
435    # Modify max_output_lines so that this test doesn't use too much time or
436    # memory.
437    ui.max_output_lines = 10
438
439    ui.register_command_handler("babble", self._babble, "")
440    ui.run_ui()
441
442    self.assertEqual(["bar"] * 10 + ["Output cut off at 10 lines!"],
443                     ui.wrapped_outputs[0].lines[:11])
444
445  def testRunUIScrollTallOutputEndHome(self):
446    """Scroll tall output with PageDown and PageUp."""
447
448    # Use End and Home to scroll a little before exiting to test scrolling.
449    ui = MockCursesUI(
450        40,
451        80,
452        command_sequence=[
453            string_to_codes("babble\n"),
454            [curses.KEY_END] * 2 + [curses.KEY_HOME] + self._EXIT
455        ])
456
457    ui.register_command_handler("babble", self._babble, "")
458    ui.run_ui()
459
460    # Screen output/scrolling should have happened exactly once.
461    self.assertEqual(4, len(ui.unwrapped_outputs))
462    self.assertEqual(4, len(ui.wrapped_outputs))
463    self.assertEqual(4, len(ui.scroll_messages))
464
465    # Before scrolling.
466    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[0].lines)
467    self.assertEqual(["bar"] * 60, ui.wrapped_outputs[0].lines[:60])
468
469    # Initial scroll: At the top.
470    self.assertIn("Scroll (PgDn): 0.00%", ui.scroll_messages[0])
471
472    # After 1st scrolling (End).
473    self.assertIn("Scroll (PgUp): 100.00%", ui.scroll_messages[1])
474
475    # After 2nd scrolling (End).
476    self.assertIn("Scroll (PgUp): 100.00%", ui.scroll_messages[2])
477
478    # After 3rd scrolling (Hhome).
479    self.assertIn("Scroll (PgDn): 0.00%", ui.scroll_messages[3])
480
481  def testRunUIWithInitCmd(self):
482    """Run UI with an initial command specified."""
483
484    ui = MockCursesUI(40, 80, command_sequence=[self._EXIT])
485
486    ui.register_command_handler("babble", self._babble, "")
487    ui.run_ui(init_command="babble")
488
489    self.assertEqual(1, len(ui.unwrapped_outputs))
490
491    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[0].lines)
492    self.assertEqual(["bar"] * 60, ui.wrapped_outputs[0].lines[:60])
493    self.assertIn("Scroll (PgDn): 0.00%", ui.scroll_messages[0])
494
495  def testCompileHelpWithoutHelpIntro(self):
496    ui = MockCursesUI(
497        40,
498        80,
499        command_sequence=[string_to_codes("help\n"), self._EXIT])
500
501    ui.register_command_handler(
502        "babble", self._babble, "babble some", prefix_aliases=["b"])
503    ui.run_ui()
504
505    self.assertEqual(["babble", "  Aliases: b", "", "  babble some"],
506                     ui.unwrapped_outputs[0].lines[:4])
507
508  def testCompileHelpWithHelpIntro(self):
509    ui = MockCursesUI(
510        40,
511        80,
512        command_sequence=[string_to_codes("help\n"), self._EXIT])
513
514    help_intro = debugger_cli_common.RichTextLines(
515        ["This is a curses UI.", "All it can do is 'babble'.", ""])
516    ui.register_command_handler(
517        "babble", self._babble, "babble some", prefix_aliases=["b"])
518    ui.set_help_intro(help_intro)
519    ui.run_ui()
520
521    self.assertEqual(1, len(ui.unwrapped_outputs))
522    self.assertEqual(
523        help_intro.lines + ["babble", "  Aliases: b", "", "  babble some"],
524        ui.unwrapped_outputs[0].lines[:7])
525
526  def testCommandHistoryNavBackwardOnce(self):
527    ui = MockCursesUI(
528        40,
529        80,
530        command_sequence=[string_to_codes("help\n"),
531                          [curses.KEY_UP],  # Hit Up and Enter.
532                          string_to_codes("\n"),
533                          self._EXIT])
534
535    ui.register_command_handler(
536        "babble", self._babble, "babble some", prefix_aliases=["b"])
537    ui.run_ui()
538
539    self.assertEqual(2, len(ui.unwrapped_outputs))
540
541    for i in [0, 1]:
542      self.assertEqual(["babble", "  Aliases: b", "", "  babble some"],
543                       ui.unwrapped_outputs[i].lines[:4])
544
545  def testCommandHistoryNavBackwardTwice(self):
546    ui = MockCursesUI(
547        40,
548        80,
549        command_sequence=[string_to_codes("help\n"),
550                          string_to_codes("babble\n"),
551                          [curses.KEY_UP],
552                          [curses.KEY_UP],  # Hit Up twice and Enter.
553                          string_to_codes("\n"),
554                          self._EXIT])
555
556    ui.register_command_handler(
557        "babble", self._babble, "babble some", prefix_aliases=["b"])
558    ui.run_ui()
559
560    self.assertEqual(3, len(ui.unwrapped_outputs))
561
562    # The 1st and 3rd outputs are for command "help".
563    for i in [0, 2]:
564      self.assertEqual(["babble", "  Aliases: b", "", "  babble some"],
565                       ui.unwrapped_outputs[i].lines[:4])
566
567    # The 2nd output is for command "babble".
568    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[1].lines)
569
570  def testCommandHistoryNavBackwardOverLimit(self):
571    ui = MockCursesUI(
572        40,
573        80,
574        command_sequence=[string_to_codes("help\n"),
575                          string_to_codes("babble\n"),
576                          [curses.KEY_UP],
577                          [curses.KEY_UP],
578                          [curses.KEY_UP],  # Hit Up three times and Enter.
579                          string_to_codes("\n"),
580                          self._EXIT])
581
582    ui.register_command_handler(
583        "babble", self._babble, "babble some", prefix_aliases=["b"])
584    ui.run_ui()
585
586    self.assertEqual(3, len(ui.unwrapped_outputs))
587
588    # The 1st and 3rd outputs are for command "help".
589    for i in [0, 2]:
590      self.assertEqual(["babble", "  Aliases: b", "", "  babble some"],
591                       ui.unwrapped_outputs[i].lines[:4])
592
593    # The 2nd output is for command "babble".
594    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[1].lines)
595
596  def testCommandHistoryNavBackwardThenForward(self):
597    ui = MockCursesUI(
598        40,
599        80,
600        command_sequence=[string_to_codes("help\n"),
601                          string_to_codes("babble\n"),
602                          [curses.KEY_UP],
603                          [curses.KEY_UP],
604                          [curses.KEY_DOWN],  # Hit Up twice and Down once.
605                          string_to_codes("\n"),
606                          self._EXIT])
607
608    ui.register_command_handler(
609        "babble", self._babble, "babble some", prefix_aliases=["b"])
610    ui.run_ui()
611
612    self.assertEqual(3, len(ui.unwrapped_outputs))
613
614    # The 1st output is for command "help".
615    self.assertEqual(["babble", "  Aliases: b", "", "  babble some"],
616                     ui.unwrapped_outputs[0].lines[:4])
617
618    # The 2nd and 3rd outputs are for command "babble".
619    for i in [1, 2]:
620      self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[i].lines)
621
622  def testCommandHistoryPrefixNavBackwardOnce(self):
623    ui = MockCursesUI(
624        40,
625        80,
626        command_sequence=[
627            string_to_codes("babble -n 1\n"),
628            string_to_codes("babble -n 10\n"),
629            string_to_codes("help\n"),
630            string_to_codes("b") + [curses.KEY_UP],  # Navigate with prefix.
631            string_to_codes("\n"),
632            self._EXIT
633        ])
634
635    ui.register_command_handler(
636        "babble", self._babble, "babble some", prefix_aliases=["b"])
637    ui.run_ui()
638
639    self.assertEqual(["bar"], ui.unwrapped_outputs[0].lines)
640    self.assertEqual(["bar"] * 10, ui.unwrapped_outputs[1].lines)
641    self.assertEqual(["babble", "  Aliases: b", "", "  babble some"],
642                     ui.unwrapped_outputs[2].lines[:4])
643    self.assertEqual(["bar"] * 10, ui.unwrapped_outputs[3].lines)
644
645  def testTerminalResize(self):
646    ui = MockCursesUI(
647        40,
648        80,
649        command_sequence=[string_to_codes("babble\n"),
650                          [curses.KEY_RESIZE, 100, 85],  # Resize to [100, 85]
651                          self._EXIT])
652
653    ui.register_command_handler(
654        "babble", self._babble, "babble some", prefix_aliases=["b"])
655    ui.run_ui()
656
657    # The resize event should have caused a second screen output event.
658    self.assertEqual(2, len(ui.unwrapped_outputs))
659    self.assertEqual(2, len(ui.wrapped_outputs))
660    self.assertEqual(2, len(ui.scroll_messages))
661
662    # The 1st and 2nd screen outputs should be identical (unwrapped).
663    self.assertEqual(ui.unwrapped_outputs[0], ui.unwrapped_outputs[1])
664
665    # The 1st scroll info should contain scrolling, because the screen size
666    # is less than the number of lines in the output.
667    self.assertIn("Scroll (PgDn): 0.00%", ui.scroll_messages[0])
668
669  def testTabCompletionWithCommonPrefix(self):
670    # Type "b" and trigger tab completion.
671    ui = MockCursesUI(
672        40,
673        80,
674        command_sequence=[string_to_codes("b\t"), string_to_codes("\n"),
675                          self._EXIT])
676
677    ui.register_command_handler(
678        "babble", self._babble, "babble some", prefix_aliases=["ba"])
679    ui.run_ui()
680
681    # The automatically registered exit commands "exit" and "quit" should not
682    # appear in the tab completion candidates because they don't start with
683    # "b".
684    self.assertEqual([["ba", "babble"]], ui.candidates_lists)
685
686    # "ba" is a common prefix of the two candidates. So the "ba" command should
687    # have been issued after the Enter.
688    self.assertEqual(1, len(ui.unwrapped_outputs))
689    self.assertEqual(1, len(ui.wrapped_outputs))
690    self.assertEqual(1, len(ui.scroll_messages))
691    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[0].lines)
692    self.assertEqual(["bar"] * 60, ui.wrapped_outputs[0].lines[:60])
693
694  def testTabCompletionEmptyTriggerWithoutCommonPrefix(self):
695    ui = MockCursesUI(
696        40,
697        80,
698        command_sequence=[string_to_codes("\t"),  # Trigger tab completion.
699                          string_to_codes("\n"),
700                          self._EXIT])
701
702    ui.register_command_handler(
703        "babble", self._babble, "babble some", prefix_aliases=["a"])
704    # Use a different alias "a" instead.
705    ui.run_ui()
706
707    # The manually registered command, along with the automatically registered
708    # exit commands should appear in the candidates.
709    self.assertEqual(
710        [["a", "babble", "cfg", "config", "exit", "h", "help", "m", "mouse",
711          "quit"]], ui.candidates_lists)
712
713    # The two candidates have no common prefix. So no command should have been
714    # issued.
715    self.assertEqual(0, len(ui.unwrapped_outputs))
716    self.assertEqual(0, len(ui.wrapped_outputs))
717    self.assertEqual(0, len(ui.scroll_messages))
718
719  def testTabCompletionNonemptyTriggerSingleCandidate(self):
720    ui = MockCursesUI(
721        40,
722        80,
723        command_sequence=[string_to_codes("b\t"),  # Trigger tab completion.
724                          string_to_codes("\n"),
725                          self._EXIT])
726
727    ui.register_command_handler(
728        "babble", self._babble, "babble some", prefix_aliases=["a"])
729    ui.run_ui()
730
731    # There is only one candidate, so no candidates should have been displayed.
732    # Instead, the completion should have been automatically keyed in, leading
733    # to the "babble" command being issue.
734    self.assertEqual([[]], ui.candidates_lists)
735
736    self.assertEqual(1, len(ui.unwrapped_outputs))
737    self.assertEqual(1, len(ui.wrapped_outputs))
738    self.assertEqual(1, len(ui.scroll_messages))
739    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[0].lines)
740    self.assertEqual(["bar"] * 60, ui.wrapped_outputs[0].lines[:60])
741
742  def testTabCompletionNoMatch(self):
743    ui = MockCursesUI(
744        40,
745        80,
746        command_sequence=[string_to_codes("c\t"),  # Trigger tab completion.
747                          string_to_codes("\n"),
748                          self._EXIT])
749
750    ui.register_command_handler(
751        "babble", self._babble, "babble some", prefix_aliases=["a"])
752    ui.run_ui()
753
754    # Only the invalid command "c" should have been issued.
755    self.assertEqual(1, len(ui.unwrapped_outputs))
756    self.assertEqual(1, len(ui.wrapped_outputs))
757    self.assertEqual(1, len(ui.scroll_messages))
758
759    self.assertEqual(["ERROR: Invalid command prefix \"c\""],
760                     ui.unwrapped_outputs[0].lines)
761    self.assertEqual(["ERROR: Invalid command prefix \"c\""],
762                     ui.wrapped_outputs[0].lines[:1])
763
764  def testTabCompletionOneWordContext(self):
765    ui = MockCursesUI(
766        40,
767        80,
768        command_sequence=[
769            string_to_codes("babble -n 3\t"),  # Trigger tab completion.
770            string_to_codes("\n"),
771            self._EXIT
772        ])
773
774    ui.register_command_handler(
775        "babble", self._babble, "babble some", prefix_aliases=["b"])
776    ui.register_tab_comp_context(["babble", "b"], ["10", "20", "30", "300"])
777    ui.run_ui()
778
779    self.assertEqual([["30", "300"]], ui.candidates_lists)
780
781    self.assertEqual(1, len(ui.unwrapped_outputs))
782    self.assertEqual(1, len(ui.wrapped_outputs))
783    self.assertEqual(1, len(ui.scroll_messages))
784    self.assertEqual(["bar"] * 30, ui.unwrapped_outputs[0].lines)
785    self.assertEqual(["bar"] * 30, ui.wrapped_outputs[0].lines[:30])
786
787  def testTabCompletionTwice(self):
788    ui = MockCursesUI(
789        40,
790        80,
791        command_sequence=[
792            string_to_codes("babble -n 1\t"),  # Trigger tab completion.
793            string_to_codes("2\t"),  # With more prefix, tab again.
794            string_to_codes("3\n"),
795            self._EXIT
796        ])
797
798    ui.register_command_handler(
799        "babble", self._babble, "babble some", prefix_aliases=["b"])
800    ui.register_tab_comp_context(["babble", "b"], ["10", "120", "123"])
801    ui.run_ui()
802
803    # There should have been two different lists of candidates.
804    self.assertEqual([["10", "120", "123"], ["120", "123"]],
805                     ui.candidates_lists)
806
807    self.assertEqual(1, len(ui.unwrapped_outputs))
808    self.assertEqual(1, len(ui.wrapped_outputs))
809    self.assertEqual(1, len(ui.scroll_messages))
810    self.assertEqual(["bar"] * 123, ui.unwrapped_outputs[0].lines)
811    self.assertEqual(["bar"] * 123, ui.wrapped_outputs[0].lines[:123])
812
813  def testRegexSearch(self):
814    """Test regex search."""
815
816    ui = MockCursesUI(
817        40,
818        80,
819        command_sequence=[
820            string_to_codes("babble -n 3\n"),
821            string_to_codes("/(b|r)\n"),  # Regex search and highlight.
822            string_to_codes("/a\n"),  # Regex search and highlight.
823            self._EXIT
824        ])
825
826    ui.register_command_handler(
827        "babble", self._babble, "babble some", prefix_aliases=["b"])
828    ui.run_ui()
829
830    # The unwrapped (original) output should never have any highlighting.
831    self.assertEqual(3, len(ui.unwrapped_outputs))
832    for i in range(3):
833      self.assertEqual(["bar"] * 3, ui.unwrapped_outputs[i].lines)
834      self.assertEqual({}, ui.unwrapped_outputs[i].font_attr_segs)
835
836    # The wrapped outputs should show highlighting depending on the regex.
837    self.assertEqual(3, len(ui.wrapped_outputs))
838
839    # The first output should have no highlighting.
840    self.assertEqual(["bar"] * 3, ui.wrapped_outputs[0].lines[:3])
841    self.assertEqual({}, ui.wrapped_outputs[0].font_attr_segs)
842
843    # The second output should have highlighting for "b" and "r".
844    self.assertEqual(["bar"] * 3, ui.wrapped_outputs[1].lines[:3])
845    for i in range(3):
846      self.assertEqual([(0, 1, "black_on_white"), (2, 3, "black_on_white")],
847                       ui.wrapped_outputs[1].font_attr_segs[i])
848
849    # The third output should have highlighting for "a" only.
850    self.assertEqual(["bar"] * 3, ui.wrapped_outputs[1].lines[:3])
851    for i in range(3):
852      self.assertEqual([(1, 2, "black_on_white")],
853                       ui.wrapped_outputs[2].font_attr_segs[i])
854
855  def testRegexSearchContinuation(self):
856    """Test continuing scrolling down to next regex match."""
857
858    ui = MockCursesUI(
859        40,
860        80,
861        command_sequence=[
862            string_to_codes("babble -n 3\n"),
863            string_to_codes("/(b|r)\n"),  # Regex search and highlight.
864            string_to_codes("/\n"),  # Continue scrolling down: 1st time.
865            string_to_codes("/\n"),  # Continue scrolling down: 2nd time.
866            string_to_codes("/\n"),  # Continue scrolling down: 3rd time.
867            string_to_codes("/\n"),  # Continue scrolling down: 4th time.
868            self._EXIT
869        ])
870
871    ui.register_command_handler(
872        "babble", self._babble, "babble some", prefix_aliases=["b"])
873    ui.run_ui()
874
875    # The 1st output is for the non-searched output. The other three are for
876    # the searched output. Even though continuation search "/" is performed
877    # four times, there should be only three searched outputs, because the
878    # last one has exceeded the end.
879    self.assertEqual(4, len(ui.unwrapped_outputs))
880
881    for i in range(4):
882      self.assertEqual(["bar"] * 3, ui.unwrapped_outputs[i].lines)
883      self.assertEqual({}, ui.unwrapped_outputs[i].font_attr_segs)
884
885    self.assertEqual(["bar"] * 3, ui.wrapped_outputs[0].lines[:3])
886    self.assertEqual({}, ui.wrapped_outputs[0].font_attr_segs)
887
888    for j in range(1, 4):
889      self.assertEqual(["bar"] * 3, ui.wrapped_outputs[j].lines[:3])
890      self.assertEqual({
891          0: [(0, 1, "black_on_white"), (2, 3, "black_on_white")],
892          1: [(0, 1, "black_on_white"), (2, 3, "black_on_white")],
893          2: [(0, 1, "black_on_white"), (2, 3, "black_on_white")]
894      }, ui.wrapped_outputs[j].font_attr_segs)
895
896    self.assertEqual([0, 0, 1, 2], ui.output_pad_rows)
897
898  def testRegexSearchUnderLineWrapping(self):
899    ui = MockCursesUI(
900        40,
901        6,  # Use a narrow window to trigger line wrapping
902        command_sequence=[
903            string_to_codes("babble -n 3 -l foo-bar-baz-qux\n"),
904            string_to_codes("/foo\n"),  # Regex search and highlight.
905            string_to_codes("/\n"),  # Continue scrolling down: 1st time.
906            string_to_codes("/\n"),  # Continue scrolling down: 2nd time.
907            string_to_codes("/\n"),  # Continue scrolling down: 3rd time.
908            string_to_codes("/\n"),  # Continue scrolling down: 4th time.
909            self._EXIT
910        ])
911
912    ui.register_command_handler(
913        "babble", self._babble, "babble some")
914    ui.run_ui()
915
916    self.assertEqual(4, len(ui.wrapped_outputs))
917    for wrapped_output in ui.wrapped_outputs:
918      self.assertEqual(["foo-", "bar-", "baz-", "qux"] * 3,
919                       wrapped_output.lines[0 : 12])
920
921    # The scroll location should reflect the line wrapping.
922    self.assertEqual([0, 0, 4, 8], ui.output_pad_rows)
923
924  def testRegexSearchNoMatchContinuation(self):
925    """Test continuing scrolling when there is no regex match."""
926
927    ui = MockCursesUI(
928        40,
929        80,
930        command_sequence=[
931            string_to_codes("babble -n 3\n"),
932            string_to_codes("/foo\n"),  # Regex search and highlight.
933            string_to_codes("/\n"),  # Continue scrolling down.
934            self._EXIT
935        ])
936
937    ui.register_command_handler(
938        "babble", self._babble, "babble some", prefix_aliases=["b"])
939    ui.run_ui()
940
941    # The regex search and continuation search in the 3rd command should not
942    # have produced any output.
943    self.assertEqual(1, len(ui.unwrapped_outputs))
944    self.assertEqual([0], ui.output_pad_rows)
945
946  def testRegexSearchContinuationWithoutSearch(self):
947    """Test continuation scrolling when no regex search has been performed."""
948
949    ui = MockCursesUI(
950        40,
951        80,
952        command_sequence=[
953            string_to_codes("babble -n 3\n"),
954            string_to_codes("/\n"),  # Continue scrolling without search first.
955            self._EXIT
956        ])
957
958    ui.register_command_handler(
959        "babble", self._babble, "babble some", prefix_aliases=["b"])
960    ui.run_ui()
961
962    self.assertEqual(1, len(ui.unwrapped_outputs))
963    self.assertEqual([0], ui.output_pad_rows)
964
965  def testRegexSearchWithInvalidRegex(self):
966    """Test using invalid regex to search."""
967
968    ui = MockCursesUI(
969        40,
970        80,
971        command_sequence=[
972            string_to_codes("babble -n 3\n"),
973            string_to_codes("/[\n"),  # Continue scrolling without search first.
974            self._EXIT
975        ])
976
977    ui.register_command_handler(
978        "babble", self._babble, "babble some", prefix_aliases=["b"])
979    ui.run_ui()
980
981    # Invalid regex should not have led to a new screen of output.
982    self.assertEqual(1, len(ui.unwrapped_outputs))
983    self.assertEqual([0], ui.output_pad_rows)
984
985    # Invalid regex should have led to a toast error message.
986    self.assertEqual(
987        [MockCursesUI._UI_WAIT_MESSAGE,
988         "ERROR: Invalid regular expression: \"[\"",
989         MockCursesUI._UI_WAIT_MESSAGE],
990        ui.toasts)
991
992  def testRegexSearchFromCommandHistory(self):
993    """Test regex search commands are recorded in command history."""
994
995    ui = MockCursesUI(
996        40,
997        80,
998        command_sequence=[
999            string_to_codes("babble -n 3\n"),
1000            string_to_codes("/(b|r)\n"),  # Regex search and highlight.
1001            string_to_codes("babble -n 4\n"),
1002            [curses.KEY_UP],
1003            [curses.KEY_UP],
1004            string_to_codes("\n"),  # Hit Up twice and Enter.
1005            self._EXIT
1006        ])
1007
1008    ui.register_command_handler(
1009        "babble", self._babble, "babble some", prefix_aliases=["b"])
1010    ui.run_ui()
1011
1012    self.assertEqual(4, len(ui.wrapped_outputs))
1013
1014    self.assertEqual(["bar"] * 3, ui.wrapped_outputs[0].lines[:3])
1015    self.assertEqual({}, ui.wrapped_outputs[0].font_attr_segs)
1016
1017    self.assertEqual(["bar"] * 3, ui.wrapped_outputs[1].lines[:3])
1018    for i in range(3):
1019      self.assertEqual([(0, 1, "black_on_white"), (2, 3, "black_on_white")],
1020                       ui.wrapped_outputs[1].font_attr_segs[i])
1021
1022    self.assertEqual(["bar"] * 4, ui.wrapped_outputs[2].lines[:4])
1023    self.assertEqual({}, ui.wrapped_outputs[2].font_attr_segs)
1024
1025    # The regex search command loaded from history should have worked on the
1026    # new screen output.
1027    self.assertEqual(["bar"] * 4, ui.wrapped_outputs[3].lines[:4])
1028    for i in range(4):
1029      self.assertEqual([(0, 1, "black_on_white"), (2, 3, "black_on_white")],
1030                       ui.wrapped_outputs[3].font_attr_segs[i])
1031
1032  def testDisplayTensorWithIndices(self):
1033    """Test displaying tensor with indices."""
1034
1035    ui = MockCursesUI(
1036        9,  # Use a small screen height to cause scrolling.
1037        80,
1038        command_sequence=[
1039            string_to_codes("print_ones --size 5\n"),
1040            [curses.KEY_NPAGE],
1041            [curses.KEY_NPAGE],
1042            [curses.KEY_NPAGE],
1043            [curses.KEY_END],
1044            [curses.KEY_NPAGE],  # This PageDown goes over the bottom limit.
1045            [curses.KEY_PPAGE],
1046            [curses.KEY_PPAGE],
1047            [curses.KEY_PPAGE],
1048            [curses.KEY_HOME],
1049            [curses.KEY_PPAGE],  # This PageDown goes over the top limit.
1050            self._EXIT
1051        ])
1052
1053    ui.register_command_handler("print_ones", self._print_ones,
1054                                "print an all-one matrix of specified size")
1055    ui.run_ui()
1056
1057    self.assertEqual(11, len(ui.unwrapped_outputs))
1058    self.assertEqual(11, len(ui.output_array_pointer_indices))
1059    self.assertEqual(11, len(ui.scroll_messages))
1060
1061    for i in range(11):
1062      cli_test_utils.assert_lines_equal_ignoring_whitespace(
1063          self, ["Tensor \"m\":", ""], ui.unwrapped_outputs[i].lines[:2])
1064      self.assertEqual(
1065          repr(np.ones([5, 5])).split("\n"), ui.unwrapped_outputs[i].lines[2:])
1066
1067    self.assertEqual({
1068        0: None,
1069        -1: [1, 0]
1070    }, ui.output_array_pointer_indices[0])
1071    self.assertIn(" Scroll (PgDn): 0.00% -[1,0] ", ui.scroll_messages[0])
1072
1073    # Scrolled down one line.
1074    self.assertEqual({
1075        0: None,
1076        -1: [2, 0]
1077    }, ui.output_array_pointer_indices[1])
1078    self.assertIn(" Scroll (PgDn/PgUp): 16.67% -[2,0] ", ui.scroll_messages[1])
1079
1080    # Scrolled down one line.
1081    self.assertEqual({
1082        0: [0, 0],
1083        -1: [3, 0]
1084    }, ui.output_array_pointer_indices[2])
1085    self.assertIn(" Scroll (PgDn/PgUp): 33.33% [0,0]-[3,0] ",
1086                  ui.scroll_messages[2])
1087
1088    # Scrolled down one line.
1089    self.assertEqual({
1090        0: [1, 0],
1091        -1: [4, 0]
1092    }, ui.output_array_pointer_indices[3])
1093    self.assertIn(" Scroll (PgDn/PgUp): 50.00% [1,0]-[4,0] ",
1094                  ui.scroll_messages[3])
1095
1096    # Scroll to the bottom.
1097    self.assertEqual({
1098        0: [4, 0],
1099        -1: None
1100    }, ui.output_array_pointer_indices[4])
1101    self.assertIn(" Scroll (PgUp): 100.00% [4,0]- ", ui.scroll_messages[4])
1102
1103    # Attempt to scroll beyond the bottom should lead to no change.
1104    self.assertEqual({
1105        0: [4, 0],
1106        -1: None
1107    }, ui.output_array_pointer_indices[5])
1108    self.assertIn(" Scroll (PgUp): 100.00% [4,0]- ", ui.scroll_messages[5])
1109
1110    # Scrolled up one line.
1111    self.assertEqual({
1112        0: [3, 0],
1113        -1: None
1114    }, ui.output_array_pointer_indices[6])
1115    self.assertIn(" Scroll (PgDn/PgUp): 83.33% [3,0]- ", ui.scroll_messages[6])
1116
1117    # Scrolled up one line.
1118    self.assertEqual({
1119        0: [2, 0],
1120        -1: None
1121    }, ui.output_array_pointer_indices[7])
1122    self.assertIn(" Scroll (PgDn/PgUp): 66.67% [2,0]- ", ui.scroll_messages[7])
1123
1124    # Scrolled up one line.
1125    self.assertEqual({
1126        0: [1, 0],
1127        -1: [4, 0]
1128    }, ui.output_array_pointer_indices[8])
1129    self.assertIn(" Scroll (PgDn/PgUp): 50.00% [1,0]-[4,0] ",
1130                  ui.scroll_messages[8])
1131
1132    # Scroll to the top.
1133    self.assertEqual({
1134        0: None,
1135        -1: [1, 0]
1136    }, ui.output_array_pointer_indices[9])
1137    self.assertIn(" Scroll (PgDn): 0.00% -[1,0] ", ui.scroll_messages[9])
1138
1139    # Attempt to scroll pass the top limit should lead to no change.
1140    self.assertEqual({
1141        0: None,
1142        -1: [1, 0]
1143    }, ui.output_array_pointer_indices[10])
1144    self.assertIn(" Scroll (PgDn): 0.00% -[1,0] ", ui.scroll_messages[10])
1145
1146  def testScrollTensorByValidIndices(self):
1147    """Test scrolling to specified (valid) indices in a tensor."""
1148
1149    ui = MockCursesUI(
1150        8,  # Use a small screen height to cause scrolling.
1151        80,
1152        command_sequence=[
1153            string_to_codes("print_ones --size 5\n"),
1154            string_to_codes("@[0, 0]\n"),  # Scroll to element [0, 0].
1155            string_to_codes("@1,0\n"),  # Scroll to element [3, 0].
1156            string_to_codes("@[0,2]\n"),  # Scroll back to line 0.
1157            self._EXIT
1158        ])
1159
1160    ui.register_command_handler("print_ones", self._print_ones,
1161                                "print an all-one matrix of specified size")
1162    ui.run_ui()
1163
1164    self.assertEqual(4, len(ui.unwrapped_outputs))
1165    self.assertEqual(4, len(ui.output_array_pointer_indices))
1166
1167    for i in range(4):
1168      cli_test_utils.assert_lines_equal_ignoring_whitespace(
1169          self, ["Tensor \"m\":", ""], ui.unwrapped_outputs[i].lines[:2])
1170      self.assertEqual(
1171          repr(np.ones([5, 5])).split("\n"), ui.unwrapped_outputs[i].lines[2:])
1172
1173    self.assertEqual({
1174        0: None,
1175        -1: [0, 0]
1176    }, ui.output_array_pointer_indices[0])
1177    self.assertEqual({
1178        0: [0, 0],
1179        -1: [2, 0]
1180    }, ui.output_array_pointer_indices[1])
1181    self.assertEqual({
1182        0: [1, 0],
1183        -1: [3, 0]
1184    }, ui.output_array_pointer_indices[2])
1185    self.assertEqual({
1186        0: [0, 0],
1187        -1: [2, 0]
1188    }, ui.output_array_pointer_indices[3])
1189
1190  def testScrollTensorByInvalidIndices(self):
1191    """Test scrolling to specified invalid indices in a tensor."""
1192
1193    ui = MockCursesUI(
1194        8,  # Use a small screen height to cause scrolling.
1195        80,
1196        command_sequence=[
1197            string_to_codes("print_ones --size 5\n"),
1198            string_to_codes("@[10, 0]\n"),  # Scroll to invalid indices.
1199            string_to_codes("@[]\n"),  # Scroll to invalid indices.
1200            string_to_codes("@\n"),  # Scroll to invalid indices.
1201            self._EXIT
1202        ])
1203
1204    ui.register_command_handler("print_ones", self._print_ones,
1205                                "print an all-one matrix of specified size")
1206    ui.run_ui()
1207
1208    # Because all scroll-by-indices commands are invalid, there should be only
1209    # one output event.
1210    self.assertEqual(1, len(ui.unwrapped_outputs))
1211    self.assertEqual(1, len(ui.output_array_pointer_indices))
1212
1213    # Check error messages.
1214    self.assertEqual("ERROR: Indices exceed tensor dimensions.", ui.toasts[2])
1215    self.assertEqual("ERROR: invalid literal for int() with base 10: ''",
1216                     ui.toasts[4])
1217    self.assertEqual("ERROR: Empty indices.", ui.toasts[6])
1218
1219  def testWriteScreenOutputToFileWorks(self):
1220    output_path = tempfile.mktemp()
1221
1222    ui = MockCursesUI(
1223        40,
1224        80,
1225        command_sequence=[
1226            string_to_codes("babble -n 2>%s\n" % output_path),
1227            self._EXIT
1228        ])
1229
1230    ui.register_command_handler("babble", self._babble, "")
1231    ui.run_ui()
1232
1233    self.assertEqual(1, len(ui.unwrapped_outputs))
1234
1235    with gfile.Open(output_path, "r") as f:
1236      self.assertEqual("bar\nbar\n", f.read())
1237
1238    # Clean up output file.
1239    gfile.Remove(output_path)
1240
1241  def testIncompleteRedirectErrors(self):
1242    ui = MockCursesUI(
1243        40,
1244        80,
1245        command_sequence=[
1246            string_to_codes("babble -n 2 >\n"),
1247            self._EXIT
1248        ])
1249
1250    ui.register_command_handler("babble", self._babble, "")
1251    ui.run_ui()
1252
1253    self.assertEqual(["ERROR: Redirect file path is empty"], ui.toasts)
1254    self.assertEqual(0, len(ui.unwrapped_outputs))
1255
1256  def testAppendingRedirectErrors(self):
1257    output_path = tempfile.mktemp()
1258
1259    ui = MockCursesUI(
1260        40,
1261        80,
1262        command_sequence=[
1263            string_to_codes("babble -n 2 >> %s\n" % output_path),
1264            self._EXIT
1265        ])
1266
1267    ui.register_command_handler("babble", self._babble, "")
1268    ui.run_ui()
1269
1270    self.assertEqual(1, len(ui.unwrapped_outputs))
1271    self.assertEqual(
1272        ["Syntax error for command: babble", "For help, do \"help babble\""],
1273        ui.unwrapped_outputs[0].lines)
1274
1275    # Clean up output file.
1276    gfile.Remove(output_path)
1277
1278  def testMouseOffTakesEffect(self):
1279    ui = MockCursesUI(
1280        40,
1281        80,
1282        command_sequence=[
1283            string_to_codes("mouse off\n"), string_to_codes("babble\n"),
1284            self._EXIT
1285        ])
1286    ui.register_command_handler("babble", self._babble, "")
1287
1288    ui.run_ui()
1289    self.assertFalse(ui._mouse_enabled)
1290    self.assertIn("Mouse: OFF", ui.scroll_messages[-1])
1291
1292  def testMouseOffAndOnTakeEffect(self):
1293    ui = MockCursesUI(
1294        40,
1295        80,
1296        command_sequence=[
1297            string_to_codes("mouse off\n"), string_to_codes("mouse on\n"),
1298            string_to_codes("babble\n"), self._EXIT
1299        ])
1300    ui.register_command_handler("babble", self._babble, "")
1301
1302    ui.run_ui()
1303    self.assertTrue(ui._mouse_enabled)
1304    self.assertIn("Mouse: ON", ui.scroll_messages[-1])
1305
1306  def testMouseClickOnLinkTriggersCommand(self):
1307    ui = MockCursesUI(
1308        40,
1309        80,
1310        command_sequence=[
1311            string_to_codes("babble -n 10 -k\n"),
1312            [curses.KEY_MOUSE, 1, 4],  # A click on a hyperlink.
1313            self._EXIT
1314        ])
1315    ui.register_command_handler("babble", self._babble, "")
1316    ui.run_ui()
1317
1318    self.assertEqual(2, len(ui.unwrapped_outputs))
1319    self.assertEqual(["bar"] * 10, ui.unwrapped_outputs[0].lines)
1320    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[1].lines)
1321
1322  def testMouseClickOnLinkWithExistingTextTriggersCommand(self):
1323    ui = MockCursesUI(
1324        40,
1325        80,
1326        command_sequence=[
1327            string_to_codes("babble -n 10 -k\n"),
1328            string_to_codes("foo"),  # Enter some existing code in the textbox.
1329            [curses.KEY_MOUSE, 1, 4],  # A click on a hyperlink.
1330            self._EXIT
1331        ])
1332    ui.register_command_handler("babble", self._babble, "")
1333    ui.run_ui()
1334
1335    self.assertEqual(2, len(ui.unwrapped_outputs))
1336    self.assertEqual(["bar"] * 10, ui.unwrapped_outputs[0].lines)
1337    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[1].lines)
1338
1339  def testMouseClickOffLinkDoesNotTriggersCommand(self):
1340    ui = MockCursesUI(
1341        40,
1342        80,
1343        command_sequence=[
1344            string_to_codes("babble -n 10 -k\n"),
1345            # A click off a hyperlink (too much to the right).
1346            [curses.KEY_MOUSE, 8, 4],
1347            self._EXIT
1348        ])
1349    ui.register_command_handler("babble", self._babble, "")
1350    ui.run_ui()
1351
1352    # The mouse click event should not triggered no command.
1353    self.assertEqual(1, len(ui.unwrapped_outputs))
1354    self.assertEqual(["bar"] * 10, ui.unwrapped_outputs[0].lines)
1355
1356    # This command should have generated no main menus.
1357    self.assertEqual([None], ui.main_menu_list)
1358
1359  def testMouseClickOnEnabledMenuItemWorks(self):
1360    ui = MockCursesUI(
1361        40,
1362        80,
1363        command_sequence=[
1364            string_to_codes("babble -n 10 -m\n"),
1365            # A click on the enabled menu item.
1366            [curses.KEY_MOUSE, 3, 2],
1367            self._EXIT
1368        ])
1369    ui.register_command_handler("babble", self._babble, "")
1370    ui.run_ui()
1371
1372    self.assertEqual(2, len(ui.unwrapped_outputs))
1373    self.assertEqual(["bar"] * 10, ui.unwrapped_outputs[0].lines)
1374    self.assertEqual(["bar"] * 60, ui.unwrapped_outputs[1].lines)
1375
1376    # Check the content of the menu.
1377    self.assertEqual(["| babble again | ahoy | "], ui.main_menu_list[0].lines)
1378    self.assertEqual(1, len(ui.main_menu_list[0].font_attr_segs))
1379    self.assertEqual(1, len(ui.main_menu_list[0].font_attr_segs[0]))
1380
1381    item_annot = ui.main_menu_list[0].font_attr_segs[0][0]
1382    self.assertEqual(2, item_annot[0])
1383    self.assertEqual(14, item_annot[1])
1384    self.assertEqual("babble", item_annot[2][0].content)
1385    self.assertEqual("underline", item_annot[2][1])
1386
1387    # The output from the menu-triggered command does not have a menu.
1388    self.assertIsNone(ui.main_menu_list[1])
1389
1390  def testMouseClickOnDisabledMenuItemTriggersNoCommand(self):
1391    ui = MockCursesUI(
1392        40,
1393        80,
1394        command_sequence=[
1395            string_to_codes("babble -n 10 -m\n"),
1396            # A click on the disabled menu item.
1397            [curses.KEY_MOUSE, 18, 1],
1398            self._EXIT
1399        ])
1400    ui.register_command_handler("babble", self._babble, "")
1401    ui.run_ui()
1402
1403    self.assertEqual(1, len(ui.unwrapped_outputs))
1404    self.assertEqual(["bar"] * 10, ui.unwrapped_outputs[0].lines)
1405
1406  def testNavigationUsingCommandLineWorks(self):
1407    ui = MockCursesUI(
1408        40,
1409        80,
1410        command_sequence=[
1411            string_to_codes("babble -n 2\n"),
1412            string_to_codes("babble -n 4\n"),
1413            string_to_codes("prev\n"),
1414            string_to_codes("next\n"),
1415            self._EXIT
1416        ])
1417    ui.register_command_handler("babble", self._babble, "")
1418    ui.run_ui()
1419
1420    self.assertEqual(4, len(ui.unwrapped_outputs))
1421    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[0].lines)
1422    self.assertEqual(["bar"] * 4, ui.unwrapped_outputs[1].lines)
1423    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[2].lines)
1424    self.assertEqual(["bar"] * 4, ui.unwrapped_outputs[3].lines)
1425
1426  def testNavigationOverOldestLimitUsingCommandLineGivesCorrectWarning(self):
1427    ui = MockCursesUI(
1428        40,
1429        80,
1430        command_sequence=[
1431            string_to_codes("babble -n 2\n"),
1432            string_to_codes("babble -n 4\n"),
1433            string_to_codes("prev\n"),
1434            string_to_codes("prev\n"),  # Navigate over oldest limit.
1435            self._EXIT
1436        ])
1437    ui.register_command_handler("babble", self._babble, "")
1438    ui.run_ui()
1439
1440    self.assertEqual(3, len(ui.unwrapped_outputs))
1441    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[0].lines)
1442    self.assertEqual(["bar"] * 4, ui.unwrapped_outputs[1].lines)
1443    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[2].lines)
1444
1445    self.assertEqual("At the OLDEST in navigation history!", ui.toasts[-2])
1446
1447  def testNavigationOverLatestLimitUsingCommandLineGivesCorrectWarning(self):
1448    ui = MockCursesUI(
1449        40,
1450        80,
1451        command_sequence=[
1452            string_to_codes("babble -n 2\n"),
1453            string_to_codes("babble -n 4\n"),
1454            string_to_codes("prev\n"),
1455            string_to_codes("next\n"),
1456            string_to_codes("next\n"),  # Navigate over latest limit.
1457            self._EXIT
1458        ])
1459    ui.register_command_handler("babble", self._babble, "")
1460    ui.run_ui()
1461
1462    self.assertEqual(4, len(ui.unwrapped_outputs))
1463    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[0].lines)
1464    self.assertEqual(["bar"] * 4, ui.unwrapped_outputs[1].lines)
1465    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[2].lines)
1466    self.assertEqual(["bar"] * 4, ui.unwrapped_outputs[3].lines)
1467
1468    self.assertEqual("At the LATEST in navigation history!", ui.toasts[-2])
1469
1470  def testMouseClicksOnNavBarWorks(self):
1471    ui = MockCursesUI(
1472        40,
1473        80,
1474        command_sequence=[
1475            string_to_codes("babble -n 2\n"),
1476            string_to_codes("babble -n 4\n"),
1477            # A click on the back (prev) button of the nav bar.
1478            [curses.KEY_MOUSE, 3, 1],
1479            # A click on the forward (prev) button of the nav bar.
1480            [curses.KEY_MOUSE, 7, 1],
1481            self._EXIT
1482        ])
1483    ui.register_command_handler("babble", self._babble, "")
1484    ui.run_ui()
1485
1486    self.assertEqual(4, len(ui.unwrapped_outputs))
1487    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[0].lines)
1488    self.assertEqual(["bar"] * 4, ui.unwrapped_outputs[1].lines)
1489    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[2].lines)
1490    self.assertEqual(["bar"] * 4, ui.unwrapped_outputs[3].lines)
1491
1492  def testMouseClicksOnNavBarAfterPreviousScrollingWorks(self):
1493    ui = MockCursesUI(
1494        40,
1495        80,
1496        command_sequence=[
1497            string_to_codes("babble -n 2\n"),
1498            [curses.KEY_NPAGE],   # Scroll down one line.
1499            string_to_codes("babble -n 4\n"),
1500            # A click on the back (prev) button of the nav bar.
1501            [curses.KEY_MOUSE, 3, 1],
1502            # A click on the forward (prev) button of the nav bar.
1503            [curses.KEY_MOUSE, 7, 1],
1504            self._EXIT
1505        ])
1506    ui.register_command_handler("babble", self._babble, "")
1507    ui.run_ui()
1508
1509    self.assertEqual(6, len(ui.unwrapped_outputs))
1510    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[0].lines)
1511    # From manual scroll.
1512    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[1].lines)
1513    self.assertEqual(["bar"] * 4, ui.unwrapped_outputs[2].lines)
1514    # From history navigation.
1515    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[3].lines)
1516    # From history navigation's auto-scroll to history scroll position.
1517    self.assertEqual(["bar"] * 2, ui.unwrapped_outputs[4].lines)
1518    self.assertEqual(["bar"] * 4, ui.unwrapped_outputs[5].lines)
1519
1520    self.assertEqual(6, len(ui.scroll_messages))
1521    self.assertIn("Scroll (PgDn): 0.00%", ui.scroll_messages[0])
1522    self.assertIn("Scroll (PgUp): 100.00%", ui.scroll_messages[1])
1523    self.assertIn("Scroll (PgDn): 0.00%", ui.scroll_messages[2])
1524    self.assertIn("Scroll (PgDn): 0.00%", ui.scroll_messages[3])
1525    self.assertIn("Scroll (PgUp): 100.00%", ui.scroll_messages[4])
1526    self.assertIn("Scroll (PgDn): 0.00%", ui.scroll_messages[5])
1527
1528
1529class ScrollBarTest(test_util.TensorFlowTestCase):
1530
1531  def testConstructorRaisesExceptionForNotEnoughHeight(self):
1532    with self.assertRaisesRegex(ValueError,
1533                                r"Insufficient height for ScrollBar \(2\)"):
1534      curses_ui.ScrollBar(0, 0, 1, 1, 0, 0)
1535
1536  def testLayoutIsEmptyForZeroRow(self):
1537    scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 0, 0)
1538    layout = scroll_bar.layout()
1539    self.assertEqual(["  "] * 8, layout.lines)
1540    self.assertEqual({}, layout.font_attr_segs)
1541
1542  def testLayoutIsEmptyFoOneRow(self):
1543    scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 0, 1)
1544    layout = scroll_bar.layout()
1545    self.assertEqual(["  "] * 8, layout.lines)
1546    self.assertEqual({}, layout.font_attr_segs)
1547
1548  def testClickCommandForOneRowIsNone(self):
1549    scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 0, 1)
1550    self.assertIsNone(scroll_bar.get_click_command(0))
1551    self.assertIsNone(scroll_bar.get_click_command(3))
1552    self.assertIsNone(scroll_bar.get_click_command(7))
1553    self.assertIsNone(scroll_bar.get_click_command(8))
1554
1555  def testLayoutIsCorrectForTopPosition(self):
1556    scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 0, 20)
1557    layout = scroll_bar.layout()
1558    self.assertEqual(["UP"] + ["  "] * 6 + ["DN"], layout.lines)
1559    self.assertEqual(
1560        {0: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
1561         1: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
1562         7: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)]},
1563        layout.font_attr_segs)
1564
1565  def testWidth1LayoutIsCorrectForTopPosition(self):
1566    scroll_bar = curses_ui.ScrollBar(0, 0, 0, 7, 0, 20)
1567    layout = scroll_bar.layout()
1568    self.assertEqual(["U"] + [" "] * 6 + ["D"], layout.lines)
1569    self.assertEqual(
1570        {0: [(0, 1, curses_ui.ScrollBar.BASE_ATTR)],
1571         1: [(0, 1, curses_ui.ScrollBar.BASE_ATTR)],
1572         7: [(0, 1, curses_ui.ScrollBar.BASE_ATTR)]},
1573        layout.font_attr_segs)
1574
1575  def testWidth3LayoutIsCorrectForTopPosition(self):
1576    scroll_bar = curses_ui.ScrollBar(0, 0, 2, 7, 0, 20)
1577    layout = scroll_bar.layout()
1578    self.assertEqual(["UP "] + ["   "] * 6 + ["DN "], layout.lines)
1579    self.assertEqual(
1580        {0: [(0, 3, curses_ui.ScrollBar.BASE_ATTR)],
1581         1: [(0, 3, curses_ui.ScrollBar.BASE_ATTR)],
1582         7: [(0, 3, curses_ui.ScrollBar.BASE_ATTR)]},
1583        layout.font_attr_segs)
1584
1585  def testWidth4LayoutIsCorrectForTopPosition(self):
1586    scroll_bar = curses_ui.ScrollBar(0, 0, 3, 7, 0, 20)
1587    layout = scroll_bar.layout()
1588    self.assertEqual([" UP "] + ["    "] * 6 + ["DOWN"], layout.lines)
1589    self.assertEqual(
1590        {0: [(0, 4, curses_ui.ScrollBar.BASE_ATTR)],
1591         1: [(0, 4, curses_ui.ScrollBar.BASE_ATTR)],
1592         7: [(0, 4, curses_ui.ScrollBar.BASE_ATTR)]},
1593        layout.font_attr_segs)
1594
1595  def testLayoutIsCorrectForBottomPosition(self):
1596    scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 19, 20)
1597    layout = scroll_bar.layout()
1598    self.assertEqual(["UP"] + ["  "] * 6 + ["DN"], layout.lines)
1599    self.assertEqual(
1600        {0: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
1601         6: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
1602         7: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)]},
1603        layout.font_attr_segs)
1604
1605  def testLayoutIsCorrectForMiddlePosition(self):
1606    scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 10, 20)
1607    layout = scroll_bar.layout()
1608    self.assertEqual(["UP"] + ["  "] * 6 + ["DN"], layout.lines)
1609    self.assertEqual(
1610        {0: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
1611         3: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)],
1612         7: [(0, 2, curses_ui.ScrollBar.BASE_ATTR)]},
1613        layout.font_attr_segs)
1614
1615  def testClickCommandsAreCorrectForMiddlePosition(self):
1616    scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 10, 20)
1617    self.assertIsNone(scroll_bar.get_click_command(-1))
1618    self.assertEqual(curses_ui._SCROLL_UP_A_LINE,
1619                     scroll_bar.get_click_command(0))
1620    self.assertEqual(curses_ui._SCROLL_UP,
1621                     scroll_bar.get_click_command(1))
1622    self.assertEqual(curses_ui._SCROLL_UP,
1623                     scroll_bar.get_click_command(2))
1624    self.assertIsNone(scroll_bar.get_click_command(3))
1625    self.assertEqual(curses_ui._SCROLL_DOWN,
1626                     scroll_bar.get_click_command(5))
1627    self.assertEqual(curses_ui._SCROLL_DOWN,
1628                     scroll_bar.get_click_command(6))
1629    self.assertEqual(curses_ui._SCROLL_DOWN_A_LINE,
1630                     scroll_bar.get_click_command(7))
1631    self.assertIsNone(scroll_bar.get_click_command(8))
1632
1633  def testClickCommandsAreCorrectForBottomPosition(self):
1634    scroll_bar = curses_ui.ScrollBar(0, 0, 1, 7, 19, 20)
1635    self.assertIsNone(scroll_bar.get_click_command(-1))
1636    self.assertEqual(curses_ui._SCROLL_UP_A_LINE,
1637                     scroll_bar.get_click_command(0))
1638    for i in range(1, 6):
1639      self.assertEqual(curses_ui._SCROLL_UP,
1640                       scroll_bar.get_click_command(i))
1641    self.assertIsNone(scroll_bar.get_click_command(6))
1642    self.assertEqual(curses_ui._SCROLL_DOWN_A_LINE,
1643                     scroll_bar.get_click_command(7))
1644    self.assertIsNone(scroll_bar.get_click_command(8))
1645
1646  def testClickCommandsAreCorrectForScrollBarNotAtZeroMinY(self):
1647    scroll_bar = curses_ui.ScrollBar(0, 5, 1, 12, 10, 20)
1648    self.assertIsNone(scroll_bar.get_click_command(0))
1649    self.assertIsNone(scroll_bar.get_click_command(4))
1650    self.assertEqual(curses_ui._SCROLL_UP_A_LINE,
1651                     scroll_bar.get_click_command(5))
1652    self.assertEqual(curses_ui._SCROLL_UP,
1653                     scroll_bar.get_click_command(6))
1654    self.assertEqual(curses_ui._SCROLL_UP,
1655                     scroll_bar.get_click_command(7))
1656    self.assertIsNone(scroll_bar.get_click_command(8))
1657    self.assertEqual(curses_ui._SCROLL_DOWN,
1658                     scroll_bar.get_click_command(10))
1659    self.assertEqual(curses_ui._SCROLL_DOWN,
1660                     scroll_bar.get_click_command(11))
1661    self.assertEqual(curses_ui._SCROLL_DOWN_A_LINE,
1662                     scroll_bar.get_click_command(12))
1663    self.assertIsNone(scroll_bar.get_click_command(13))
1664
1665
1666if __name__ == "__main__":
1667  googletest.main()
1668