• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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."""
16from __future__ import absolute_import
17from __future__ import division
18from __future__ import print_function
19
20import argparse
21import curses
22import tempfile
23import threading
24
25import numpy as np
26from six.moves import queue
27
28from tensorflow.python.debug.cli import cli_test_utils
29from tensorflow.python.debug.cli import curses_ui
30from tensorflow.python.debug.cli import debugger_cli_common
31from tensorflow.python.debug.cli import tensor_format
32from tensorflow.python.framework import test_util
33from tensorflow.python.platform import gfile
34from tensorflow.python.platform import googletest
35
36
37def string_to_codes(cmd):
38  return [ord(c) for c in cmd]
39
40
41def 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
46class 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
230class 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
1527class 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
1664if __name__ == "__main__":
1665  googletest.main()
1666