Skip to content

Commit 6880ba2

Browse files
committed
libtest support for simulated keystrokes
* keystrokes() * test_case -keystrokes * TEST_OPTS='trace_keys' * docs
1 parent 7169bbb commit 6880ba2

File tree

2 files changed

+252
-11
lines changed

2 files changed

+252
-11
lines changed

test/README.adoc

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ filter=<file-glob>:<case-glob>::
5757

5858
trace::
5959

60-
Show trace information.
60+
Show tig trace information.
61+
62+
trace_keys::
63+
64+
Show which keystrokes will be sent before each test.
6165

6266
todos::
6367

@@ -77,3 +81,66 @@ Testing API
7781
-----------
7882
7983
test_require(git-worktree, address-sanitizer, diff-highlight, readline)::
84+
85+
keystrokes([-append | -keysym | -repeat=<int>] key-sequence, [key-sequence, ...])::
86+
87+
Key sequences are given as Python strings, and accept
88+
https://docs.python.org/2.0/ref/strings.html[the same string escapes as
89+
Python]. Example: `'\134'` encodes a literal backslash. +
90+
+
91+
The key sequence may also contain special embedded codes:
92+
`%(keysym:<name>)`, `%(keypause:<seconds>)`, or `%(keysignal:<signal>)`. +
93+
+
94+
*`%(keysym:<name>)`* will be translated into the raw characters
95+
associated with the symbolic key name. <name> may be any form accepted
96+
by 'bind' in `~/.tigrc`, or any terminal capability name known to
97+
`tput`. Examples: `%(keysym:Left)`, `%(keysym:Ctrl-A)`. +
98+
+
99+
As a convenience, the `-keysym` option causes subsequent arguments to
100+
be interpreted as a series of keysym names. Example:
101+
-----------------------------------------------------------------------------
102+
keystrokes -keysym 'Down' 'Up'
103+
-----------------------------------------------------------------------------
104+
::
105+
*`%(keypause:<seconds>)`* will insert a pause in the simulated
106+
keystrokes. If pauses are added, the test timeout may also need to be
107+
increased. +
108+
+
109+
*`%(keysignal:<signal>)`* will send a Unix signal to tig. The signal
110+
may be given as a number or symbol. Example: `%(keysignal:SIGWINCH)` +
111+
+
112+
To send tig a literal sequence matching the characters of an embedded
113+
code, escape it with a backslash: `\%(keypause:1)`. +
114+
+
115+
Exiting: The passed key sequence should always arrange a clean exit.
116+
Tig will otherwise be shut down by a signal, which is less consistent
117+
for the purpose of testing. +
118+
+
119+
Appending: keystrokes may be defined in multiple passes using `-append`.
120+
This enables composing keystrokes with and without `-keysym`. The last
121+
call to `keystrokes()` should arrange a clean exit from tig. +
122+
+
123+
Repeating: `-repeat=<int>` can be used to define repeated sequences.
124+
Only one key-sequence argument may be used with `-repeat`. Example:
125+
-----------------------------------------------------------------------------
126+
keystrokes -keysym -repeat=20 'Down'
127+
-----------------------------------------------------------------------------
128+
::
129+
Interaction with `steps()`: It is possible to use both `steps()` and
130+
`keystrokes()` in the same test: during test execution, the `steps()`
131+
script will run first, and the simulated keystrokes will be sent to tig
132+
after the script finishes. When used together, `steps()` is modified so
133+
that it does not imply `:quit`. +
134+
+
135+
Whitespace handling: The "Enter" key will be fed to tig as a carriage
136+
return (`\r`). Interior newlines in key sequences, whether literal or
137+
encoded, will be translated to carriage returns before sending them to
138+
tig. But note that leading or trailing whitespace on the key-sequence
139+
argument is ignored. To send "Enter", "Tab", or "Space" as the first or
140+
last key in the sequence, use escape codes or keysyms (_ie_ `\r`,`\t`,
141+
`\040`, `%(keysym:Enter)`, `%(keysym:Tab)`, or `%(keysym:Space)`.) +
142+
+
143+
144+
test_case([--before=<string>, --after=<string>, --script=<string>, --args=<string>, --cwd=<string>, --tigrc=<string>, --assert-stderr=<string>, --todo=<string>, --subshell=<string>, --timeout=<string>, --keystrokes=<string>]) < expected::
145+
146+
test_require(git-worktree, address-sanitizer, diff-highlight, readline, python)::

