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