Check for errors before checking for prompt
[ci.git] / test-in-vm.sh
blobf6af0646daf1e77037ceab33871635a840900c7c
1 #!/bin/bash
4 # Copyright (c) 2016 Vojtech Horky
5 # All rights reserved.
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions
9 # are met:
11 # - Redistributions of source code must retain the above copyright
12 # notice, this list of conditions and the following disclaimer.
13 # - Redistributions in binary form must reproduce the above copyright
14 # notice, this list of conditions and the following disclaimer in the
15 # documentation and/or other materials provided with the distribution.
16 # - The name of the author may not be used to endorse or promote products
17 # derived from this software without specific prior written permission.
19 # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
20 # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
21 # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
22 # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
23 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
24 # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
28 # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 # This code was originally inspired by "virtual testing lab" scripts created
33 # by Jan Buchar for testing his firewall implementation in HelenOS:
34 # https://code.launchpad.net/~teyras/hpf-virtlab/trunk
37 xx_echo() {
38 echo ">>" "$@"
41 xx_echo2() {
42 echo " -" "$@"
45 xx_debug() {
46 $XX_DEBUG_ECHO " :" "$@"
49 xx_run_debug() {
50 xx_debug "$@"
51 "$@"
54 xx_activity() {
55 $XX_ACTIVITY_ECHO " +" "$@"
58 xx_fatal() {
59 echo "!!" "$@"
60 xx_shutdown
63 ## Prepares pipe to QEMU monitor.
64 # Feed QEMU commands to stdin of this function.
66 # Do not forget to check for return code as checking it inside pipe
67 # is not very useful (we cannot terminate the script from that).
69 # @param 1 Machine id.
70 xx_do_qemu_command_pipe() {
71 socat STDIN "UNIX-CONNECT:$XX_TEMP/$1.monitor"
74 ## Sends command to QEMU monitor.
75 # This function terminates the scenario on failure of the monitoring pipe.
77 # @param 1 Machine id.
78 # @param 2 Command to send.
79 xx_do_qemu_command() {
80 xx_debug "echo '$2' | socat STDIN 'UNIX-CONNECT:$XX_TEMP/$1.monitor'"
81 echo "$2" | socat STDIN "UNIX-CONNECT:$XX_TEMP/$1.monitor"
82 res=$?
83 if [ $res -ne 0 ]; then
84 xx_shutdown
88 ## Types the text to a specific machine.
90 # @param 1 Machine id.
91 # @param 2 Text to type.
92 xx_do_type() {
93 ( echo "$2" | fold -w 1 | sed \
94 -e 's/ /spc/' \
95 -e 's/\./dot/' \
96 -e 's/-/minus/' \
97 -e 's/+/shift-equal/' \
98 -e 's/_/shift-minus/' \
99 -e 's/@/shift-2/' \
100 -e 's/:/shift-semicolon/' \
101 -e 's/\//slash/' \
102 -e 's/=/equal/' \
103 -e 's/|/shift-backslash/' \
104 -e 's/\([[:upper:]]\)/shift-\L\1/' \
105 -e 's/\\/backslash/' | \
106 while read code; do
107 xx_do_qemu_command "$1" "sendkey $code"
108 done
110 res=$?
111 if [ $res -ne 0 ]; then
112 xx_shutdown
116 xx_get_var() {
117 local varname="$1"
118 local default="$2"
119 shift 2
120 local i
121 for i in "$@"; do
122 case $i in
123 $varname=*)
124 echo "$i" | cut '-d=' -f 2-
125 return
129 esac
130 done
131 echo "$default"
134 xx_get_def_var() {
135 local i
136 for i in "$@"; do
137 echo "$i" | grep -v '^[a-zA-Z][a-zA-Z0-9_]*='
138 done
139 echo
142 xx_get_boolean_var() {
143 value=`xx_get_var "$@" | tr 'A-Z' 'a-z'`
144 if [ "$value" = "yes" -o "$value" = "true" ]; then
145 echo "true"
146 elif [ "$value" = "no" -o "$value" = "false" ]; then
147 echo "false"
148 else
149 echo
154 xx_shutdown() {
155 local i
156 for i in $XX_KNOWN_MACHINES; do
157 xx_stop_machine name="$i"
158 done
159 exit 1
162 xx_assert_var() {
163 if [ -z "$2" ]; then
164 xx_echo "Option $1 not specified or invalid, terminating."
165 xx_shutdown
169 xx_do_check_var() {
170 if [ -z "$2" ]; then
171 xx_fatal "Option $1 not specified with no suitable default."
173 if [ -n "$3" ]; then
174 if ! echo "$2" | grep -q "$3"; then
175 xx_fatal "Option $1 does not match '$3' (got '$2')."
180 xx_do_compute_timeout() {
181 echo $(( `date +%s` + 10 * $1 ))
185 xx_start_machine() {
186 local cdrom=`xx_get_var cdrom "$XX_CDROM_FILE" "$@"`
187 local name=`xx_get_var name default "$@"`
188 local wait_for_vterm=`xx_get_boolean_var vterm true "$@"`
190 xx_do_check_var "xx_start_machine/name" "$name" '^[a-zA-Z][0-9a-zA-Z]*$'
191 xx_do_check_var "xx_start_machine/cdrom" "$cdrom"
193 local extra_opts=""
194 if $XX_HEADLESS; then
195 extra_opts="-display none"
197 if $XX_USE_KVM; then
198 extra_opts="$extra_opts -enable-kvm"
201 xx_echo "Starting machine $name from $cdrom."
203 local qemu_command=""
204 if [ $XX_ARCH == "ia32" ]; then
205 qemu_command=qemu-system-i386
206 elif [ $XX_ARCH == "amd64" ]; then
207 qemu_command=qemu-system-x86_64
209 if [ -z "$qemu_command" ]; then
210 xx_fatal "Unable to find proper emulator."
213 xx_run_debug $qemu_command \
214 $extra_opts \
215 -device e1000,vlan=0 -net user \
216 -redir udp:8080::8080 -redir udp:8081::8081 \
217 -redir tcp:8080::8080 -redir tcp:8081::8081 \
218 -redir tcp:2223::2223 \
219 -usb \
220 -m 256 \
221 -daemonize -pidfile "$XX_TEMP/$name.pid" \
222 -monitor "unix:$XX_TEMP/$name.monitor,server,nowait" \
223 -boot d \
224 -cdrom "$cdrom"
225 sleep 1
226 xx_do_qemu_command "$name" "sendkey ret"
228 XX_LAST_MACHINE=$name
230 XX_KNOWN_MACHINES="$XX_KNOWN_MACHINES $name"
232 if $wait_for_vterm; then
233 xx_echo2 "Waiting for OS to boot into GUI..."
234 if ! xx_do_wait_for_text "$name" `xx_do_compute_timeout 15` "to see a few survival tips"; then
235 xx_fatal "OS have not booted into a known state."
243 xx_stop_machine() {
244 local name=`xx_get_var name $XX_LAST_MACHINE "$@"`
246 xx_echo "Forcefully killing machine $name."
247 xx_do_qemu_command "$name" "quit"
248 sleep 1
250 if [ "$name" = "$XX_LAST_MACHINE" ]; then
251 XX_LAST_MACHINE=""
256 ## Wait for text to appear on machine console.
258 # @param 1 Machine id.
259 # @param 2 UNIX end-time (date %s + TIME_OUT).
260 # @param 3 Text to match.
261 xx_do_wait_for_text() {
262 while true; do
263 xx_activity "Taking screenshot, looking for '$3'."
265 xx_do_screenshot "$1" "$XX_TEMP/$1-full.ppm" \
266 "$XX_TEMP/$1-term.png" "$XX_TEMP/$1-term.txt"
268 if grep -q "$3" <"$XX_TEMP/$1-term.txt"; then
269 return 0
272 if [ `date +%s` -gt $2 ]; then
273 return 1
276 sleep 1
277 done
280 xx_assert() {
281 local timeout=`xx_get_var timeout $XX_DEFAULT_TIMEOUT "$@"`
282 local machine=`xx_get_var machine $XX_LAST_MACHINE "$@"`
283 local error_msg=`xx_get_var error "" "$@"`
284 local text=`xx_get_def_var "$@"`
286 xx_echo "Checking that '$text' will appear on $machine within ${timeout}s."
288 if ! xx_do_wait_for_text "$machine" `xx_do_compute_timeout $timeout` "$text"; then
289 xx_fatal "Failed to recognize '$text' on $machine."
293 xx_die_on() {
294 local timeout=`xx_get_var timeout $XX_DEFAULT_TIMEOUT "$@"`
295 local machine=`xx_get_var machine $XX_LAST_MACHINE "$@"`
296 local error_msg=`xx_get_var message "" "$@"`
297 local text=`xx_get_def_var "$@"`
299 xx_echo "Checking that '$text' will not appear on $machine within ${timeout}s."
301 if xx_do_wait_for_text "$machine" `xx_do_compute_timeout $timeout` "$text"; then
302 xx_fatal "Prohibited text '$text' spotted on $machine."
306 xx_sleep() {
307 local amount=`xx_get_def_var "$@"`
309 xx_echo "Waiting for ${amount}s."
310 sleep $amount
314 xx_cls() {
315 local machine=`xx_get_var machine $XX_LAST_MACHINE "$@"`
317 xx_echo "Clearing the screen on $machine."
318 for i in `seq 1 35`; do
319 xx_do_qemu_command "$machine" "sendkey ret"
320 done
321 sleep 1
324 xx_do_ocr_prepare() {
325 convert "$1" -crop "8x16" +repage +adjoin txt:- \
326 | sed -e 's|[0-9]*,[0-9]*: ([^)]*)[ ]*#\([0-9A-Fa-f]\{6\}\).*|\1|' -e 's:^#.*:@:' -e 's#000000#0#g' -e 's#FFFFFF#F#' \
327 | sed -e ':a' -e 'N;s#\n##;s#^@##;/@$/{s#@$##p;d}' -e 't a'
331 xx_do_ocr() {
332 xx_do_ocr_prepare "$1" | sed -f "$XX_MY_HOME/ocr.sed"
335 xx_do_screenshot() {
336 xx_do_qemu_command "$1" "screendump $2"
337 if [ -n "$3" ]; then
338 # Looping as the screenshot might take some time to save
339 local retries=3
340 while true; do
341 if convert "$2" -crop 640x480+4+24 +repage -colors 2 -monochrome "$3"; then
342 break
344 retries=$(( $retries - 1 ))
345 if [ $retries -le 0 ]; then
346 xx_fatal "Failed to convert screen to monochrome."
348 sleep 1
349 done
351 if [ -n "$4" ]; then
352 xx_do_ocr "$3" | paste -sd '' | fold -w 80 >"$4"
357 ## Wait for text to appear on machine console.
359 # @param 1 Machine id.
360 # @param 2 UNIX end-time (date %s + TIME_OUT).
361 # @param 3 Text that shall not be matched.
362 # @param 4 Text that shall be matched before timeout.
363 # @param 5 Consider prompt as a success.
364 # @param 6 Check for standard Bdsh error messages.
365 # @retval 0 Expected text was matched.
366 # @retval 1 Prompt text was matched.
367 # @retval 2 Time-out, nothing matched.
368 # @retval 3 Unexpected text was matched.
369 # @retval 4 Standard Bdsh error message detected.
370 xx_do_complex_text_waiting() {
371 xx_debug "waiting: $1/$2 match='$4', no-match='$3'"
372 xx_debug "waiting: $1/$2 prompt_is_success=$5 check_for_bdsh_error=$6"
374 while true; do
375 xx_activity "Taking screenshot, checking for specific output."
377 xx_do_screenshot "$1" "$XX_TEMP/$1-full.ppm" \
378 "$XX_TEMP/$1-term.png" "$XX_TEMP/$1-term.txt"
380 if [ -n "$3" ] && grep -q "$3" <"$XX_TEMP/$1-term.txt"; then
381 return 3
384 if [ -n "$4" ] && grep -q "$4" <"$XX_TEMP/$1-term.txt" ; then
385 return 0
388 if $6 && grep -q -e 'Cannot spawn' -e 'Command failed' <"$XX_TEMP/$1-term.txt"; then
389 return 4
392 if $5 && grep -q '^.*/ # _[ ]*$' <"$XX_TEMP/$1-term.txt" ; then
393 return 0
396 if [ `date +%s` -gt $2 ]; then
397 return 2
400 sleep 1
401 done
404 xx_cmd() {
405 local cmd=`xx_get_def_var "$@"`
406 local timeout=`xx_get_var timeout $XX_DEFAULT_TIMEOUT "$@"`
407 local machine=`xx_get_var machine $XX_LAST_MACHINE "$@"`
408 local error_msg=`xx_get_var error "" "$@"`
409 local text=`xx_get_var assert "" "$@"`
410 local negtext=`xx_get_var die_on "" "$@"`
411 local text_is_empty=`if [ -n "$text" ]; then echo false; else echo true; fi`
413 xx_echo "Sending '$cmd' to $machine."
414 xx_do_type "$machine" "$cmd"
415 xx_do_qemu_command "$machine" "sendkey ret"
418 xx_do_complex_text_waiting "$machine" `xx_do_compute_timeout $timeout` \
419 "$negtext" "$text" $text_is_empty true
420 local res=$?
422 xx_debug "xx_do_complex_text_waiting = $res"
424 case $res in
425 0|1)
426 return 0
429 if $text_is_empty; then
430 xx_fatal "Command timed-out."
431 else
432 xx_fatal "Failed to match '$text'."
435 3|4)
436 if [ -n "$error_msg" ]; then
437 xx_fatal "$error_msg"
438 else
439 xx_fatal "Command failed."
443 xx_fatal "Internal error, we shall never reach this line."
445 esac
450 xx_do_print_help() {
451 echo "Usage: $1 [options] scenario-file [scenarios-file ...]"
452 cat <<'EOF_USAGE'
453 where [options] are:
455 --help Print this help and exit.
456 --headless Hide the screen of the virtual machine.
457 --root=DIR Find HelenOS image in the source directory.
458 --image=FILE File with main HelenOS image (specify --arch).
459 --arch=ARCH Architecture of the image file (see --image).
460 --no-kvm Do not try to run QEMU with KVM enabled.
461 --fail-fast Exit with first error.
462 --debug Print (a lot of) debugging messages.
464 EOF_USAGE
468 XX_MY_HOME=`which -- "$0" 2>/dev/null`
469 # Maybe, we are running Bash
470 [ -z "$XX_MY_HOME" ] && XX_MY_HOME=`which -- "$BASH_SOURCE" 2>/dev/null`
471 XX_MY_HOME=`dirname -- "$XX_MY_HOME"`
473 XX_DEBUG_ECHO=:
474 XX_ACTIVITY_ECHO=:
475 XX_TRAIN_OCR=false
476 XX_HEADLESS=false
477 XX_USE_KVM=true
478 XX_TEMP="$PWD/tmp-vm/"
479 XX_HELENOS_ROOT="."
480 XX_AUTODETECT_HELENOS=false
481 XX_ARCH=""
482 XX_CDROM_FILE=""
483 XX_FAIL_FAST=false
485 XX_KNOWN_MACHINES=""
486 XX_DEFAULT_TIMEOUT=5
487 XX_LAST_MACHINE=default
490 # Replace with getopt eventually.
491 while [ $# -gt 0 ]; do
492 case "$1" in
493 --headless)
494 XX_HEADLESS=true
496 --train-ocr)
497 XX_TRAIN_OCR=true
499 --train-ocr=*)
500 XX_TRAIN_OCR=true
501 XX_TRAIN_OCR_FILE=`echo "$1" | cut '-d=' -f 2-`
503 --debug)
504 XX_DEBUG_ECHO=echo
506 --activity)
507 XX_ACTIVITY_ECHO=echo
509 --root=*)
510 XX_HELENOS_ROOT=`echo "$1" | cut '-d=' -f 2-`
511 XX_AUTODETECT_HELENOS=true
513 --image=*)
514 XX_CDROM_FILE=`echo "$1" | cut '-d=' -f 2-`
515 XX_AUTODETECT_HELENOS=false
517 --arch=*)
518 XX_ARCH=`echo "$1" | cut '-d=' -f 2-`
519 XX_AUTODETECT_HELENOS=false
521 --temp=*)
522 XX_TEMP=`echo "$1" | cut '-d=' -f 2-`
524 --no-kvm)
525 XX_USE_KVM=false
527 --fail-fast)
528 XX_FAIL_FAST=true
530 --help|-h|-?)
531 xx_do_print_help "$0"
532 exit 0
534 --*)
535 xx_fatal "Unknown option $1."
538 break
540 esac
541 shift
542 done
544 mkdir -p "$XX_TEMP"
546 if $XX_AUTODETECT_HELENOS; then
547 if ! [ -r "$XX_HELENOS_ROOT/Makefile.config" ]; then
548 xx_fatal "Cannot open $XX_HELENOS_ROOT/Makefile.config."
550 XX_ARCH=`grep '^PLATFORM = ' "$XX_HELENOS_ROOT/Makefile.config" | cut '-d=' -f 2- | tr -d ' '`
551 XX_CDROM_FILE=$XX_HELENOS_ROOT/`cat "$XX_HELENOS_ROOT/defaults/$XX_ARCH/output"`
555 if $XX_TRAIN_OCR; then
556 if [ -z "$XX_TRAIN_OCR_FILE" ]; then
557 echo "Usage notes for --train-ocr command-line switch."
558 echo
559 echo "Prepare HelenOS image.iso with train-ocr.txt in its root."
560 echo
561 echo "Run this script with the ISO and with --train-ocr=out.sed"
562 echo "where out.sed is output file with OCR commands."
563 echo
564 echo "If the script finishes with no error you can replace the"
565 echo "old OCR scheme (ocr.sed) with the new one in out.sed."
566 echo
567 exit
570 # Set to false to debug the script below without running QEMU
571 # all the time
572 if true; then
573 xx_start_machine name=ocr vterm=false
574 xx_echo "Will display the training set on the VM screen"
575 xx_echo "(this will take a while)."
576 sleep 20
577 xx_do_type ocr "cat train-ocr.txt"
578 sleep 1
579 xx_do_qemu_command ocr "sendkey ret"
580 sleep 5
581 xx_do_screenshot ocr "$XX_TEMP/ocr-full.ppm" "$XX_TEMP/ocr-term.png"
582 xx_stop_machine
585 xx_echo "Doing OCR..."
586 xx_do_ocr_prepare "$XX_TEMP/ocr-term.png" \
588 prev1_line=""
589 prev2_line=""
590 prev3_line=""
591 counter=0
592 while read current_line; do
593 if [ $counter -gt 5 ]; then
594 while read current_line; do
595 if ! [ "$current_line" = "$prev1_line" -o "$current_line" = "$prev2_line" ]; then
596 break
598 done
599 alphabet=`sed -n 's#^\(.\)\(.\)\(\1\2\)\{4,\}##p' train-ocr.txt`
600 alphabet_size=`echo "$alphabet" | wc -c`
601 for i in `seq 1 $(( $alphabet_size - 1))`; do
602 symbol="`echo \"$alphabet\" | fold -w 1 | sed -n \"${i}p\" | sed 's#[*\.\&\\:\/\[]\|\]#\\\\&#g'`"
603 echo "s:$current_line:$symbol:"
604 read current_line
605 done
606 echo "$current_line" | tr 'F' '0' | sed 's#.*#s:&:_:#'
607 echo "$current_line" | tr '0F' '.' | sed 's#.*#s:&:?:#'
608 break
610 if [ "$current_line" = "$prev2_line" -a "$prev1_line" = "$prev3_line" -a "$prev1_line" != "$prev2_line" ]; then
611 counter=$(( $counter + 1 ))
612 else
613 counter=0
615 prev3_line="$prev2_line"
616 prev2_line="$prev1_line"
617 prev1_line="$current_line"
618 done >"$XX_TRAIN_OCR_FILE"
620 xx_echo "New OCR scheme is in $XX_TRAIN_OCR_FILE."
621 exit
625 XX_RESULT_SUMMARY="$XX_TEMP/summary.$$.txt"
627 date '+# Execution started on %Y-%m-%d %H:%M.' >"$XX_RESULT_SUMMARY"
628 echo '# =======================================' >>"$XX_RESULT_SUMMARY"
631 # Run it
632 for i in "$@"; do
633 echo "# Starting scenario $i..."
635 . $i
637 if [ $? -eq 0 ]; then
638 res="OK"
639 else
640 res="FAIL"
642 echo "# Scenario $i terminated, $res."
643 printf '%-35s %4s\n' "$i" "$res" >>"$XX_RESULT_SUMMARY"
644 if $XX_FAIL_FAST && [ "$res" = "FAIL" ]; then
645 exit 1
647 done
650 date '+# Execution finished on %Y-%m-%d %H:%M.' >>"$XX_RESULT_SUMMARY"
652 # Display the results.
653 echo
655 cat "$XX_RESULT_SUMMARY"