tfdbg: Fix a bug related to regex search and scrolling when there is line wrapping

The issue: Prior to this CL, if there are is wrapping and you do regex search and scrolling, the output pad will scroll according to line indices in the original (unwrapped) text lines instead of the wrapped text lines, which misses the actual matches. This CL fixes this issue.
Change: 138191265
This commit is contained in:
Shanqing Cai 2016-11-04 07:15:40 -08:00 committed by TensorFlower Gardener
parent 59daeca315
commit 9ec30b9727
4 changed files with 85 additions and 20 deletions

View File

@ -168,7 +168,7 @@ class CursesUI(object):
# Regex search state.
self._curr_search_regex = None
self._regex_match_lines = None
self._unwrapped_regex_match_lines = []
# Size of view port on screen, which is always smaller or equal to the
# screen size.
@ -433,7 +433,7 @@ class CursesUI(object):
self._curr_search_regex = regex
self._display_output(self._curr_unwrapped_output, highlight_regex=regex)
elif self._regex_match_lines:
elif self._unwrapped_regex_match_lines:
# Command is "/". Continue scrolling down matching lines.
self._display_output(
self._curr_unwrapped_output,
@ -682,11 +682,15 @@ class CursesUI(object):
output: (RichTextLines) text lines to display on the screen. These lines
may have widths exceeding the screen width. This method will take care
of the wrapping.
Returns:
(List of int) A list of line indices, in the wrapped output, where there
are regex matches.
"""
# Wrap the output lines according to screen width.
self._curr_wrapped_output = debugger_cli_common.wrap_rich_text_lines(
output, self._max_x - 1)
self._curr_wrapped_output, wrapped_line_indices = (
debugger_cli_common.wrap_rich_text_lines(output, self._max_x - 1))
# Append lines to curr_wrapped_output so that the user can scroll to a
# state where the last text line is on the top of the output area.
@ -706,13 +710,20 @@ class CursesUI(object):
self._output_pad_width) = self._display_lines(self._curr_wrapped_output,
self._output_num_rows)
# The indices of lines with regex matches (if any) need to be mapped to
# indices of wrapped lines.
return [
wrapped_line_indices[line]
for line in self._unwrapped_regex_match_lines
]
def _display_output(self, output, is_refresh=False, highlight_regex=None):
"""Display text output in a scrollable text pad.
This method does some preprocessing on the text lines, render them on the
screen and scroll to the appropriate line. These are done according to regex
highlighting requests (if any), scroll-to-next-match requests (if any),
and screen refrexh requests (if any).
and screen refresh requests (if any).
TODO(cais): Separate these unrelated request to increase clarity and
maintainability.
@ -734,16 +745,17 @@ class CursesUI(object):
if not is_refresh:
# Perform new regex search on the current output.
self._regex_match_lines = output.annotations[
self._unwrapped_regex_match_lines = output.annotations[
debugger_cli_common.REGEX_MATCH_LINES_KEY]
else:
# Continue scrolling down.
self._output_pad_row += 1
else:
self._curr_unwrapped_output = output
self._unwrapped_regex_match_lines = []
# Display output on the screen.
self._screen_display_output(output)
wrapped_regex_match_lines = self._screen_display_output(output)
# Now that the text lines are displayed on the screen scroll to the
# appropriate line according to previous scrolling state and regex search
@ -751,7 +763,7 @@ class CursesUI(object):
if highlight_regex:
next_match_line = -1
for match_line in self._regex_match_lines:
for match_line in wrapped_regex_match_lines:
if match_line >= self._output_pad_row:
next_match_line = match_line
break
@ -1099,7 +1111,7 @@ class CursesUI(object):
0: [(len(candidates_prefix), len(candidates_line), "yellow")]
})
candidates_output = debugger_cli_common.wrap_rich_text_lines(
candidates_output, _ = debugger_cli_common.wrap_rich_text_lines(
candidates_output, self._max_x - 2)
# Calculate how many lines the candidate text should occupy. Limit it to

View File

@ -201,10 +201,17 @@ class CursesTest(test_util.TensorFlowTestCase):
type=int,
default=60,
help="How many times to babble")
ap.add_argument(
"-l",
"--line",
dest="line",
type=str,
default="bar",
help="The content of each line")
parsed = ap.parse_args(args)
return debugger_cli_common.RichTextLines(["bar"] * parsed.num_times)
return debugger_cli_common.RichTextLines([parsed.line] * parsed.num_times)
def _print_ones(self, args, screen_info=None):
ap = argparse.ArgumentParser(
@ -726,7 +733,7 @@ class CursesTest(test_util.TensorFlowTestCase):
"babble", self._babble, "babble some", prefix_aliases=["b"])
ui.run_ui()
# The unwrapped (original) output should never have any highglighting.
# The unwrapped (original) output should never have any highlighting.
self.assertEqual(3, len(ui.unwrapped_outputs))
for i in range(3):
self.assertEqual(["bar"] * 3, ui.unwrapped_outputs[i].lines)
@ -794,6 +801,32 @@ class CursesTest(test_util.TensorFlowTestCase):
self.assertEqual([0, 0, 1, 2], ui.output_pad_rows)
def testRegexSearchUnderLineWrapping(self):
ui = MockCursesUI(
40,
5, # Use a narrow window to trigger line wrapping
command_sequence=[
string_to_codes("babble -n 3 -l foo-bar-baz-qux\n"),
string_to_codes("/foo\n"), # Regex search and highlight.
string_to_codes("/\n"), # Continue scrolling down: 1st time.
string_to_codes("/\n"), # Continue scrolling down: 2nd time.
string_to_codes("/\n"), # Continue scrolling down: 3rd time.
string_to_codes("/\n"), # Continue scrolling down: 4th time.
self._EXIT
])
ui.register_command_handler(
"babble", self._babble, "babble some")
ui.run_ui()
self.assertEqual(4, len(ui.wrapped_outputs))
for wrapped_output in ui.wrapped_outputs:
self.assertEqual(["foo-", "bar-", "baz-", "qux"] * 3,
wrapped_output.lines[0 : 12])
# The scroll location should reflect the line wrapping.
self.assertEqual([0, 0, 4, 8], ui.output_pad_rows)
def testRegexSearchNoMatchContinuation(self):
"""Test continuing scrolling when there is no regex match."""
@ -811,8 +844,8 @@ class CursesTest(test_util.TensorFlowTestCase):
"babble", self._babble, "babble some", prefix_aliases=["b"])
ui.run_ui()
# The regex search and continuation search should not have produced any
# output.
# The regex search and continuation search in the 3rd command should not
# have produced any output.
self.assertEqual(1, len(ui.unwrapped_outputs))
self.assertEqual([0], ui.output_pad_rows)

