-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
lib/bats: Add background process helper functions
Closes #181. All except for `skip_if_missing_background_utilities` and `run_test_script_in_background` are imported from mbland/custom-links, where they were used to test the `./go serve` command.
- Loading branch information
Showing
2 changed files
with
329 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
#! /usr/bin/env bash | ||
# | ||
# Functions for managing background processes | ||
# | ||
# These functions make it easier to write Bats test cases that validate the | ||
# behavior of long-running processes such as servers: | ||
# | ||
# @test '$SUITE: my-server should start successfully' { | ||
# skip_if_missing_background_utilities | ||
# run_in_background 'my-server' | ||
# wait_for_background_output 'my-server is now ready' | ||
# stop_background_run | ||
# assert_... | ||
# } | ||
# | ||
# Call `skip_if_missing_background_utilities` at the beginning of each test case | ||
# to skip it if the host system lacks any of the process management utilities | ||
# required by the functions from this file. | ||
# | ||
# `run_in_background` is equivalent to the Bats `run` function, except that it | ||
# launches a background process without waiting for it to exit. | ||
# `run_test_script_in_background` creates and runs a test script in the | ||
# background in one step. | ||
# | ||
# `wait_for_background_output` will pause test execution until the output | ||
# from the process launched by `run_in_background` matches a specified | ||
# pattern. If the output isn't seen before the timeout expires, it prints | ||
# an error message and returns nonzero, which will fail the test case. | ||
# | ||
# `stop_background_run` terminates the background process launched by | ||
# `run_in_background` and sets `status`, `output`, and `lines`. | ||
|
||
. "${BASH_SOURCE%/*}/helpers" | ||
|
||
# Skips a test case if any background process management utilities are missing. | ||
# | ||
# These helpers require that the `pkill`, `sleep`, and `tail` system utilities | ||
# are available. | ||
# | ||
# Of these, `pkill` may be missing from Windows-based Bash environments by | ||
# default. For these platforms: | ||
# | ||
# - Cygwin: Install the procps-ng package | ||
# - MSYS2: Install procps via `pacman -Sy procps` | ||
# - Git for Windows: Install the Git for Windows SDK; run `pacman -Sy procps` | ||
skip_if_missing_background_utilities() { | ||
skip_if_system_missing 'pkill' 'sleep' 'tail' | ||
} | ||
|
||
# Equivalent to the Bats `run` function for background processes. | ||
# | ||
# After calling this function, you can use `wait_for_background_output` to wait | ||
# for the process to enter an expected state, then call `stop_background_run` to | ||
# end the process and set the `output`, `lines`, and `status` variables. | ||
# | ||
# Arguments: | ||
# $@: Command to run as a background process | ||
# | ||
# Globals set by this function: | ||
# BATS_BACKGROUND_RUN_OUTPUT: File into which process output is collected | ||
# BATS_BACKGROUND_RUN_PID: Process ID of the background process | ||
run_in_background() { | ||
set "$DISABLE_BATS_SHELL_OPTIONS" | ||
export BATS_BACKGROUND_RUN_OUTPUT | ||
BATS_BACKGROUND_RUN_OUTPUT="$BATS_TEST_ROOTDIR/background-run-output.txt" | ||
printf '' >"$BATS_BACKGROUND_RUN_OUTPUT" | ||
"$@" >"$BATS_BACKGROUND_RUN_OUTPUT" 2>&1 & | ||
export BATS_BACKGROUND_RUN_PID="$!" | ||
restore_bats_shell_options | ||
} | ||
|
||
# Creates and runs a test script in the background in one step | ||
# | ||
# Arguments: | ||
# $@: Passed directly through to `create_bats_test_script` | ||
run_test_script_in_background() { | ||
create_bats_test_script "$@" | ||
run_in_background "$BATS_TEST_ROOTDIR/$1" | ||
} | ||
|
||
# Pauses test execution until a background process produces expected output. | ||
# | ||
# Call this after `run_in_background` to ensure the process enters an expected | ||
# state before continuing with the test. If the expected output isn't seen | ||
# within the `timeout` interval, this function will print an error message and | ||
# return nonzero. | ||
# | ||
# To wait on output added to a different file from the one created by | ||
# `run_in_background`, prefix the call to this function with | ||
# `BATS_BACKGROUND_RUN_OUTPUT` set to the file you wish to wait on, e.g.: | ||
# | ||
# BATS_BACKGROUND_RUN_OUTPUT="$BATS_TEST_ROOTDIR/foo.txt" \ | ||
# wait_for_background_output 'foo bar baz' | ||
# | ||
# Arguments: | ||
# pattern: Regular expression matching output signifying expected state | ||
# timeout: Timeout for the wait operation in seconds | ||
# | ||
# Globals set by `run_in_background`: | ||
# BATS_BACKGROUND_RUN_OUTPUT: File into which process output is collected | ||
wait_for_background_output() { | ||
set "$DISABLE_BATS_SHELL_OPTIONS" | ||
local pattern="$1" | ||
local timeout="${2:-3}" | ||
local input_cmd=('tail' '-f' "$BATS_BACKGROUND_RUN_OUTPUT") | ||
local kill_input_pid='0' | ||
local line | ||
|
||
if [[ -z "$BATS_BACKGROUND_RUN_OUTPUT" ]]; then | ||
printf 'run_in_background not called\n' >&2 | ||
restore_bats_shell_options '1' | ||
return | ||
elif [[ -z "$pattern" ]]; then | ||
printf 'pattern not specified\n' >&2 | ||
restore_bats_shell_options '1' | ||
return | ||
fi | ||
|
||
# Since `tail -f` will block forever, even if the background process died, we | ||
# kill it automatically after a timeout period. | ||
(sleep "$timeout"; pkill -f "${input_cmd[*]}" >/dev/null 2>&1) & | ||
kill_input_pid="$!" | ||
|
||
while read -r line; do | ||
if [[ "$line" =~ $pattern ]]; then | ||
# Kill the sleep so `pkill -f 'tail -f'` will run sooner. | ||
pkill -P "$kill_input_pid" sleep | ||
restore_bats_shell_options | ||
return | ||
fi | ||
done < <("${input_cmd[@]}") | ||
|
||
printf "Output did not match regular expression:\n '%s'\n\n" "$pattern" >&2 | ||
printf 'OUTPUT:\n------\n%s' "$(< "$BATS_BACKGROUND_RUN_OUTPUT")" >&2 | ||
restore_bats_shell_options '1' | ||
} | ||
|
||
# Terminates the background process launched by `run_in_background`. | ||
# | ||
# Also sets `output`, `lines`, and `status`, though `lines` preserves empty | ||
# lines from `output`. | ||
# | ||
# Note that the `QUIT` and `INT` are handled specially by Bash, and thus aren't | ||
# appropriate signals to send to background Bash processes. | ||
# | ||
# Arguments: | ||
# signal (optional): Signal to send to the process; defaults to TERM | ||
# | ||
# Globals set by `run_in_background`: | ||
# BATS_BACKGROUND_RUN_OUTPUT: File into which process output is collected | ||
# BATS_BACKGROUND_RUN_PID: Process ID of the background process | ||
stop_background_run() { | ||
set "$DISABLE_BATS_SHELL_OPTIONS" | ||
local signal="${1:-TERM}" | ||
|
||
if [[ -n "$BATS_BACKGROUND_RUN_PID" ]]; then | ||
kill "-${signal}" "$BATS_BACKGROUND_RUN_PID" >/dev/null 2>&1 | ||
wait "$BATS_BACKGROUND_RUN_PID" | ||
status="$?" | ||
output="$(<"$BATS_BACKGROUND_RUN_OUTPUT")" | ||
rm "$BATS_BACKGROUND_RUN_OUTPUT" | ||
unset 'BATS_BACKGROUND_RUN_PID' 'BATS_BACKGROUND_RUN_OUTPUT' | ||
split_bats_output_into_lines | ||
fi | ||
restore_bats_shell_options | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
#! /usr/bin/env bats | ||
|
||
load environment | ||
load "$_GO_CORE_DIR/lib/bats/background-process" | ||
|
||
# We define array variables on one line and assign to it on another thanks to | ||
# Bash <4.25; see commit b421c7382fc1dafb4d865d2357276168eac30744 and | ||
# commit c6bf1cf46c7816c969a0c5d45a4badeb50963f95. | ||
SKIP_TEST= | ||
BACKGROUND_SCRIPT= | ||
BACKGROUND_MESSAGE= | ||
|
||
setup() { | ||
test_filter | ||
SKIP_TEST=('skip-test' | ||
"load '$_GO_CORE_DIR/lib/bats/background-process'" | ||
'@test "skip_if_missing_background_utilities" {' | ||
' skip_if_missing_background_utilities' | ||
' printf "Did not skip" >&2' | ||
' return 1' | ||
'}') | ||
|
||
# The kill-sleep-on-trap trick is from: | ||
# http://mywiki.wooledge.org/SignalTrap#When_is_the_signal_handled.3F | ||
BACKGROUND_SCRIPT=('bg-run' | ||
'printf "%s\n" "Ready..." "Set..." "$BACKGROUND_MESSAGE"' | ||
"trap 'kill \"\$!\"' TERM HUP" | ||
'sleep 10 &' | ||
'wait "$!"') | ||
} | ||
|
||
teardown() { | ||
if [[ -n "$BATS_BACKGROUND_RUN_PID" ]]; then | ||
kill "$BATS_BACKGROUND_RUN_PID" | ||
wait | ||
fi | ||
unset 'BATS_BACKGROUND_RUN_PID' 'BATS_BACKGROUND_RUN_OUTPUT' | ||
remove_bats_test_dirs | ||
} | ||
|
||
kill_background_test_script() { | ||
set "$DISABLE_BATS_SHELL_OPTIONS" | ||
pkill -P "$BATS_BACKGROUND_RUN_PID" sleep | ||
unset 'BATS_BACKGROUND_RUN_PID' | ||
wait | ||
restore_bats_shell_options | ||
} | ||
|
||
@test "$SUITE: don't skip if all system utilities are present" { | ||
stub_program_in_path 'pkill' | ||
stub_program_in_path 'sleep' | ||
stub_program_in_path 'tail' | ||
|
||
run_bats_test_suite "${SKIP_TEST[@]}" | ||
restore_programs_in_path 'pkill' 'sleep' 'tail' | ||
assert_failure | ||
assert_output_matches 'Did not skip' | ||
} | ||
|
||
@test "$SUITE: skip if any system utilities are missing" { | ||
run_bats_test_suite_in_isolation "${SKIP_TEST[@]}" | ||
assert_success | ||
fail_if output_matches 'Did not skip' | ||
|
||
local skip_msg='ok 1 # skip (pkill, sleep, tail not installed on the system)' | ||
local test_case_name='skip_if_missing_background_utilities' | ||
assert_lines_equal '1..1' "$skip_msg $test_case_name" | ||
} | ||
|
||
@test "$SUITE: run{,_test_script}_in_background launches background process" { | ||
skip_if_missing_background_utilities | ||
assert_equal '' "$BATS_BACKGROUND_RUN_OUTPUT" | ||
assert_equal '' "$BATS_BACKGROUND_RUN_PID" | ||
|
||
export BACKGROUND_MESSAGE='Hello, World!' | ||
run_test_script_in_background "${BACKGROUND_SCRIPT[@]}" | ||
|
||
assert_equal "$!" "$BATS_BACKGROUND_RUN_PID" | ||
kill_background_test_script | ||
|
||
assert_equal "$BATS_TEST_ROOTDIR/background-run-output.txt" \ | ||
"$BATS_BACKGROUND_RUN_OUTPUT" | ||
assert_file_equals "$BATS_BACKGROUND_RUN_OUTPUT" \ | ||
'Ready...' \ | ||
'Set...' \ | ||
"$BACKGROUND_MESSAGE" | ||
} | ||
|
||
@test "$SUITE: wait_for_background_output wakes up on expected output" { | ||
skip_if_missing_background_utilities | ||
export BACKGROUND_MESSAGE='Hello, World!' | ||
run_test_script_in_background "${BACKGROUND_SCRIPT[@]}" | ||
wait_for_background_output "$BACKGROUND_MESSAGE" | ||
kill_background_test_script | ||
} | ||
|
||
@test "$SUITE: wait_for_background fails if run_in_background not called" { | ||
run wait_for_background_output | ||
assert_failure 'run_in_background not called' | ||
} | ||
|
||
@test "$SUITE: wait_for_background fails if no pattern specified" { | ||
# Setting BATS_BACKGROUND_RUN_OUTPUT simulates run_in_background here. | ||
BATS_BACKGROUND_RUN_OUTPUT='foobar' run wait_for_background_output | ||
assert_failure 'pattern not specified' | ||
} | ||
|
||
@test "$SUITE: wait_for_background fails if pattern not seen within timeout" { | ||
skip_if_missing_background_utilities | ||
export BACKGROUND_MESSAGE='Goodbye, World!' | ||
|
||
run_test_script_in_background "${BACKGROUND_SCRIPT[@]}" | ||
run wait_for_background_output 'Hello, World!' '0.25' | ||
kill_background_test_script | ||
|
||
assert_failure 'Output did not match regular expression:' \ | ||
" 'Hello, World!'" \ | ||
'' \ | ||
'OUTPUT:' \ | ||
'------' \ | ||
'Ready...' \ | ||
'Set...' \ | ||
"$BACKGROUND_MESSAGE" | ||
} | ||
|
||
@test "$SUITE: stop_background_run does nothing if no background process" { | ||
stop_background_run | ||
assert_success '' | ||
assert_lines_equal | ||
} | ||
|
||
@test "$SUITE: stop_background_run stops the background process and sets vars" { | ||
skip_if_missing_background_utilities | ||
export BACKGROUND_MESSAGE='Hello, World!' | ||
local output_file | ||
|
||
run_test_script_in_background "${BACKGROUND_SCRIPT[@]}" | ||
output_file="$BATS_BACKGROUND_RUN_OUTPUT" | ||
wait_for_background_output "$BACKGROUND_MESSAGE" | ||
stop_background_run | ||
|
||
assert_equal '' "$BATS_BACKGROUND_RUN_PID" | ||
assert_equal '' "$BATS_BACKGROUND_RUN_OUTPUT" | ||
if [[ -f "$output_file" ]]; then | ||
fail "expected BATS_BACKGROUND_RUN_OUTPUT file to be removed: $output_file" | ||
fi | ||
assert_status "$((128 + $(kill -l TERM)))" | ||
assert_equal $'Ready...\nSet...\n'"$BACKGROUND_MESSAGE" "$output" | ||
assert_lines_equal \ | ||
'Ready...' \ | ||
'Set...' \ | ||
"$BACKGROUND_MESSAGE" | ||
} | ||
|
||
@test "$SUITE: stop_background_run sends the specified signal" { | ||
skip_if_missing_background_utilities | ||
export BACKGROUND_MESSAGE='Hello, World!' | ||
|
||
run_test_script_in_background "${BACKGROUND_SCRIPT[@]}" | ||
wait_for_background_output "$BACKGROUND_MESSAGE" | ||
stop_background_run 'HUP' | ||
assert_status "$((128 + $(kill -l HUP)))" | ||
} |