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