diff options
Diffstat (limited to 'test/compose/test-compose')
-rwxr-xr-x | test/compose/test-compose | 439 |
1 files changed, 439 insertions, 0 deletions
diff --git a/test/compose/test-compose b/test/compose/test-compose new file mode 100755 index 000000000..f7643b078 --- /dev/null +++ b/test/compose/test-compose @@ -0,0 +1,439 @@ +#!/usr/bin/env bash +# +# Usage: test-docker-compose [testname] +# +# DEVELOPER NOTE: you almost certainly don't need to play in here. See README. +# +ME=$(basename $0) + +############################################################################### +# BEGIN stuff you can but probably shouldn't customize + +# Directory where this script (and extra test configs) live +TEST_ROOTDIR=$(realpath $(dirname $0)) + +# Podman executable +PODMAN_BIN=$(realpath bin)/podman + +# Github repo containing sample docker-compose setups +# FIXME: we should probably version this +AWESOME_COMPOSE=https://github.com/docker/awesome-compose + +# Local path to docker socket +DOCKER_SOCK=/var/run/docker.sock + +# END stuff you can but probably shouldn't customize +############################################################################### +# BEGIN setup + +TMPDIR=${TMPDIR:-/var/tmp} +WORKDIR=$(mktemp --tmpdir -d $ME.tmp.XXXXXX) + +# Log of all HTTP requests and responses; always make '.log' point to latest +LOGBASE=${TMPDIR}/$ME.log +LOG=${LOGBASE}.$(date +'%Y%m%dT%H%M%S') +ln -sf $LOG $LOGBASE + +# Keep track of test count and failures in files, not variables, because +# variables don't carry back up from subshells. +testcounter_file=$WORKDIR/.testcounter +failures_file=$WORKDIR/.failures + +echo 0 >$testcounter_file +echo 0 >$failures_file + +# END setup +############################################################################### +# BEGIN infrastructure code - the helper functions used in tests themselves + +######### +# die # Exit error with a message to stderr +######### +function die() { + echo "$ME: $*" >&2 + exit 1 +} + +######## +# is # Simple comparison +######## +function is() { + local actual=$1 + local expect=$2 + local testname=$3 + + if [[ $actual = $expect ]]; then + # On success, include expected value; this helps readers understand + _show_ok 1 "$testname=$expect" + return + fi + _show_ok 0 "$testname" "$expect" "$actual" +} + +########## +# like # Compare, but allowing patterns +########## +function like() { + local actual=$1 + local expect=$2 + local testname=$3 + + # "is" (equality) is a subset of "like", but one that expr fails on if + # the expected result has shell-special characters like '['. Treat it + # as a special case. + + if [[ "$actual" = "$expect" ]]; then + _show_ok 1 "$testname=$expect" + return + fi + + if expr "$actual" : ".*$expect" &>/dev/null; then + # On success, include expected value; this helps readers understand + _show_ok 1 "$testname ('$actual') ~ $expect" + return + fi + _show_ok 0 "$testname" "~ $expect" "$actual" +} + +############## +# _show_ok # Helper for is() and like(): displays 'ok' or 'not ok' +############## +function _show_ok() { + local ok=$1 + local testname=$2 + + # If output is a tty, colorize pass/fail + local red= + local green= + local reset= + local bold= + if [ -t 1 ]; then + red='\e[31m' + green='\e[32m' + reset='\e[0m' + bold='\e[1m' + fi + + _bump $testcounter_file + count=$(<$testcounter_file) + + # "skip" is a special case of "ok". Assume that our caller has included + # the magical '# skip - reason" comment string. + if [[ $ok == "skip" ]]; then + # colon-plus: replace green with yellow, but only if green is non-null + green="${green:+\e[33m}" + ok=1 + fi + if [ $ok -eq 1 ]; then + echo -e "${green}ok $count $testname${reset}" + echo "ok $count $testname" >>$LOG + return + fi + + # Failed + local expect=$3 + local actual=$4 + printf "${red}not ok $count $testname${reset}\n" + printf "${red}# expected: %s${reset}\n" "$expect" + printf "${red}# actual: ${bold}%s${reset}\n" "$actual" + + echo "not ok $count $testname" >>$LOG + echo " expected: $expect" >>$LOG + + _bump $failures_file +} + +########### +# _bump # Increment a counter in a file +########### +function _bump() { + local file=$1 + + count=$(<$file) + echo $(( $count + 1 )) >| $file +} + +############# +# jsonify # convert 'foo=bar,x=y' to json {"foo":"bar","x":"y"} +############# +function jsonify() { + # split by comma + local -a settings_in + read -ra settings_in <<<"$1" + + # convert each to double-quoted form + local -a settings_out + for i in ${settings_in[*]}; do + settings_out+=$(sed -e 's/\(.*\)=\(.*\)/"\1":"\2"/' <<<$i) + done + + # ...and wrap inside braces. + # FIXME: handle commas + echo "{${settings_out[*]}}" +} + +####### +# t # Main test helper +####### +function t() { + local method=$1; shift + local path=$1; shift + local curl_args + + local testname="$method $path" + # POST requests require an extra params arg + if [[ $method = "POST" ]]; then + curl_args="-d $(jsonify $1)" + testname="$testname [$curl_args]" + shift + fi + + # entrypoint path can include a descriptive comment; strip it off + path=${path%% *} + + # curl -X HEAD but without --head seems to wait for output anyway + if [[ $method == "HEAD" ]]; then + curl_args="--head" + fi + local expected_code=$1; shift + + # If given path begins with /, use it as-is; otherwise prepend /version/ + local url=http://$HOST:$PORT + if expr "$path" : "/" >/dev/null; then + url="$url$path" + else + url="$url/v1.40/$path" + fi + + # Log every action we do + echo "-------------------------------------------------------------" >>$LOG + echo "\$ $testname" >>$LOG + rm -f $WORKDIR/curl.* + # -s = silent, but --write-out 'format' gives us important response data + response=$(curl -s -X $method ${curl_args} \ + -H 'Content-type: application/json' \ + --dump-header $WORKDIR/curl.headers.out \ + --write-out '%{http_code}^%{content_type}^%{time_total}' \ + -o $WORKDIR/curl.result.out "$url") + + # Any error from curl is instant bad news, from which we can't recover + rc=$? + if [[ $rc -ne 0 ]]; then + echo "FATAL: curl failure ($rc) on $url - cannot continue" >&2 + exit 1 + fi + + # Show returned headers (without trailing ^M or empty lines) in log file. + # Sometimes -- I can't remember why! -- we don't get headers. + if [[ -e $WORKDIR/curl.headers.out ]]; then + tr -d '\015' < $WORKDIR/curl.headers.out | egrep '.' >>$LOG + fi + + IFS='^' read actual_code content_type time_total <<<"$response" + printf "X-Response-Time: ${time_total}s\n\n" >>$LOG + + # Log results, if text. If JSON, filter through jq for readability. + if [[ $content_type =~ /octet ]]; then + output="[$(file --brief $WORKDIR/curl.result.out)]" + echo "$output" >>$LOG + else + output=$(< $WORKDIR/curl.result.out) + + if [[ $content_type =~ application/json ]]; then + jq . <<<"$output" >>$LOG + else + echo "$output" >>$LOG + fi + fi + + # Test return code + is "$actual_code" "$expected_code" "$testname : status" + + # Special case: 204/304, by definition, MUST NOT return content (rfc2616) + if [[ $expected_code = 204 || $expected_code = 304 ]]; then + if [ -n "$*" ]; then + die "Internal error: ${expected_code} status returns no output; fix your test." + fi + if [ -n "$output" ]; then + _show_ok 0 "$testname: ${expected_code} status returns no output" "''" "$output" + fi + return + fi + + local i + + # Special case: if response code does not match, dump the response body + # and skip all further subtests. + if [[ $actual_code != $expected_code ]]; then + echo -e "# response: $output" + for i; do + _show_ok skip "$testname: $i # skip - wrong return code" + done + return + fi + + for i; do + if expr "$i" : "[^=~]\+=.*" >/dev/null; then + # Exact match on json field + json_field=$(expr "$i" : "\([^=]*\)=") + expect=$(expr "$i" : '[^=]*=\(.*\)') + actual=$(jq -r "$json_field" <<<"$output") + is "$actual" "$expect" "$testname : $json_field" + elif expr "$i" : "[^=~]\+~.*" >/dev/null; then + # regex match on json field + json_field=$(expr "$i" : "\([^~]*\)~") + expect=$(expr "$i" : '[^~]*~\(.*\)') + actual=$(jq -r "$json_field" <<<"$output") + like "$actual" "$expect" "$testname : $json_field" + else + # Direct string comparison + is "$output" "$i" "$testname : output" + fi + done +} + +################### +# start_service # Run the socket listener +################### +service_pid= +function start_service() { + test -x $PODMAN_BIN || die "Not found: $PODMAN_BIN" + + rm -rf $WORKDIR/{root,runroot,cni} + mkdir $WORKDIR/cni + cp /etc/cni/net.d/*podman*conflist $WORKDIR/cni/ + + $PODMAN_BIN \ + --root $WORKDIR/root \ + --runroot $WORKDIR/runroot \ + --cgroup-manager=systemd \ + --cni-config-dir $WORKDIR/cni \ + system service \ + --time 0 unix:/$DOCKER_SOCK \ + &> $WORKDIR/server.log & + service_pid=$! + + # Wait (FIXME: how do we test the socket?) + local _timeout=5 + while [ $_timeout -gt 0 ]; do + # FIXME: should we actually try a read or write? + test -S $DOCKER_SOCK && return + sleep 1 + _timeout=$(( $_timeout - 1 )) + done + cat $WORKDIR/server.log + die "Timed out waiting for service" +} + +############ +# podman # Needed by some test scripts to invoke the actual podman binary +############ +function podman() { + echo "\$ $PODMAN_BIN $*" >>$WORKDIR/output.log + $PODMAN_BIN --root $WORKDIR "$@" >>$WORKDIR/output.log 2>&1 +} + +# END infrastructure code +############################################################################### +# BEGIN sanity checks + +for tool in curl docker-compose; do + type $tool &>/dev/null || die "$ME: Required tool '$tool' not found" +done + +# END sanity checks +############################################################################### +# BEGIN entry handler (subtest invoker) + +TESTS_DIR=$WORKDIR/awesome-compose + +git clone $AWESOME_COMPOSE $TESTS_DIR +git -C $TESTS_DIR checkout -q a3c38822277bcca04abbadf34120dcff808db3ec + +# Identify the tests to run. If called with args, use those as globs. +tests_to_run=() +if [ -n "$*" ]; then + shopt -s nullglob + for i; do + match=(${TEST_ROOTDIR}/*${i}*.curl) + if [ ${#match} -eq 0 ]; then + die "No match for $TEST_ROOTDIR/*$i*.curl" + fi + tests_to_run+=("${match[@]}") + done + shopt -u nullglob +else + tests_to_run=(${TEST_ROOTDIR}/*.curl) +fi + +# Test count: each of those tests might have a local set of subtests +n_tests=$((2 * ${#tests_to_run[*]})) +for t in ${tests_to_run[@]}; do + n_curls=$(wc -l $t | awk '{print $1}') + n_tests=$(( n_tests + n_curls )) +done + + +echo "1..$n_tests" + +for t in ${tests_to_run[@]}; do + testname="$(basename $t .curl)" + + start_service + + logfile=$WORKDIR/$testname.log + ( + cd $TESTS_DIR/$testname || die "Cannot cd $TESTS_DIR/$testname" + docker-compose up -d &> $logfile + if [[ $? -ne 0 ]]; then + _show_ok 0 "$testname - up" "[ok]" "$(< $logfile)" + # FIXME: cat log + docker-compose down >>$logfile 2>&1 # No status check here + exit 1 + fi + _show_ok 1 "$testname - up" + + # FIXME: run tests, e.g. curl + curls=$TEST_ROOTDIR/$testname.curl + if [[ -e $curls ]]; then + while read port expect; do + actual=$(curl --retry 5 --retry-connrefused -s http://127.0.0.1:$port/) + curl_rc=$? + if [ $curl_rc -ne 0 ]; then + _show_ok 0 "$testname - curl failed with status $curl_rc" + docker-compose down >>$logfile 2>&1 + exit 1 + fi + like "$actual" "$expect" "$testname : port $port" + done < $curls + fi + + docker-compose down &> $logfile + if [[ $? -eq 0 ]]; then + _show_ok 1 "$testname - down" + else + _show_ok 0 "$testname - down" "[ok]" "$(< $logfile)" + # FIXME: show error + fi + ) + + kill $service_pid + wait $service_pid + + # FIXME: otherwise we get EBUSY + umount $WORKDIR/root/overlay &>/dev/null +done + +# END entry handler +############################################################################### + +# Clean up + +test_count=$(<$testcounter_file) +failure_count=$(<$failures_file) + +if [ -z "$PODMAN_TESTS_KEEP_WORKDIR" ]; then + rm -rf $WORKDIR +fi + +exit $failure_count |