# Copyright 2019-2020 Free Software Foundation, Inc. # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # An ANSI terminal emulator for expect. # The expect "spawn" function puts the tty name into the spawn_out # array; but dejagnu doesn't export this globally. So, we have to # wrap spawn with our own function, so that we can capture this value. # The value is later used in calls to stty. proc tuiterm_spawn { args } { set result [uplevel builtin_spawn $args] global gdb_spawn_name upvar spawn_out spawn_out if { [info exists spawn_out] } { set gdb_spawn_name $spawn_out(slave,name) } else { unset gdb_spawn_name } return $result } # Initialize tuiterm.exp environment. proc tuiterm_env_init { } { # Override spawn with tui_spawn. rename spawn builtin_spawn rename tuiterm_spawn spawn } # Finalize tuiterm.exp environment. proc tuiterm_env_finish { } { # Restore spawn. rename spawn tuiterm_spawn rename builtin_spawn spawn } namespace eval Term { variable _rows variable _cols variable _chars variable _cur_x variable _cur_y variable _attrs variable _last_char variable _resize_count # If ARG is empty, return DEF: otherwise ARG. This is useful for # defaulting arguments in CSIs. proc _default {arg def} { if {$arg == ""} { return $def } return $arg } # Erase in the line Y from SX to just before EX. proc _clear_in_line {sx ex y} { variable _attrs variable _chars set lattr [array get _attrs] while {$sx < $ex} { set _chars($sx,$y) [list " " $lattr] incr sx } } # Erase the lines from SY to just before EY. proc _clear_lines {sy ey} { variable _cols while {$sy < $ey} { _clear_in_line 0 $_cols $sy incr sy } } # Beep. proc _ctl_0x07 {} { } # Backspace. proc _ctl_0x08 {} { variable _cur_x incr _cur_x -1 if {$_cur_x < 0} { variable _cur_y variable _cols set _cur_x [expr {$_cols - 1}] incr _cur_y -1 if {$_cur_y < 0} { set _cur_y 0 } } } # Linefeed. proc _ctl_0x0a {} { variable _cur_y variable _rows incr _cur_y 1 if {$_cur_y >= $_rows} { error "FIXME scroll" } } # Carriage return. proc _ctl_0x0d {} { variable _cur_x set _cur_x 0 } # Make room for characters. proc _csi_@ {args} { set n [_default [lindex $args 0] 1] variable _cur_x variable _cur_y variable _chars set in_x $_cur_x set out_x [expr {$_cur_x + $n}] for {set i 0} {$i < $n} {incr i} { set _chars($out_x,$_cur_y) $_chars($in_x,$_cur_y) incr in_x incr out_x } } # Cursor Up. proc _csi_A {args} { variable _cur_y set arg [_default [lindex $args 0] 1] set _cur_y [expr {max ($_cur_y - $arg, 0)}] } # Cursor Down. proc _csi_B {args} { variable _cur_y variable _rows set arg [_default [lindex $args 0] 1] set _cur_y [expr {min ($_cur_y + $arg, $_rows)}] } # Cursor Forward. proc _csi_C {args} { variable _cur_x variable _cols set arg [_default [lindex $args 0] 1] set _cur_x [expr {min ($_cur_x + $arg, $_cols)}] } # Cursor Back. proc _csi_D {args} { variable _cur_x set arg [_default [lindex $args 0] 1] set _cur_x [expr {max ($_cur_x - $arg, 0)}] } # Cursor Next Line. proc _csi_E {args} { variable _cur_x variable _cur_y variable _rows set arg [_default [lindex $args 0] 1] set _cur_x 0 set _cur_y [expr {min ($_cur_y + $arg, $_rows)}] } # Cursor Previous Line. proc _csi_F {args} { variable _cur_x variable _cur_y variable _rows set arg [_default [lindex $args 0] 1] set _cur_x 0 set _cur_y [expr {max ($_cur_y - $arg, 0)}] } # Cursor Horizontal Absolute. proc _csi_G {args} { variable _cur_x variable _cols set arg [_default [lindex $args 0] 1] set _cur_x [expr {min ($arg - 1, $_cols)}] } # Move cursor (don't know the official name of this one). proc _csi_H {args} { variable _cur_x variable _cur_y set _cur_y [expr {[_default [lindex $args 0] 1] - 1}] set _cur_x [expr {[_default [lindex $args 1] 1] - 1}] } # Cursor Forward Tabulation. proc _csi_I {args} { set n [_default [lindex $args 0] 1] variable _cur_x variable _cols incr _cur_x [expr {$n * 8 - $_cur_x % 8}] if {$_cur_x >= $_cols} { set _cur_x [expr {$_cols - 1}] } } # Erase. proc _csi_J {args} { variable _cur_x variable _cur_y variable _rows variable _cols set arg [_default [lindex $args 0] 0] if {$arg == 0} { _clear_in_line $_cur_x $_cols $_cur_y _clear_lines [expr {$_cur_y + 1}] $_rows } elseif {$arg == 1} { _clear_lines 0 [expr {$_cur_y - 1}] _clear_in_line 0 $_cur_x $_cur_y } elseif {$arg == 2} { _clear_lines 0 $_rows } } # Erase Line. proc _csi_K {args} { variable _cur_x variable _cur_y variable _cols set arg [_default [lindex $args 0] 0] if {$arg == 0} { # From cursor to end. _clear_in_line $_cur_x $_cols $_cur_y } elseif {$arg == 1} { _clear_in_line 0 $_cur_x $_cur_y } elseif {$arg == 2} { _clear_in_line 0 $_cols $_cur_y } } # Delete lines. proc _csi_M {args} { variable _cur_y variable _rows variable _cols variable _chars set count [_default [lindex $args 0] 1] set y $_cur_y set next_y [expr {$y + 1}] while {$count > 0 && $next_y < $_rows} { for {set x 0} {$x < $_cols} {incr x} { set _chars($x,$y) $_chars($x,$next_y) } incr y incr next_y incr count -1 } _clear_lines $next_y $_rows } # Erase chars. proc _csi_X {args} { set n [_default [lindex $args 0] 1] # Erase characters but don't move cursor. variable _cur_x variable _cur_y variable _attrs variable _chars set lattr [array get _attrs] set x $_cur_x for {set i 0} {$i < $n} {incr i} { set _chars($x,$_cur_y) [list " " $lattr] incr x } } # Backward tab stops. proc _csi_Z {args} { set n [_default [lindex $args 0] 1] variable _cur_x set _cur_x [expr {max (int (($_cur_x - 1) / 8) * 8 - ($n - 1) * 8, 0)}] } # Repeat. proc _csi_b {args} { variable _last_char set n [_default [lindex $args 0] 1] _insert [string repeat $_last_char $n] } # Line Position Absolute. proc _csi_d {args} { variable _cur_y set _cur_y [expr {[_default [lindex $args 0] 1] - 1}] } # Select Graphic Rendition. proc _csi_m {args} { variable _attrs foreach item $args { switch -exact -- $item { "" - 0 { set _attrs(intensity) normal set _attrs(fg) default set _attrs(bg) default set _attrs(underline) 0 set _attrs(reverse) 0 } 1 { set _attrs(intensity) bold } 2 { set _attrs(intensity) dim } 4 { set _attrs(underline) 1 } 7 { set _attrs(reverse) 1 } 22 { set _attrs(intensity) normal } 24 { set _attrs(underline) 0 } 27 { set _attrs(reverse) 1 } 30 - 31 - 32 - 33 - 34 - 35 - 36 - 37 { set _attrs(fg) $item } 39 { set _attrs(fg) default } 40 - 41 - 42 - 43 - 44 - 45 - 46 - 47 { set _attrs(bg) $item } 49 { set _attrs(bg) default } } } } # Insert string at the cursor location. proc _insert {str} { verbose "INSERT <<$str>>" variable _cur_x variable _cur_y variable _rows variable _cols variable _attrs variable _chars set lattr [array get _attrs] foreach char [split $str {}] { set _chars($_cur_x,$_cur_y) [list $char $lattr] incr _cur_x if {$_cur_x >= $_cols} { set _cur_x 0 incr _cur_y if {$_cur_y >= $_rows} { error "FIXME scroll" } } } } # Initialize. proc _setup {rows cols} { global stty_init set stty_init "rows $rows columns $cols" variable _rows variable _cols variable _cur_x variable _cur_y variable _attrs variable _resize_count set _rows $rows set _cols $cols set _cur_x 0 set _cur_y 0 set _resize_count 0 array set _attrs { intensity normal fg default bg default underline 0 reverse 0 } _clear_lines 0 $_rows } # Accept some output from gdb and update the screen. WAIT_FOR is # a regexp matching the line to wait for. Return 0 on timeout, 1 # on success. proc wait_for {wait_for} { global expect_out global gdb_prompt variable _cur_x variable _cur_y set prompt_wait_for "$gdb_prompt \$" while 1 { gdb_expect { -re "^\[\x07\x08\x0a\x0d\]" { scan $expect_out(0,string) %c val set hexval [format "%02x" $val] verbose "+++ _ctl_0x${hexval}" _ctl_0x${hexval} } -re "^\x1b(\[0-9a-zA-Z\])" { verbose "+++ unsupported escape" error "unsupported escape" } -re "^\x1b\\\[(\[0-9;\]*)(\[a-zA-Z@\])" { set cmd $expect_out(2,string) set params [split $expect_out(1,string) ";"] verbose "+++ _csi_$cmd <<<$expect_out(1,string)>>>" eval _csi_$cmd $params } -re "^\[^\x07\x08\x0a\x0d\x1b\]+" { _insert $expect_out(0,string) variable _last_char set _last_char [string index $expect_out(0,string) end] } timeout { # Assume a timeout means we somehow missed the # expected result, and carry on. return 0 } } # If the cursor appears just after the prompt, return. It # isn't reliable to check this only after an insertion, # because curses may make "unusual" redrawing decisions. if {$wait_for == "$prompt_wait_for"} { set prev [get_line $_cur_y $_cur_x] } else { set prev [get_line $_cur_y] } if {[regexp -- $wait_for $prev]} { if {$wait_for == "$prompt_wait_for"} { break } set wait_for $prompt_wait_for } } return 1 } # Like ::clean_restart, but ensures that gdb starts in an # environment where the TUI can work. ROWS and COLS are the size # of the terminal. EXECUTABLE, if given, is passed to # clean_restart. proc clean_restart {rows cols {executable {}}} { global env stty_init save_vars {env(TERM) stty_init} { setenv TERM ansi _setup $rows $cols if {$executable == ""} { ::clean_restart } else { ::clean_restart $executable } } } # Setup ready for starting the tui, but don't actually start it. # Returns 1 on success, 0 if TUI tests should be skipped. proc prepare_for_tui {} { if {[skip_tui_tests]} { return 0 } gdb_test_no_output "set tui border-kind ascii" gdb_test_no_output "maint set tui-resize-message on" return 1 } # Start the TUI. Returns 1 on success, 0 if TUI tests should be # skipped. proc enter_tui {} { if {![prepare_for_tui]} { return 0 } command_no_prompt_prefix "tui enable" return 1 } # Send the command CMD to gdb, then wait for a gdb prompt to be # seen in the TUI. CMD should not end with a newline -- that will # be supplied by this function. proc command {cmd} { global gdb_prompt send_gdb "$cmd\n" set str [string_to_regexp $cmd] set str "^$gdb_prompt $str" wait_for $str } # As proc command, but don't wait for a initial prompt. This is used for # inital terminal commands, where there's no prompt yet. proc command_no_prompt_prefix {cmd} { send_gdb "$cmd\n" set str [string_to_regexp $cmd] wait_for "^$str" } # Return the text of screen line N, without attributes. Lines are # 0-based. If C is given, stop before column C. Columns are also # zero-based. proc get_line {n {c ""}} { variable _rows # This can happen during resizing, if the cursor seems to # temporarily be off-screen. if {$n >= $_rows} { return "" } set result "" variable _cols variable _chars set c [_default $c $_cols] set x 0 while {$x < $c} { append result [lindex $_chars($x,$n) 0] incr x } return $result } # Get just the character at (X, Y). proc get_char {x y} { variable _chars return [lindex $_chars($x,$y) 0] } # Get the entire screen as a string. proc get_all_lines {} { variable _rows variable _cols variable _chars set result "" for {set y 0} {$y < $_rows} {incr y} { for {set x 0} {$x < $_cols} {incr x} { append result [lindex $_chars($x,$y) 0] } append result "\n" } return $result } # Get the text just before the cursor. proc get_current_line {} { variable _cur_x variable _cur_y return [get_line $_cur_y $_cur_x] } # Helper function for check_box. Returns empty string if the box # is found, description of why not otherwise. proc _check_box {x y width height} { set x2 [expr {$x + $width - 1}] set y2 [expr {$y + $height - 1}] if {[get_char $x $y] != "+"} { return "ul corner" } if {[get_char $x $y2] != "+"} { return "ll corner" } if {[get_char $x2 $y] != "+"} { return "ur corner" } if {[get_char $x2 $y2] != "+"} { return "lr corner" } # Note we do not check the full horizonal borders of the box. # The top will contain a title, and the bottom may as well, if # it is overlapped by some other border. However, at most a # title should appear as '+-VERY LONG TITLE-+', so we can # check for the '+-' on the left, and '-+' on the right. if {[get_char [expr {$x + 1}] $y] != "-"} { return "ul title padding" } if {[get_char [expr {$x2 - 1}] $y] != "-"} { return "ul title padding" } # Now check the vertical borders. for {set i [expr {$y + 1}]} {$i < $y2 - 1} {incr i} { if {[get_char $x $i] != "|"} { return "left side $i" } if {[get_char $x2 $i] != "|"} { return "right side $i" } } return "" } # Check for a box at the given coordinates. proc check_box {test_name x y width height} { set why [_check_box $x $y $width $height] if {$why == ""} { pass $test_name } else { dump_screen fail "$test_name ($why)" } } # Check whether the text contents of the terminal match the # regular expression. Note that text styling is not considered. proc check_contents {test_name regexp} { set contents [get_all_lines] if {![gdb_assert {[regexp -- $regexp $contents]} $test_name]} { dump_screen } } # Check the contents of a box on the screen. This is a little # like check_contents, but doens't check the whole screen # contents, only the contents of a single box. This procedure # includes (effectively) a call to check_box to ensure there is a # box where expected, if there is then the contents of the box are # matched against REGEXP. proc check_box_contents {test_name x y width height regexp} { variable _chars set why [_check_box $x $y $width $height] if {$why != ""} { dump_screen fail "$test_name (box check: $why)" return } # Now grab the contents of the box, join each line together # with a newline character and match against REGEXP. set result "" for {set yy [expr {$y + 1}]} {$yy < [expr {$y + $height - 1}]} {incr yy} { for {set xx [expr {$x + 1}]} {$xx < [expr {$x + $width - 1}]} {incr xx} { append result [lindex $_chars($xx,$yy) 0] } append result "\n" } if {![gdb_assert {[regexp -- $regexp $result]} $test_name]} { dump_screen } } # A debugging function to dump the current screen, with line # numbers. proc dump_screen {} { variable _rows variable _cols verbose -log "Screen Dump ($_cols x $_rows):" for {set y 0} {$y < $_rows} {incr y} { set fmt [format %5d $y] verbose -log "$fmt [get_line $y]" } } # Resize the terminal. proc _do_resize {rows cols} { variable _chars variable _rows variable _cols set old_rows [expr {min ($_rows, $rows)}] set old_cols [expr {min ($_cols, $cols)}] # Copy locally. array set local_chars [array get _chars] unset _chars set _rows $rows set _cols $cols _clear_lines 0 $_rows for {set x 0} {$x < $old_cols} {incr x} { for {set y 0} {$y < $old_rows} {incr y} { set _chars($x,$y) $local_chars($x,$y) } } } proc resize {rows cols} { variable _rows variable _cols variable _resize_count global gdb_spawn_name # expect handles each argument to stty separately. This means # that gdb will see SIGWINCH twice. Rather than rely on this # behavior (which, after all, could be changed), we make it # explicit here. This also simplifies waiting for the redraw. _do_resize $rows $_cols stty rows $_rows < $gdb_spawn_name # Due to the strange column resizing behavior, and because we # don't care about this intermediate resize, we don't check # the size here. wait_for "@@ resize done $_resize_count" incr _resize_count # Somehow the number of columns transmitted to gdb is one less # than what we request from expect. We hide this weird # details from the caller. _do_resize $_rows $cols stty columns [expr {$_cols + 1}] < $gdb_spawn_name wait_for "@@ resize done $_resize_count, size = ${_cols}x${rows}" incr _resize_count } }