test/tools/libtest.sh

Lines changed: 184 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export ASAN_OPTIONS=detect_leaks=false
7373
export TEST_OPTS="${TEST_OPTS:-}"
7474
# Used by tig_script to set the test "scope" used by test_tig.
7575
export TEST_NAME=
76+
export TEST_KEYSTROKES=
7677

7778
[ -e "$output_dir" ] && rm -rf -- "$output_dir"
7879
mkdir -p -- "$output_dir/$work_dir"
@@ -174,8 +175,15 @@ tig_script() {
174175
export TIG_SCRIPT="$HOME/${prefix}steps"
175176
export TEST_NAME="$name"
176177

177-
# Ensure that the steps finish by quitting
178-
printf '%s\n:quit\n' "$*" \
178+
if [ -z "${TEST_KEYSTROKES:-}" ]; then
179+
# ensure that the steps finish by quitting
180+
quit_str=':quit'
181+
else
182+
# unless simulated keystrokes are also to be sent
183+
quit_str=''
184+
fi
185+
186+
printf '%s\n%s\n' "$*" "$quit_str" \
179187
| sed -e 's/^[ ]*//' \
180188
| sed "s|:save-display[ ]\{1,\}\([^ ]\{1,\}\)|:save-display $HOME/\1|" \
181189
| sed "s|:save-options[ ]\{1,\}\([^ ]\{1,\}\)|:save-options $HOME/\1|" \
@@ -187,6 +195,84 @@ steps() {
187195
tig_script "" "$@"
188196
}
189197

198+
_tig_keystrokes_driver()
199+
{
200+
file="$1"; shift
201+
append_mode="$1"; shift
202+
203+
if [ -n "$append_mode" ]; then
204+
printf '%s' "$*" >> "$file"
205+
else
206+
printf '%s' "$*" > "$file"
207+
fi
208+
}
209+
210+
tig_keystrokes()
211+
{
212+
name="$1"; shift
213+
prefix="${name:+$name.}"
214+
215+
export TEST_KEYSTROKES="$HOME/${prefix}keystrokes"
216+
export TEST_NAME="$name"
217+
218+
append_mode=''
219+
keysym_mode=''
220+
repeat_mode=''
221+
while [ "$#" -gt 0 ]; do
222+
case "$1" in
223+
-append|--append) append_mode=yes && shift;;
224+
-keysym|--keysym|-keysyms|--keysyms) keysym_mode=yes && shift;;
225+
-repeat=*|--repeat=*) repeat_mode="$(expr "$1" : '--*repeat=\([0-9][0-9]*\)')" && shift || die "bad value $1";;
226+
*) break;;
227+
esac
228+
done
229+
230+
if [ -n "${TIG_SCRIPT:-}" ] && [ -s "$TIG_SCRIPT" ] && [ "$(tail -1 < "${TIG_SCRIPT}")" = ':quit' ]; then
231+
# remove the trailing :quit from a script if it already was defined
232+
head -n "$(expr "$(grep -c '^' < "$TIG_SCRIPT")" - 1)" < "${TIG_SCRIPT}" > "${TIG_SCRIPT}.tmp"
233+
mv -f -- "${TIG_SCRIPT}.tmp" "$TIG_SCRIPT"
234+
fi
235+
236+
if [ -n "$repeat_mode" ] && [ "$#" -gt 1 ]; then
237+
die "tig_keystrokes -repeat can only be used with a single argument"
238+
fi
239+
240+
if [ -n "$repeat_mode" ]; then
241+
test "$#" -gt 1 && die "tig_keystrokes -repeat can only be used with a single key-sequence argument"
242+
test "$repeat_mode" -lt 1 && repeat_mode=1
243+
244+
chunk="$1"
245+
if [ -n "$keysym_mode" ]; then
246+
chunk="%(keysym:$1)"
247+
fi
248+
249+
while [ "$repeat_mode" -gt 0 ]; do
250+
_tig_keystrokes_driver "$TEST_KEYSTROKES" "$append_mode" "$chunk"
251+
append_mode=yes
252+
repeat_mode="$((repeat_mode - 1))"
253+
done
254+
elif [ -n "$keysym_mode" ]; then
255+
for chunk in "$@"; do
256+
_tig_keystrokes_driver "$TEST_KEYSTROKES" "$append_mode" "%(keysym:$chunk)"
257+
append_mode=yes
258+
done
259+
else
260+
_tig_keystrokes_driver "$TEST_KEYSTROKES" "$append_mode" "$@"
261+
fi
262+
263+
if [ -n "$valgrind" ]; then
264+
test_skip "simulated keystrokes are not yet reliable under valgrind"
265+
fi
266+
267+
test_require python
268+
test_require python-termios
269+
}
270+
271+
keystrokes()
272+
{
273+
tig_keystrokes "" "$@"
274+
}
275+
190276
stdin() {
191277
file "stdin" "$@"
192278
}
@@ -276,6 +362,44 @@ process_tree()
276362
done
277363
}
278364