View File

@ -223,12 +223,16 @@ def wrap_rich_text_lines(inp, cols):
cols: Number of columns, as an int.
Returns:
A new instance of RichTextLines, with line lengths limited to cols.
1) A new instance of RichTextLines, with line lengths limited to cols.
2) A list of new (wrapped) line index. For example, if the original input
consists of three lines and only the second line is wrapped, and it's
wrapped into two lines, this return value will be: [0, 1, 3].
Raises:
ValueError: If inputs have invalid types.
"""
new_line_indices = []
if not isinstance(inp, RichTextLines):
raise ValueError("Invalid type of input screen_output")
@ -239,6 +243,8 @@ def wrap_rich_text_lines(inp, cols):
row_counter = 0 # Counter for new row index
for i in xrange(len(inp.lines)):
new_line_indices.append(out.num_lines())
line = inp.lines[i]
if i in inp.annotations:
@ -298,7 +304,7 @@ def wrap_rich_text_lines(inp, cols):
if not isinstance(key, int):
out.annotations[key] = inp.annotations[key]
return out
return out, new_line_indices
class CommandHandlerRegistry(object):

View File

@ -453,16 +453,18 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
def testNoActualWrapping(self):
# Large column limit should lead to no actual wrapping.
out = debugger_cli_common.wrap_rich_text_lines(self._orig_screen_output,
100)
out, new_line_indices = debugger_cli_common.wrap_rich_text_lines(
self._orig_screen_output, 100)
self.assertEqual(self._orig_screen_output.lines, out.lines)
self.assertEqual(self._orig_screen_output.font_attr_segs,
out.font_attr_segs)
self.assertEqual(self._orig_screen_output.annotations, out.annotations)
self.assertEqual(new_line_indices, [0, 1, 2])
def testWrappingWithAttrCutoff(self):
out = debugger_cli_common.wrap_rich_text_lines(self._orig_screen_output, 11)
out, new_line_indices = debugger_cli_common.wrap_rich_text_lines(
self._orig_screen_output, 11)
# Add non-row-index field to out.
out.annotations["metadata"] = "foo"
@ -493,6 +495,8 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
# Chec that the non-row-index field is present in output.
self.assertEqual("foo", out.annotations["metadata"])
self.assertEqual(new_line_indices, [0, 1, 3])
def testWrappingWithMultipleAttrCutoff(self):
self._orig_screen_output = debugger_cli_common.RichTextLines(
["Folk song:", "Roses are red", "Violets are blue"],
@ -501,7 +505,8 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
annotations={1: "longer wavelength",
2: "shorter wavelength"})
out = debugger_cli_common.wrap_rich_text_lines(self._orig_screen_output, 5)
out, new_line_indices = debugger_cli_common.wrap_rich_text_lines(
self._orig_screen_output, 5)
# Check wrapped text.
self.assertEqual(9, len(out.lines))
@ -537,6 +542,8 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
self.assertFalse(7 in out.annotations)
self.assertFalse(8 in out.annotations)
self.assertEqual(new_line_indices, [0, 2, 5])
def testWrappingInvalidArguments(self):
with self.assertRaisesRegexp(ValueError,
"Invalid type of input screen_output"):
@ -546,6 +553,13 @@ class WrapScreenOutputTest(test_util.TensorFlowTestCase):
debugger_cli_common.wrap_rich_text_lines(
debugger_cli_common.RichTextLines(["foo", "bar"]), "12")
def testWrappingEmptyInput(self):
out, new_line_indices = debugger_cli_common.wrap_rich_text_lines(
debugger_cli_common.RichTextLines([]), 10)
self.assertEqual([], out.lines)
self.assertEqual([], new_line_indices)
class SliceRichTextLinesText(test_util.TensorFlowTestCase):