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