365+
descend_to_pg_leader()
366+
{
367+
parent_pid="$1"; shift
368+
enforce_shortcmd="${1:-}"
369+
370+
leader_pgid='-1'
371+
leader_shortcmd=''
372+
373+
# A short delay is enough to ensure that all relevant processes are up,
374+
# have completed any initial fork/execs, and allows for some irrelevant
375+
# processes to be cleared.
376+
sleep 1
377+
378+
for pid in $(process_tree "$parent_pid"); do
379+
test "$pid" -lt 1 && continue
380+
ORIG_IFS="$IFS"; IFS=' '
381+
set -- $(ps -o pgid=,command= "$pid" 2>/dev/null)
382+
IFS="$ORIG_IFS"
383+
test "$#" -lt 2 && continue
384+
385+
# By convention a process-group leader uses its own PID as PGID
386+
test "$pid" -ne "$1" && continue
387+
388+
leader_pgid="$1"
389+
leader_shortcmd="$(basename -- "$2")"
390+
break
391+
done
392+
393+
if [ "$leader_pgid" -lt 1 ]; then
394+
die "could not find a process-group leader"
395+
fi
396+
if [ -n "$enforce_shortcmd" ] && [ "$leader_shortcmd" != "$enforce_shortcmd" ]; then
397+
die "expected process-group leader named $enforce_shortcmd"
398+
fi
399+
400+
printf '%s\n' "$leader_pgid"
401+
}
402+
279403
#
280404
# Parse TEST_OPTS
281405
#
@@ -288,6 +412,7 @@ indent=' '
288412
verbose=
289413
debugger=
290414
runner=exec
415+
trace_keys=
291416
trace=
292417
todos=
293418
valgrind=
@@ -307,6 +432,7 @@ for arg in ${MAKE_TEST_OPTS:-} ${TEST_OPTS:-}; do
307432
debugger=*) debugger="$(expr "$arg" : 'debugger=\(.*\)')" ;;
308433
debugger) debugger="$(auto_detect_debugger)" ;;
309434
timeout=*) timeout="$(expr "$arg" : 'timeout=\(.*\)')" ;;
435+
trace[-_]keys|trace[-_]keystrokes) trace_keys=yes ;;
310436
trace) trace=yes ;;
311437
todo|todos) todos=yes ;;
312438
valgrind) valgrind="$HOME/valgrind.log" ;;
@@ -614,6 +740,40 @@ install_pid_timeout() {
614740
) >/dev/null 2>&1 &
615741
}
616742

