From b24b2962040f63129af293835f9d5f2e353f6cad Mon Sep 17 00:00:00 2001 From: Remko Popma Date: Mon, 13 Jan 2020 09:46:46 +0900 Subject: [PATCH] [#906] initial tests to verify bash TAB completion with DejaGNU and Expect --- .gitignore | 8 + src/test/dejagnu.tests/README.adoc | 47 + src/test/dejagnu.tests/completion/basic.exp | 27 + .../completion/picocompletion.exp | 25 + src/test/dejagnu.tests/config/bashrc | 54 ++ src/test/dejagnu.tests/config/default.exp | 24 + src/test/dejagnu.tests/config/inputrc | 18 + src/test/dejagnu.tests/config/unix.exp | 7 + src/test/dejagnu.tests/lib/completion.exp | 26 + .../lib/completions/basicExample.exp | 38 + .../lib/completions/picocompletion-demo.exp | 55 ++ src/test/dejagnu.tests/lib/library.exp | 873 ++++++++++++++++++ src/test/dejagnu.tests/lib/library.sh | 38 + src/test/dejagnu.tests/lib/unit.exp | 25 + src/test/dejagnu.tests/run | 67 ++ src/test/dejagnu.tests/runCompletion | 8 + 16 files changed, 1340 insertions(+) create mode 100644 src/test/dejagnu.tests/README.adoc create mode 100644 src/test/dejagnu.tests/completion/basic.exp create mode 100644 src/test/dejagnu.tests/completion/picocompletion.exp create mode 100644 src/test/dejagnu.tests/config/bashrc create mode 100644 src/test/dejagnu.tests/config/default.exp create mode 100644 src/test/dejagnu.tests/config/inputrc create mode 100644 src/test/dejagnu.tests/config/unix.exp create mode 100644 src/test/dejagnu.tests/lib/completion.exp create mode 100644 src/test/dejagnu.tests/lib/completions/basicExample.exp create mode 100644 src/test/dejagnu.tests/lib/completions/picocompletion-demo.exp create mode 100644 src/test/dejagnu.tests/lib/library.exp create mode 100644 src/test/dejagnu.tests/lib/library.sh create mode 100644 src/test/dejagnu.tests/lib/unit.exp create mode 100644 src/test/dejagnu.tests/run create mode 100644 src/test/dejagnu.tests/runCompletion diff --git a/.gitignore b/.gitignore index a12b5d5e..40e5db1f 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,11 @@ picocli.iml /picocli-shell-jline2/out/ /picocli-shell-jline3/out/ /picocli-spring-boot-starter/out/ +/src/test/dejagnu.tests/xtrace.log +/src/test/dejagnu.tests/log/completion.sum +/src/test/dejagnu.tests/log/completion.log +/src/test/dejagnu.tests/completion.log +/src/test/dejagnu.tests/completion.sum +/src/test/dejagnu.tests/testrun.log +/src/test/dejagnu.tests/testrun.sum +/src/test/dejagnu.tests/tmp/ diff --git a/src/test/dejagnu.tests/README.adoc b/src/test/dejagnu.tests/README.adoc new file mode 100644 index 00000000..46c591ff --- /dev/null +++ b/src/test/dejagnu.tests/README.adoc @@ -0,0 +1,47 @@ += Testing Completion with Expect and DejaGNU + +The files in this directory allow testing bash TAB completion. +The tests use the https://www.gnu.org/software/dejagnu/[DejaGNU] framework, +which is written in https://www.nist.gov/services-resources/software/expect[Expect]. +Expect in turn uses http://tcl.sourceforge.net/[Tcl] -- Tool command language. + +The tests work by starting a bash session with a predictable configuration, +then sourcing some of the completion scripts in `src/test/resources`, +and sending strings to the shell that trigger the shell to print completion candidates. +The testing framework then asserts that these completion candidates are the expected ones. + +== Setup +First, install DejaGNU: + +[source,bash] +---- +sudo apt-get install dejagnu +---- + +That will install the following packages: +dejagnu expect libtcl8.6 tcl-expect tcl8.6. + +To get the `cmdline` and `textutil` packages used in `dejagnu.tests/lib/library.exp`, get `tcllib`: + +[source,bash] +---- +sudo apt-get install tcllib +---- + +== Running the Tests + +I have not yet investigated how to automate this. +For now, just `cd` into the `dejagnu.tests` directory and run the `./runCompletions` script: + +[source,bash] +---- +cd src/tests/dejagnu.tests +./runCompletion -v # -v gives verbose output into the log directory +---- + +== TODO + +* Ensure these tests are run automatically as part of the Gradle build +* Log files should be created in the `build` directory, not in `src/tests/dejagnu.tests/log` +* Figure out how the tests can be started from any directory + diff --git a/src/test/dejagnu.tests/completion/basic.exp b/src/test/dejagnu.tests/completion/basic.exp new file mode 100644 index 00000000..ff78045b --- /dev/null +++ b/src/test/dejagnu.tests/completion/basic.exp @@ -0,0 +1,27 @@ +# By calling the ./runCompletion script (or runtest --tool=completion), +# we ensure the tests are driven by the dejagnu framework: +# +# Before this script is invoked: +# * proc completion_init in dejagnu.tests/lib/completion.exp is called; +# * the first time completion_init is called, completion_start is called; +# * this in turn starts a bash session and initializes it to give predictable behaviour: +# (see dejagnu.tests/config/bashrc and dejagnu.tests/config/inputrc for details) +# +# Next, this script is called: +# 1. source the completion script +# 2. call proc assert_source_completions in dejagnu.tests/lib/library.exp (line 420) +# This verifies that _some_ completion for the specified command is defined, and then +# 3. executes the dejagnu.tests/lib/completions/$cmd.exp script. +# This lib/completions/$cmd.exp script is where the completions are verified. +set test "basic TAB completion" +set script "$srcdir/../resources/basic.bash" +#set script "/mnt/c/Users/remko/IdeaProjects/picocli3/src/test/resources/basic.bash" + +verbose "sending 'source $script\r' to the shell..." +send "source $script\r" + +set cmd "basicExample" +assert_source_completions "$cmd" + + + diff --git a/src/test/dejagnu.tests/completion/picocompletion.exp b/src/test/dejagnu.tests/completion/picocompletion.exp new file mode 100644 index 00000000..d6f22933 --- /dev/null +++ b/src/test/dejagnu.tests/completion/picocompletion.exp @@ -0,0 +1,25 @@ +# By calling the ./runCompletion script (or runtest --tool=completion), +# we ensure the tests are driven by the dejagnu framework: +# +# Before this script is invoked: +# * proc completion_init in dejagnu.tests/lib/completion.exp is called; +# * the first time completion_init is called, completion_start is called; +# * this in turn starts a bash session and initializes it to give predictable behaviour: +# (see dejagnu.tests/config/bashrc and dejagnu.tests/config/inputrc for details) +# +# Next, this script is called: +# 1. source the completion script +# 2. call proc assert_source_completions in dejagnu.tests/lib/library.exp (line 420) +# This verifies that _some_ completion for the specified command is defined, and then +# 3. executes the dejagnu.tests/lib/completions/$cmd.exp script. +# This lib/completions/$cmd.exp script is where the completions are verified. + +set test "TAB completion" +set script "$srcdir/../resources/picocompletion-demo_completion.bash" +#set script "/mnt/c/Users/remko/IdeaProjects/picocli3/src/test/resources/picocompletion-demo_completion.bash" + +verbose "sending 'source $script\r' to the shell..." +send "source $script\r" + +set cmd "picocompletion-demo" +assert_source_completions "$cmd" diff --git a/src/test/dejagnu.tests/config/bashrc b/src/test/dejagnu.tests/config/bashrc new file mode 100644 index 00000000..63029fb7 --- /dev/null +++ b/src/test/dejagnu.tests/config/bashrc @@ -0,0 +1,54 @@ +# bashrc file for bash-completion test suite + +# Note that we do some initialization that would be too late to do here in +# library.exp's start_bash() and conftest.py. + +# Use emacs key bindings +set -o emacs + +# Use bash strict mode +#set -o posix + +# Unset `command_not_found_handle' as defined on Debian/Ubuntu, because this +# troubles and slows down testing +unset -f command_not_found_handle + +export TESTDIR=$(pwd) +mkdir -p $TESTDIR/tmp + +export PS2='> ' + +# Also test completions of system administrator commands, which are +# installed via the same PATH expansion in `bash_completion.have()' +export PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin + +# ...as well as games on some systems not in PATH by default: +export PATH=$PATH:/usr/games:/usr/local/games + +# For clean test state, avoid sourcing user's ~/.bash_completion +export BASH_COMPLETION_USER_FILE=/dev/null + +# ...and avoid stuff in BASH_COMPLETION_USER_DIR and system install locations +# overriding in-tree completions. Setting the user dir would otherwise suffice, +# but simple xspec completions are only installed if a separate one is not +# found in any completion dirs. Therefore we also point the "system" dirs to +# locations that should not yield valid completions and helpers paths either. +export BASH_COMPLETION_USER_DIR=$(cd "$SRCDIR/.."; pwd) +# /var/empty isn't necessarily actually always empty :P +export BASH_COMPLETION_COMPAT_DIR=/var/empty/bash_completion.d +export XDG_DATA_DIRS=/var/empty + +# Make sure default settings are in effect +unset -v \ + COMP_CONFIGURE_HINTS \ + COMP_CVS_REMOTE \ + COMP_KNOWN_HOSTS_WITH_HOSTFILE \ + COMP_TAR_INTERNAL_PATHS + +# Load bash testsuite helper functions +. $SRCDIR/lib/library.sh + +# Local variables: +# mode: shell-script +# End: +# ex: filetype=sh diff --git a/src/test/dejagnu.tests/config/default.exp b/src/test/dejagnu.tests/config/default.exp new file mode 100644 index 00000000..de141704 --- /dev/null +++ b/src/test/dejagnu.tests/config/default.exp @@ -0,0 +1,24 @@ +# Set default expect fallback routines +expect_after { + eof { + if {[info exists test]} { + fail "$test at eof" + } elseif {[info level] > 0} { + fail "[info level 1] at eof" + } else { + fail "eof" + } + } + timeout { + if {[info exists test]} { + fail "$test at timeout" + } elseif {[info level] > 0} { + fail "[info level 1] at timeout" + } else { + fail "timeout" + } + } +} + +verbose "config/default.exp just got loaded..." + diff --git a/src/test/dejagnu.tests/config/inputrc b/src/test/dejagnu.tests/config/inputrc new file mode 100644 index 00000000..5992491a --- /dev/null +++ b/src/test/dejagnu.tests/config/inputrc @@ -0,0 +1,18 @@ +# Readline init file for DejaGnu testsuite +# See: info readline + + # Press TAB once (instead of twice) to auto-complete +set show-all-if-ambiguous on + # No bell. No ^G in output +set bell-style none + # Don't query user about viewing the number of possible completions +set completion-query-items -1 + # Display completions sorted horizontally, not vertically +set print-completions-horizontally on + # Don't use pager when showing completions +set page-completions off + +# Local variables: +# mode: shell-script +# End: +# ex: filetype=sh diff --git a/src/test/dejagnu.tests/config/unix.exp b/src/test/dejagnu.tests/config/unix.exp new file mode 100644 index 00000000..dcced125 --- /dev/null +++ b/src/test/dejagnu.tests/config/unix.exp @@ -0,0 +1,7 @@ +# Unsure why this file is necessary; without it we get this warning: +# WARNING: Couldn't find tool config file for unix, using default. +# Loading /usr/share/dejagnu/config/default.exp +proc completion_exit {} {} +proc completion_version {} {} + +verbose "config/unix.exp just got loaded..." diff --git a/src/test/dejagnu.tests/lib/completion.exp b/src/test/dejagnu.tests/lib/completion.exp new file mode 100644 index 00000000..7b26dea7 --- /dev/null +++ b/src/test/dejagnu.tests/lib/completion.exp @@ -0,0 +1,26 @@ +source $::srcdir/lib/library.exp + + +proc completion_exit {} { + send "\rexit\r" +} + + +proc completion_init {test_file_name} { + verbose "Entered completion_init..." + # Call completion_start() only once + if {! [info exists ::BASH_VERSINFO]} { + verbose "Calling completion_start..." + completion_start + } +} + + +proc completion_start {} { + start_interactive_test +} + + +proc completion_version {} { + puts "$::TESTDIR, bash-$::BASH_VERSION" +} diff --git a/src/test/dejagnu.tests/lib/completions/basicExample.exp b/src/test/dejagnu.tests/lib/completions/basicExample.exp new file mode 100644 index 00000000..d8bfeb13 --- /dev/null +++ b/src/test/dejagnu.tests/lib/completions/basicExample.exp @@ -0,0 +1,38 @@ +proc setup {} { + save_env +} + +proc teardown {} { + assert_env_unmodified {/OLDPWD=/d} +} + +setup + + + # Try completion with one hyphen +set cmd "basicExample -" +set test "Tab should complete '$cmd'" +verbose "Sending $cmd\t..." +send "$cmd\t" +#exp_internal 1 # tell Expect to print diagnostics on its internal operations +expect { + -re "--timeUnit\\s*--timeout\\s*-t\\s*-u\r\n/@$cmd" { pass "'$cmd' COMPLETED OK"} + -re /@ { unresolved "$test at prompt" } + default { unresolved "$test" } +} +sync_after_int + + # Try completion with two hyphens +set cmd "basicExample --" +set test "Tab should complete '$cmd'" +verbose "Sending ${cmd}\t..." +send "$cmd\t" +expect { + -re "--timeUnit\\s*--timeout\r\n/@${cmd}time" { pass "'$cmd' COMPLETED OK"} + -re /@ { unresolved "$test at prompt" } + default { unresolved "$test" } +} +sync_after_int + + +teardown diff --git a/src/test/dejagnu.tests/lib/completions/picocompletion-demo.exp b/src/test/dejagnu.tests/lib/completions/picocompletion-demo.exp new file mode 100644 index 00000000..30fc2491 --- /dev/null +++ b/src/test/dejagnu.tests/lib/completions/picocompletion-demo.exp @@ -0,0 +1,55 @@ +proc setup {} { + save_env +} + +proc teardown {} { + assert_env_unmodified {/OLDPWD=/d} +} + +setup + + + # Try completion without args +set cmd "picocompletion-demo " +set test "Tab should complete '${cmd}'" +verbose "Sending ${cmd}\t..." +send "${cmd}\t" + +expect "${cmd}\r\n" +expect { + -re "sub1\\s*sub2\r\n/@${cmd}sub" { pass "$test: we got sub1 and sub2" } + -re /@ { unresolved "$test at prompt" } + default { unresolved "$test" } +} +sync_after_int + + # Try completion for sub1 without options +set cmd " picocompletion-demo sub1 " +set test "Tab should not show completions for '${cmd}'" +verbose "Sending ${cmd}\t..." +send "${cmd}\t" + +set timeout 1 +expect "${cmd}\r\n" +expect { + -re "${cmd}\t\[^\\s]*" { fail "$test: we got $expect_out(0,string)" } + default { pass "$test" } +} +sync_after_int + + # Try completion for sub1 +set cmd " picocompletion-demo sub1 -" +set test "Tab should complete '${cmd}'" +verbose "Sending ${cmd}\t..." +send "${cmd}\t" + +set timeout 10 +expect "${cmd}\r\n" +expect { + -re "--candidates\\s*--num\\s*--str\r\n/@${cmd}-" { pass "$test: we got options for sub1" } + -re /@ { unresolved "$test at prompt" } + default { unresolved "$test" } +} +sync_after_int + +teardown diff --git a/src/test/dejagnu.tests/lib/library.exp b/src/test/dejagnu.tests/lib/library.exp new file mode 100644 index 00000000..a9b85c9a --- /dev/null +++ b/src/test/dejagnu.tests/lib/library.exp @@ -0,0 +1,873 @@ +# Source `init.tcl' again to restore the `unknown' procedure +# NOTE: DejaGnu has an old `unknown' procedure which unfortunately disables +# tcl auto-loading. +source [file join [info library] init.tcl] +package require cmdline +package require textutil::string + + + +# Execute a bash command and make sure the exit status is successful. +# If not, output the error message. +# @param string $cmd Bash command line to execute. If empty string (""), the +# exit status of the previously executed bash command will be +# checked; specify `title' to adorn the error message. +# @param string $title (optional) Command title. If empty, `cmd' is used. +# @param string $prompt (optional) Bash prompt. Default is "/@" +# @param mixed $out (optional) Reference to (tcl) variable to hold output. +# If variable equals -1 (default) the bash command is expected +# to return no output. If variable equals 0, any output +# from the bash command is disregarded. +proc assert_bash_exec {{aCmd ""} {title ""} {prompt /@} {out -1}} { + if {$out != 0 && $out != -1} {upvar $out results} + if {[string length $aCmd] != 0} { + send "$aCmd\r" + expect -ex "$aCmd\r\n" + } + if {[string length $title] == 0} {set title $aCmd} + expect -ex $prompt + set results $expect_out(buffer); # Catch output + # Remove $prompt suffix from output + set results [ + string range $results 0 [ + expr [string length $results] - [string length $prompt] - 1 + ] + ] + if {$out == -1 && [string length $results] > 0} { + fail "ERROR Unexpected output from bash command \"$title\"" + } + + set cmd "echo $?" + send "$cmd\r" + expect { + -ex "$cmd\r\n0\r\n$prompt" {} + $prompt {fail "ERROR executing bash command \"$title\""} + } +} + + +# Test `type ...' in bash +# Indicate "unsupported" if `type' exits with error status. +# @param string $command Command to locate +proc assert_bash_type {command} { + set test "$command should be available in bash" + set cmd "type $command &>/dev/null && echo -n 0 || echo -n 1" + send "$cmd\r" + expect "$cmd\r\n" + expect { + -ex 0 { set result true } + -ex 1 { set result false; unsupported "$test" } + } + expect "/@" + return $result +} + + +# Make sure the expected list matches the real list, as returned by executing +# the specified bash command. +# Specify `-sort' if the real list is sorted. +# @param list $expected Expected list items +# @param string $cmd Bash command to execute in order to generate real list +# items +# @param string $test Test title. Becomes "$cmd should show expected output" +# if empty string. +# @param list $args Options: +# -sort Compare list sorted. Default is unsorted +# -prompt Bash prompt. Default is `/@' +# -chunk-size N Compare list N items at a time. Default +# is 20. +proc assert_bash_list {expected cmd test {args {}}} { + array set arg [::cmdline::getoptions args { + {sort "compare list sorted"} + {prompt.arg /@ "bash prompt"} + {chunk-size.arg 20 "compare N list items at a time"} + }] + set prompt $arg(prompt) + if {$test == ""} {set test "$cmd should show expected output"} + if {[llength $expected] == 0} { + assert_no_output $cmd $test $prompt + } else { + send "$cmd\r" + expect -ex "$cmd\r\n" + if {$arg(sort)} {set bash_sort "-bash-sort"} {set bash_sort ""} + if {[ + eval match_items \$expected $bash_sort -chunk-size \ + \$arg(chunk-size) -end-newline -end-prompt \ + -prompt \$prompt + ]} { + pass "$test" + } else { + fail "$test" + } + } +} + + +# Make sure the expected items are returned by TAB-completing the specified +# command. If the number of expected items is one, expected is: +# +# $cmd$expected[] +# +# SPACE is not expected if -nospace is specified. +# +# If the number of expected items is greater than one, expected is: +# +# $cmd\n +# $expected\n +# $prompt + ($cmd - AUTO) + longest-common-prefix-of-$expected +# +# AUTO is calculated like this: If $cmd ends with non-whitespace, and +# the last argument of $cmd equals the longest-common-prefix of +# $expected, $cmd minus this argument will be expected. +# +# If the algorithm above fails, you can manually specify the CWORD to be +# subtracted from $cmd specifying `-expect-cmd-minus CWORD'. Known cases where +# this is useful are when: +# - the last whitespace is escaped, e.g. "finger foo\ " or "finger +# 'foo " +# +# @param list $expected Expected completions. +# @param string $cmd Command given to generate items +# @param string $test Test title +# @param list $args Options: +# -prompt PROMPT Bash prompt. Default is `/@' +# -chunk-size CHUNK-SIZE Compare list CHUNK-SIZE items at +# a time. Default is 20. +# -nospace Don't expect space character to be output after completion match. +# Valid only if a single completion is expected. +# -expect-cmd-minus DWORD Expect $cmd minus DWORD to be echoed. +# Expected is: +# +# $cmd\n +# $expected\n +# $prompt + ($cmd - DWORD) + longest-common-prefix-of-$expected +# +proc assert_complete {expected cmd {test ""} {args {}}} { + set args_orig $args + array set arg [::cmdline::getoptions args { + {prompt.arg "/@" "bash prompt"} + {chunk-size.arg 20 "compare N list items at a time"} + {nospace "don't expect space after completion"} + {expect-cmd-minus.arg "" "Expect cmd minus DWORD after prompt"} + }] + if {[llength $expected] == 0} { + assert_no_complete $cmd $test + } elseif {[llength $expected] == 1} { + eval assert_complete_one \$expected \$cmd \$test $args_orig + } else { + eval assert_complete_many \$expected \$cmd \$test $args_orig + } +} + + +# Make sure the expected multiple items are returned by TAB-completing the +# specified command. +# @see assert_complete() +proc assert_complete_many {expected cmd {test ""} {args {}}} { + array set arg [::cmdline::getoptions args { + {prompt.arg "/@" "bash prompt"} + {chunk-size.arg 20 "compare N list items at a time"} + {nospace "don't expect space after completion"} + {expect-cmd-minus.arg "" "Expect cmd minus CWORD after prompt"} + }] + if {$test == ""} {set test "$cmd should show completions"} + set prompt $arg(prompt) + set dword "" + if {$arg(expect-cmd-minus) != ""} {set dword $arg(expect-cmd-minus)} + + send "$cmd\t" + expect -ex "$cmd\r\n" + + # Make sure expected items are unique + set expected [lsort -unique $expected] + + # Determine common prefix of completions + set common [::textutil::string::longestCommonPrefixList $expected] + + set cmd2 [_remove_cword_from_cmd $cmd $dword $common] + + set prompt "$prompt$cmd2$common" + if {$arg(nospace)} {set endspace ""} else {set endspace "-end-space"} + set endprompt "-end-prompt" + if {[ + eval match_items \$expected -bash-sort -chunk-size \ + \$arg(chunk-size) $endprompt $endspace -prompt \$prompt + ]} { + pass "$test" + } else { + fail "$test" + } +} + + +# Make sure the expected single item is returned by TAB-completing the +# specified command. +# @see assert_complete() +proc assert_complete_one {expected cmd {test ""} {args {}}} { + array set arg [::cmdline::getoptions args { + {prompt.arg "/@" "bash prompt"} + {chunk-size.arg 20 "compare N list items at a time"} + {nospace "don't expect space after completion"} + {expect-cmd-minus.arg "" "Expect cmd minus CWORD after prompt"} + }] + set prompt $arg(prompt) + + if {$test == ""} {set test "$cmd should show completion"} + send "$cmd\t" + expect -ex "$cmd" + set cur ""; # Default to empty word to complete on + set words [split_words_bash $cmd] + if {[llength $words] > 1} { + # Assume last word of `$cmd' is word to complete on. + set index [expr [llength $words] - 1] + set cur [lindex $words $index] + } + # Remove second word from beginning of $expected + if {[string first $cur $expected] == 0} { + set expected [list [string range $expected [string length $cur] end]] + } + + if {$arg(nospace)} {set endspace ""} else {set endspace "-end-space"} + if {[ + eval match_items \$expected -bash-sort -chunk-size \ + \$arg(chunk-size) $endspace -prompt \$prompt + ]} { + pass "$test" + } else { + fail "$test" + } +} + + +# @param string $cmd Command to remove current-word-to-complete from. +# @param string $dword (optional) Manually specify current-word-to-complete, +# i.e. word to remove from $cmd. If empty string (default), +# `_remove_cword_from_cmd' autodetects if the last argument is the +# current-word-to-complete by checking if $cmd doesn't end with whitespace. +# Specifying `dword' is only necessary if this autodetection fails, e.g. +# when the last whitespace is escaped or quoted, e.g. "finger foo\ " or +# "finger 'foo " +# @param string $common (optional) Common prefix of expected completions. +# @return string Command with current-word-to-complete removed +proc _remove_cword_from_cmd {cmd {dword ""} {common ""}} { + set cmd2 $cmd + # Is $dword specified? + if {[string length $dword] > 0} { + # Remove $dword from end of $cmd + if {[string last $dword $cmd] == [string length $cmd] - [string length $dword]} { + set cmd2 [string range $cmd 0 [expr [string last $dword $cmd] - 1]] + } + } else { + # No, $dword not specified; + # Check if last argument is really a word-to-complete, i.e. + # doesn't end with whitespace. + # NOTE: This check fails if trailing whitespace is escaped or quoted, + # e.g. "finger foo\ " or "finger 'foo ". Specify parameter + # $dword in those cases. + # Is last char whitespace? + if {! [string is space [string range $cmd end end]]} { + # No, last char isn't whitespace; + set cmds [split $cmd] + # Does word-to-complete start with $common? + if {[string first $common [lrange $cmds end end]] == 0} { + # Remove word-to-complete from end of $cmd + set cmd2 [lrange $cmds 0 end-1] + append cmd2 " " + } + } + } + return $cmd2 +} + + +# Escape regexp special characters +proc _escape_regexp_chars {var} { + upvar $var str + regsub -all {([\^$+*?.|(){}[\]\\])} $str {\\\1} str +} + + +# Make sure the expected files are returned by TAB-completing the specified +# command in the specified subdirectory. Be prepared to filter out OLDPWD +# changes when calling assert_env_unmodified() after using this procedure. +# @param list $expected +# @param string $cmd Command given to generate items +# @param string $dir Subdirectory to attempt completion in. The directory must be relative from the $TESTDIR and without a trailing slash. E.g. `fixtures/evince' +# @param string $test Test title +# @param list $args See: assert_complete() +# @result boolean True if successful, False if not +proc assert_complete_dir {expected cmd dir {test ""} {args {}}} { + set prompt "/@" + assert_bash_exec "cd $dir" "" $prompt + eval assert_complete \$expected \$cmd \$test $args + sync_after_int $prompt + assert_bash_exec {cd "$TESTDIR"} +} + + + +# Make sure the bash environment hasn't changed between now and the last call +# to `save_env()'. +# @param string $sed Sed commands to preprocess diff output. +# Example calls: +# +# # Replace `COMP_PATH=.*' with `COMP_PATH=PATH' +# assert_env_unmodified {s/COMP_PATH=.*/COMP_PATH=PATH/} +# +# # Remove lines containing `OLDPWD=' +# assert_env_unmodified {/OLDPWD=/d} +# +# @param string $file Filename to generate environment save file from. See +# `gen_env_filename()'. +# @param string $diff Expected diff output (after being processed by $sed) +# @see save_env() +proc assert_env_unmodified {{sed ""} {file ""} {diff ""}} { + set test "Environment should not be modified" + _save_env [gen_env_filename $file 2] + + # Prepare sed script + + # Escape special bash characters ("\) + regsub -all {([\"\\])} $sed {\\\1} sed; #"# (fix Vim syntax highlighting) + # Escape newlines + regsub -all {\n} [string trim $sed] "\r\n" sed + + # Prepare diff script + + # If diff is filled, escape newlines and make sure it ends with a newline + if {[string length [string trim $diff]]} { + regsub -all {\n} [string trim $diff] "\r\n" diff + append diff "\r\n" + } else { + set diff "" + } + + # Execute diff + + # NOTE: The dummy argument 'LAST-ARG' sets bash variable $_ (last argument) to + # 'LAST-ARG' so that $_ doesn't mess up the diff (as it would if $_ + # was the (possibly multi-lined) sed script). + set cmd "diff_env \"[gen_env_filename $file 1]\" \"[gen_env_filename $file 2]\" \"$sed\" LAST-ARG" + send "$cmd\r" + expect "LAST-ARG\r\n" + + expect { + -re "^$diff[wd]@$" { pass "$test" } + -re [wd]@ { + fail "$test" + + # Show diff to user + + set diff $expect_out(buffer) + # Remove possible `\r\n[wd]@' from end of diff + if {[string last "\r\n[wd]@" $diff] == [string length $diff] - [string length "\r\n[wd]@"]} { + set diff [string range $diff 0 [expr [string last "\r\n[wd]@" $diff] - 1]] + } + send_user $diff; + } + } +} + + +# Check that no completion is attempted on a certain command. +# Params: +# @cmd The command to attempt to complete. +# @test Optional parameter with test name. +proc assert_no_complete {{cmd} {test ""}} { + if {[string length $test] == 0} { + set test "$cmd shouldn't complete" + } + + send "$cmd\t" + expect -ex "$cmd" + + # We can't anchor on $, simulate typing a magical string instead. + set endguard "Magic End Guard" + send "$endguard" + expect { + -re "^$endguard$" { pass "$test" } + default { fail "$test" } + timeout { fail "$test" } + } +} + + +# Check that no output is generated on a certain command. +# @param string $cmd The command to attempt to complete. +# @param string $test Optional parameter with test name. +# @param string $prompt (optional) Bash prompt. Default is "/@" +proc assert_no_output {{cmd} {test ""} {prompt /@}} { + if {[string length $test] == 0} { + set test "$cmd shouldn't generate output" + } + + send "$cmd\r" + expect -ex "$cmd" + + expect { + -re "^\r\n$prompt$" { pass "$test" } + default { fail "$test" } + timeout { fail "$test" } + } +} + + +# Source/run file with additional tests if completion for the specified command +# is installed in bash, and the command is available. +# @param string $command Command to check completion availability for. +# @param string $file (optional) File to source/run. Default is +# "lib/completions/$cmd.exp". +proc assert_source_completions {command {file ""}} { +# if {[assert_bash_type $command] # we will test custom commands +# && [assert_install_completion_for $command]} { ... } + if {[assert_completions_installed_for $command]} { + if {[string length $file] == 0} { + set file "$::srcdir/lib/completions/$command.exp" + } + source $file + } else { + untested $command + } +} + + +# Sort list. +# `exec sort' is used instead of `lsort' to achieve exactly the +# same sort order as in bash. +# @param list $items +# @return list Sort list +proc bash_sort {items} { + return [split [exec sort << [join $items "\n"]] "\n"] +} + + +# Get hostnames +# @param list $args Options: +# -unsorted Do not sort unique. Default is sort unique. +# @return list Hostnames +proc get_hosts {{args {}}} { + array set arg [::cmdline::getoptions args { + {unsorted "do not sort unique"} + }] + set sort "| sort -u" + if {$arg(unsorted)} {set sort ""} + set hosts [exec bash -c "compgen -A hostname $sort"] + # NOTE: Circumventing var `avahi_hosts' and appending directly to `hosts' + # causes an empty element to be inserted in `hosts'. + # -- FVu, Fri Jul 17 23:11:46 CEST 2009 + set avahi_hosts [get_hosts_avahi] + if {[llength $avahi_hosts] > 0} { + lappend hosts $avahi_hosts + } + return $hosts +} + + +# Get hostnames according to avahi +# @return list Hostnames +proc get_hosts_avahi {} { + # Retrieving hosts is successful? + if { [catch {exec bash -c { + type avahi-browse >&/dev/null \ + && avahi-browse -cpr _workstation._tcp 2>/dev/null | command grep ^= | cut -d\; -f7 | sort -u + }} hosts] } { + # No, retrieving hosts yields error; + # Reset hosts + set hosts {} + } + return $hosts +} + + +# Initialize tcl globals with bash variables +proc init_tcl_bash_globals {} { + global BASH_VERSINFO BASH_VERSION COMP_WORDBREAKS LC_CTYPE + assert_bash_exec {printf "%s" "$COMP_WORDBREAKS"} {} /@ COMP_WORDBREAKS + assert_bash_exec {printf "%s " "${BASH_VERSINFO[@]}"} "" /@ BASH_VERSINFO + set BASH_VERSINFO [eval list $BASH_VERSINFO] + assert_bash_exec {printf "%s" "$BASH_VERSION"} "" /@ BASH_VERSION + assert_bash_exec {printf "%s" "$TESTDIR"} "" /@ TESTDIR + assert_bash_exec {eval $(locale); printf "%s" "$LC_CTYPE"} "" /@ LC_CTYPE +} + + +# DEPRECATED +# TODO REMOVE THIS PROCEDURE +# Try installing completion for the specified command. +# @param string $command Command to install completion for. +# @return boolean True (1) if completion is installed, False (0) if not. +proc _install_completion_for {command} { + set test "$command should have completion installed in bash" + set cmd "__load_completion $command && echo -n 0 || echo -n 1" + send "$cmd\r" + expect "$cmd\r\n" + expect { + -ex 0 { set result true } + -ex 1 { set result false } + } + expect "/@" + return $result +} + +# Tests whether completions are installed for the specified command. +# @param string $command Command to verify that completions are installed. +# @return boolean True (1) if completion is installed, False (0) if not. +proc assert_completions_installed_for {command} { + set test "$command should have completion installed in bash" + set cmd "complete -p $command &>/dev/null && echo -n 0 || echo -n 1" + send "$cmd\r" + expect "$cmd\r\n" + expect { + -ex 0 { set result true } + -ex 1 { set result false } + } + expect "/@" + return $result +} + + +# Detect if test suite is running under Cygwin/Windows +proc is_cygwin {} { + expr {[string first [string tolower [exec uname -s]] cygwin] >= 0} +} + + +# Expect items, a limited number (20) at a time. +# Break items into chunks because `expect' seems to have a limited buffer size +# @param list $items Expected list items +# @param list $args Options: +# -bash-sort Compare list bash-sorted. Default is +# unsorted +# -prompt PROMPT Bash prompt. Default is `/@' +# -chunk-size CHUNK-SIZE Compare list CHUNK-SIZE items at +# a time. Default is 20. +# -end-newline Expect newline after last item. +# Default is not. +# -end-prompt Expect prompt after last item. +# Default is not. +# -end-space Expect single space after last item. +# Default is not. Valid only if +# `end-newline' not set. +# @result boolean True if successful, False if not +proc match_items {items {args {}}} { + array set arg [::cmdline::getoptions args { + {bash-sort "compare list sorted"} + {prompt.arg "/@" "bash prompt"} + {chunk-size.arg 20 "compare N list items at a time"} + {end-newline "expect newline after last item"} + {end-prompt "expect prompt after last item"} + {end-space "expect space ater last item"} + }] + set prompt $arg(prompt) + set size $arg(chunk-size) + if {$arg(bash-sort)} {set items [bash_sort $items]} + set result false + for {set i 0} {$i < [llength $items]} {set i [expr {$i + $size}]} { + # For chunks > 1, allow leading whitespace + if {$i > $size} { set expected "\\s*" } else { set expected "" } + for {set j 0} {$j < $size && $i + $j < [llength $items]} {incr j} { + set item "[lindex $items [expr {$i + $j}]]" + _escape_regexp_chars item + append expected $item + if {[llength $items] > 1} {append expected {\s+}} + } + if {[llength $items] == 1} { + if {$arg(end-prompt)} {set end $prompt} {set end ""} + # Both trailing space and newline are specified? + if {$arg(end-newline) && $arg(end-space)} { + # Indicate both trailing space or newline are ok + set expected2 "|^$expected $end$"; # Include space + append expected "\r\n$end"; # Include newline + } else { + if {$arg(end-newline)} {append expected "\r\n$end"} + if {$arg(end-space)} {append expected " $end"} + set expected2 "" + } + expect { + -re "^$expected$$expected2" { set result true } + -re "^$prompt$" {set result false; break } + default { set result false; break } + timeout { set result false; break } + } + } else { + set end "" + if {$arg(end-prompt) && $i + $j == [llength $items]} { + set end "$prompt" + _escape_regexp_chars end + # \$ matches real end of expect_out buffer + set end "$end\$" + } + expect { + -re "^$expected$end" { set result true } + default { set result false; break } + timeout { set result false; break } + } + } + } + return $result +} + + +# Generate filename to save environment to. +# @param string $file File-basename to save environment to. If the file has a +# `.exp' suffix, it is removed. E.g.: +# - "file.exp" becomes "file.env1~" +# - "" becomes "env.env1~" +# - "filename" becomes "filename.env1~" +# The file will be stored in the $TESTDIR/tmp directory. +# @param integer $seq Sequence number. Must be either 1 or 2. +proc gen_env_filename {{file ""} {seq 1}} { + if {[string length $file] == 0} { + set file "env" + } else { + # Remove possible directories + set file [file tail $file] + # Remove possible '.exp' suffix from filename + if {[string last ".exp" $file] == [string length $file] - [string length ".exp"]} { + set file [string range $file 0 [expr [string last ".exp" $file] - 1]] + } + } + return "\$TESTDIR/tmp/$file.env$seq~" +} + + +# Save the environment for later comparison +# @param string $file Filename to generate environment save file from. See +# `gen_env_filename()'. +proc save_env {{file ""}} { + _save_env [gen_env_filename $file 1] +} + + +# Save the environment for later comparison +# @param string File to save the environment to. Default is "$TESTDIR/tmp/env1~". +# @see assert_env_unmodified() +proc _save_env {{file ""}} { + assert_bash_exec "{ (set -o posix ; set); declare -F; shopt -p; set -o; } > \"$file\"" +} + + +# Source bash_completion package +# TODO REMOVE THIS PROCEDURE +proc source_bash_completion {} { + assert_bash_exec {source $(cd "$SRCDIR/.."; pwd)/bash_completion} +} + + +# Split line into words, disregarding backslash escapes (e.g. \b (backspace), +# \g (bell)), but taking backslashed spaces into account. +# Aimed for simulating bash word splitting. +# Example usage: +# +# % set a {f cd\ \be} +# % split_words $a +# f {cd\ \be} +# +# @param string Line to split +# @return list Words +proc split_words_bash {line} { + set words {} + set glue false + foreach part [split $line] { + set glue_next false + # Does `part' end with a backslash (\)? + if {[string last "\\" $part] == [string length $part] - [string length "\\"]} { + # Remove end backslash + set part [string range $part 0 [expr [string length $part] - [string length "\\"] - 1]] + # Indicate glue on next run + set glue_next true + } + # Must `part' be appended to latest word (= glue)? + if {[llength $words] > 0 && [string is true $glue]} { + # Yes, join `part' to latest word; + set zz [lindex $words [expr [llength $words] - 1]] + # Separate glue with backslash-space (\ ); + lset words [expr [llength $words] - 1] "$zz\\ $part" + } else { + # No, don't append word to latest word; + # Append `part' as separate word + lappend words $part + } + set glue $glue_next + } + return $words +} + + +# Given a list of items this proc finds a (part, full) pair so that when +# completing from $part $full will be the only option. +# +# Arguments: +# list The list of full completions. +# partName Output parameter for the partial string. +# fullName Output parameter for the full string, member of item. +# +# Results: +# 1, or 0 if no suitable result was found. +proc find_unique_completion_pair {{list} {partName} {fullName}} { + upvar $partName part + upvar $fullName full + set bestscore 0 + # Uniquify the list, that's what completion does too. + set list [lsort -unique $list] + set n [llength $list] + for {set i 0} {$i < $n} {incr i} { + set cur [lindex $list $i] + set curlen [string length $cur] + + set prev [lindex $list [expr {$i - 1}]] + set next [lindex $list [expr {$i + 1}]] + set diffprev [expr {$prev == ""}] + set diffnext [expr {$next == ""}] + + # Analyse each item of the list and look for the minimum length of the + # partial prefix which is distinct from both $next and $prev. The list + # is sorted so the prefix will be unique in the entire list. + # + # In the worst case we analyse every character in the list 3 times. + # That's actually very fast, sorting could take more. + for {set j 0} {$j < $curlen} {incr j} { + set curchar [string index $cur $j] + if {!$diffprev && [string index $prev $j] != $curchar} { + set diffprev 1 + } + if {!$diffnext && [string index $next $j] != $curchar} { + set diffnext 1 + } + if {$diffnext && $diffprev} { + break + } + } + + # At the end of the loop $j is the index of last character of + # the unique partial prefix. The length is one plus that. + set parlen [expr {$j + 1}] + if {$parlen >= $curlen} { + continue + } + + # Try to find the most "readable pair"; look for a long pair where + # $part is about half of $full. + if {$parlen < $curlen / 2} { + set parlen [expr {$curlen / 2}] + } + set score [expr {$curlen - $parlen}] + if {$score > $bestscore} { + set bestscore $score + set part [string range $cur 0 [expr {$parlen - 1}]] + set full $cur + } + } + return [expr {$bestscore != 0}] +} + + +# Start bash running as test environment. +proc start_bash {} { + global TESTDIR TOOL_EXECUTABLE spawn_id env srcdirabs + set TESTDIR [pwd] + set srcdirabs [file normalize $::srcdir]; # Absolute srcdir + # If `--tool_exec' option not specified, use "bash" + if {! [info exists TOOL_EXECUTABLE]} {set TOOL_EXECUTABLE bash} + set env(SRCDIR) $::srcdir + set env(SRCDIRABS) $::srcdirabs + + # PS1, INPUTRC, TERM and stty columns must be initialized + # *before* starting bash to take proper effect. + + # Set fixed prompt `/@' + set env(PS1) "/@" + # Configure readline + set env(INPUTRC) "$::srcdir/config/inputrc" + # Avoid escape junk at beginning of line from readline, + # see e.g. http://bugs.gentoo.org/246091 + set env(TERM) "dumb" + # Ensure enough columns so expect doesn't have to care about line breaks + set stty_init "columns 150" + + exp_spawn $TOOL_EXECUTABLE --norc + assert_bash_exec {} "$TOOL_EXECUTABLE --norc" + assert_bash_exec "source $::srcdir/config/bashrc" +} + + +# Redirect xtrace output to a file. +# +# 'set -x' can be very useful for debugging but by default it writes to +# stderr. +# +# This function uses file descriptor 6. This will break if any completion +# tries to use the same descriptor. +proc init_bash_xtrace {{fname xtrace.log}} { + verbose "Enabling bash xtrace output to '$fname'" + assert_bash_exec "exec 6>'$fname'" + assert_bash_exec "BASH_XTRACEFD=6" + assert_bash_exec "set -o xtrace" +} + + +# Setup test environment +# +# Common initialization for unit and completion tests. +proc start_interactive_test {} { + start_bash + #source_bash_completion + init_tcl_bash_globals + + global OPT_BASH_XTRACE + if {[info exists OPT_BASH_XTRACE]} { + init_bash_xtrace + } + global OPT_BUFFER_SIZE + if {![info exists OPT_BUFFER_SIZE]} { + set OPT_BUFFER_SIZE 20000 + } + verbose "Changing default expect match buffer size to $OPT_BUFFER_SIZE" + match_max $OPT_BUFFER_SIZE + global OPT_TIMEOUT + if {[info exists OPT_TIMEOUT]} { + global timeout + verbose "Changing default expect timeout from $timeout to $OPT_TIMEOUT" + set timeout $OPT_TIMEOUT + } +} + + +# Interrupt completion and sync with prompt. +# Send signals QUIT & INT. +# @param string $prompt (optional) Bash prompt. Default is "/@" +proc sync_after_int {{prompt /@}} { +# verbose "Entered sync_after_int..." + set test "Sync after INT" + sleep .1 +# send \031\003; # QUIT/INT # OLD: this seemed to cause the issue mentioned in the below link where the first char of the next command is stripped off... + send \x03; # ^C (SIGINT) + # Wait to allow bash to become ready + # See also: http://lists.alioth.debian.org/pipermail/bash-completion-devel/ + # 2010-February/002566.html + sleep .1 + # NOTE: Regexp `.*' causes `expect' to discard previous unknown output. + # This is necessary if a completion doesn't match expectations. + # For instance with `filetype_xspec' completion (e.g. `kdvi') if + # one expects `.txt' as a completion (wrong, because it isn't + # there), the unmatched completions need to be cleaned up. + expect -re ".*\\^C.*$prompt$" +# verbose "Returning from sync_after_int" +} + + +proc sync_after_tab {} { + # NOTE: Wait in case completion returns nothing - because `units' isn't + # installed, so that "^$cdm.*$" doesn't match too early - before + # comp_install has finished + sleep .4 +} + + +# Return current working directory with `TESTDIR' stripped +# @return string Working directory. E.g. /, or /fixtures/ +proc wd {} { + global TESTDIR + # Remove `$TESTDIR' prefix from current working directory + set wd [string replace [pwd] 0 [expr [string length $TESTDIR] - 1]]/ +} diff --git a/src/test/dejagnu.tests/lib/library.sh b/src/test/dejagnu.tests/lib/library.sh new file mode 100644 index 00000000..ed5a85d4 --- /dev/null +++ b/src/test/dejagnu.tests/lib/library.sh @@ -0,0 +1,38 @@ +# Bash library for bash-completion DejaGnu testsuite + + +# @param $1 Char to add to $COMP_WORDBREAKS +add_comp_wordbreak_char() { + [[ "${COMP_WORDBREAKS//[^$1]}" ]] || COMP_WORDBREAKS+=$1 +} # add_comp_wordbreak_char() + + +# Diff environment files to detect if environment is unmodified +# @param $1 File 1 +# @param $2 File 2 +# @param $3 Additional sed script +diff_env() { + diff "$1" "$2" | sed -e " +# Remove diff line indicators + /^[0-9,]\{1,\}[acd]/d +# Remove diff block separators + /---/d +# Remove underscore variable + /[<>] _=/d +# Remove PPID bash variable + /[<>] PPID=/d +# Remove BASH_REMATCH bash variable + /[<>] BASH_REMATCH=/d +# Remove functions starting with underscore + /[<>] declare -f _/d + $3" +} # diff_env() + + +# Output array elements, sorted and separated by newline +# Unset variable after outputting. +# @param $1 Name of array variable to process +echo_array() { + local name=$1[@] + printf "%s\n" "${!name}" | sort +} # echo_array() diff --git a/src/test/dejagnu.tests/lib/unit.exp b/src/test/dejagnu.tests/lib/unit.exp new file mode 100644 index 00000000..e113e1b5 --- /dev/null +++ b/src/test/dejagnu.tests/lib/unit.exp @@ -0,0 +1,25 @@ +source $::srcdir/lib/library.exp + + +proc unit_exit {} { + # Exit bash + send "\rexit\r" +} + + +proc unit_init {test_file_name} { + # Call unit_start() only once + if {! [info exists ::BASH_VERSINFO]} { + unit_start + } +} + + +proc unit_start {} { + start_interactive_test +} + + +proc unit_version {} { + puts "$::TESTDIR, bash-$::BASH_VERSION" +} diff --git a/src/test/dejagnu.tests/run b/src/test/dejagnu.tests/run new file mode 100644 index 00000000..6180bbbf --- /dev/null +++ b/src/test/dejagnu.tests/run @@ -0,0 +1,67 @@ +#!/bin/bash + + +# Print some helpful messages. +usage() { + echo "Run bash-completion tests" + echo + echo "The 'tool' is determined automatically from filenames." + echo "Unrecognized options are passed through to dejagnu by default." + echo + echo "Interesting options:" + echo " --tool_exec= Test against a different bash executable." + echo " --buffer-size Change expect match buffer size from our default of 20000 bytes." + echo " --debug Create a dbg.log in the test directory with detailed expect match information." + echo " --timeout Change expect timeout from the default of 10 seconds." + echo " --debug-xtrace Create an xtrace.log in the test directory with set -x output." + echo + echo "Example run: ./run unit/_get_cword.exp unit/compgen.exp" +} + + +# Try to set the tool variable; or fail if trying to set different values. +set_tool() { + if [[ $tool ]]; then + if [[ $tool != $1 ]]; then + echo "Tool spec mismatch ('$tool' and '$1'). See --usage." + exit 1 + fi + else + tool=$1 + fi +} + + +cd "$(dirname "${BASH_SOURCE[0]}")" + + +# Loop over the arguments. +args=() +while [[ $# > 0 ]]; do + case "$1" in + --help|--usage) usage; exit 1;; + --buffer-size) shift; buffer_size=$1;; + --buffer-size=*) buffer_size=${1/--buffer-size=};; + --debug-xtrace) args+=(OPT_BASH_XTRACE=1);; + --timeout) shift; timeout=$1;; + --timeout=*) timeout=${1/--timeout=};; + --tool=*) set_tool "${1#/--tool=}";; + --tool) shift; set_tool "$1";; + completion/*.exp|*/completion/*.exp|unit/*.exp|*/unit/*.exp) + arg=${1%/*} + set_tool "${arg##*/}" + args+=("${1##*/}") + ;; + *) args+=("$1") + esac + shift +done + +[[ -n $buffer_size ]] && args+=("OPT_BUFFER_SIZE=$buffer_size") +[[ -n $timeout ]] && args+=("OPT_TIMEOUT=$timeout") +[[ -z $tool ]] && { echo "Must specify tool somehow"; exit 1; } + +runtest --outdir log --tool $tool "${args[@]}" +rc=$? +[[ $rc -ne 0 && -n "$CI" ]] && cat log/$tool.log +exit $rc diff --git a/src/test/dejagnu.tests/runCompletion b/src/test/dejagnu.tests/runCompletion new file mode 100644 index 00000000..38a82622 --- /dev/null +++ b/src/test/dejagnu.tests/runCompletion @@ -0,0 +1,8 @@ +#!/bin/bash + +# NOTE: I tried setting up bash_completion_lib within ./lib files, but DejaGnu +# isn't initialized at that point (i.e. output of `expect' is shown on +# stdout - `open_logs' hasn't run yet?). And running code from a library +# file isn't probably a good idea either. +exec "${bashcomp_bash:-$BASH}" \ + "$(dirname "${BASH_SOURCE[0]}")/run" --tool completion $*