743+
simulate_keystrokes()
744+
{
745+
if [ -z "${TEST_KEYSTROKES:-}" ] || ! [ -s "$TEST_KEYSTROKES" ]; then
746+
return
747+
fi
748+
749+
if ! [ "${1:-}" -gt 1 ]; then
750+
die "simulate_keystrokes requires an argument: the pgid to attach to"
751+
else
752+
# We were probably passed the PID, and can assume PID == PGID.
753+
# Calling descend_to_pg_leader is safe but not usually needed.
754+
tig_pgid="$1"; shift
755+
fi
756+
757+
if [ -n "$valgrind" ]; then
758+
759+
# valgrind needs a generous delay to warm up
760+
# todo: it might need extra time on its first run
761+
sleep 5
762+
763+
tig_pgid="$(descend_to_pg_leader "$tig_pgid" valgrind 2>/dev/null)"
764+
if [ -z "$tig_pgid" ]; then
765+
die "could not find a process-group leader"
766+
fi
767+
fi
768+
769+
if [ -n "$trace_keys" ]; then
770+
printf '%s%s %s %s %s\n' "$indent" keystroke-stuffer --with-shutdown "$tig_pgid" "$TEST_KEYSTROKES"
771+
keystroke-stuffer --debug --debug "$tig_pgid" "$TEST_KEYSTROKES" | sed "s/^/$indent$indent/"
772+
fi
773+
774+
keystroke-stuffer --with-shutdown "$tig_pgid" "$TEST_KEYSTROKES"
775+
}
776+
617777
valgrind_exec()
618778
{
619779
kernel="$(uname -s 2>/dev/null || printf 'unknown\n')"
@@ -686,6 +846,7 @@ test_tig()
686846
tig_pid="$!"
687847
signal=14
688848
install_pid_timeout "$tig_pid" "$signal"
849+
simulate_keystrokes "$tig_pid"
689850
wait "$tig_pid"
690851
fi
691852
status_code="$?"
@@ -726,15 +887,16 @@ test_case()
726887
printf '%s\n' "$name" >> test-cases
727888
cat > "$name.expected"
728889

729-
touch -- "$name-before" "$name-after" "$name-script" "$name-args" "$name-tigrc" "$name-assert-stderr" "$name-todo" "$name-subshell" "$name-timeout"
890+
touch -- "$name-before" "$name-after" "$name-script" "$name-args" "$name-tigrc" "$name-assert-stderr" \
891+
"$name-todo" "$name-subshell" "$name-timeout" "$name-keystrokes"
730892

731893
while [ "$#" -gt 0 ]; do
732894
arg="$1"; shift
733895
key="$(expr "X$arg" : 'X--\([^=]*\).*')"
734896
value="$(expr "X$arg" : 'X--[^=]*=\(.*\)')"
735897

736898
case "$key" in
737-
before|after|script|args|cwd|tigrc|assert-stderr|todo|subshell|timeout)
899+
before|after|script|args|cwd|tigrc|assert-stderr|todo|subshell|timeout|keystrokes)
738900
printf '%s\n' "$value" > "$name-$key" ;;
739901
assert-equals)
740902
filename="$(expr "X$value" : 'X\([^=]*\)')"
@@ -744,6 +906,12 @@ test_case()
744906
*) die "Unknown test_case argument: $arg"
745907
esac
746908
done
909+
910+
# hack to stop tests from wedging. unsatisfactory that the script
911+
# file is modified implicitly in multiple places.
912+
if ! [ -s "$name-script" ] && ! [ -s "$name-keystrokes" ]; then
913+
printf ':none\n' > "$name-script"
914+
fi
747915
}
748916

749917
run_test_cases()
@@ -781,10 +949,15 @@ run_test_cases()
781949
test_exec_work_dir "$SHELL" "$HOME/$name-before"
782950
fi
783951
(
784-
tig_script "$name" "
785-
$(if [ -e "$name-script" ]; then cat < "$name-script"; fi)
786-
:save-display $name.screen
787-
"
952+
if [ -s "$name-script" ]; then
953+
tig_script "$name" "
954+
$(cat < "$name-script")
955+
:save-display $name.screen
956+
"
957+
fi
958+
if [ -s "$name-keystrokes" ]; then
959+
tig_keystrokes "$name" "$(cat < "$name-keystrokes")"
960+
fi
788961
if [ -e "$name-cwd" ]; then
789962
work_dir="$work_dir/$(cat < "$name-cwd")"
790963
fi
@@ -800,8 +973,9 @@ run_test_cases()
800973
if [ -e "$name-after" ]; then
801974
test_exec_work_dir "$SHELL" "$HOME/$name-after"
802975
fi
803-
804-
assert_equals "$name.screen" < "$name.expected"
976+
if [ -s "$name-script" ]; then
977+
assert_equals "$name.screen" < "$name.expected"
978+
fi
805979
if [ -s "$name-assert-stderr" ]; then
806980
assert_equals "$name.stderr" < "$name-assert-stderr"
807981
else

0 commit comments

Comments
 (0)