#!/usr/bin/env bash # # Usage: test-compose [testname] # ME=$(basename $0) ############################################################################### # BEGIN stuff you can but probably shouldn't customize # Directory where this script and all subtests live TEST_ROOTDIR=$(realpath $(dirname $0)) # Podman executable PODMAN_BIN=$(realpath $TEST_ROOTDIR/../../bin)/podman # Local path to docker socket with unix prefix # The path will be changed for rootless users DOCKER_SOCK=/var/run/docker.sock # END stuff you can but probably shouldn't customize ############################################################################### # BEGIN setup export 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 ################# # is_rootless # Check if we run as normal user ################# function is_rootless() { [ "$(id -u)" -ne 0 ] } ######### # 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" # Not all errors include actual/expect if [[ -n "$expect" || -n "$actual" ]]; then printf "${red}# expected: %s${reset}\n" "$expect" printf "${red}# actual: ${bold}%s${reset}\n" "$actual" fi 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 } ############### # test_port # Run curl against a port, check results against expectation ############### function test_port() { local port="$1" # e.g. 5000 local op="$2" # '=' or '~' local expect="$3" # what to expect from curl output # -s -S means "silent, but show errors" local actual actual=$(curl --retry 3 --retry-all-errors -s -S http://127.0.0.1:$port/) local curl_rc=$? if [ $curl_rc -ne 0 ]; then _show_ok 0 "$testname - curl (port $port) failed with status $curl_rc" echo "# podman ps -a:" $PODMAN_BIN --storage-driver=vfs --root $WORKDIR/root --runroot $WORKDIR/runroot ps -a if type -p ss; then echo "# ss -tulpn:" ss -tulpn echo "# podman unshare --rootless-cni ss -tulpn:" $PODMAN_BIN --storage-driver=vfs --root $WORKDIR/root --runroot $WORKDIR/runroot unshare --rootless-cni ss -tulpn fi echo "# cat $WORKDIR/server.log:" cat $WORKDIR/server.log echo "# cat $logfile:" cat $logfile return fi case "$op" in '=') is "$actual" "$expect" "$testname : port $port" ;; '~') like "$actual" "$expect" "$testname : port $port" ;; *) die "Invalid operator '$op'" ;; esac } ################### # start_service # Run the socket listener ################### service_pid= function start_service() { test -x $PODMAN_BIN || die "Not found: $PODMAN_BIN" # FIXME: use ${testname} subdir but we can't: 50-char limit in runroot if ! is_rootless; then rm -rf $WORKDIR/{root,runroot,cni} else $PODMAN_BIN unshare rm -rf $WORKDIR/{root,runroot,cni} fi rm -f $DOCKER_SOCK mkdir --mode 0755 $WORKDIR/{root,runroot,cni} chcon --reference=/var/lib/containers $WORKDIR/root $PODMAN_BIN \ --log-level debug \ --storage-driver=vfs \ --root $WORKDIR/root \ --runroot $WORKDIR/runroot \ --cgroup-manager=systemd \ --network-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 $*" >>$WORKDIR/output.log output=$($PODMAN_BIN \ --storage-driver=vfs \ --root $WORKDIR/root \ --runroot $WORKDIR/runroot \ --network-config-dir $WORKDIR/cni \ "$@") echo -n "$output" >>$WORKDIR/output.log } ################### # random_string # Returns a pseudorandom human-readable string ################### function random_string() { # Numeric argument, if present, is desired length of string local length=${1:-10} head /dev/urandom | tr -dc a-zA-Z0-9 | head -c$length } # 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) # When rootless use a socket path accessible by the rootless user if is_rootless; then DOCKER_SOCK="$WORKDIR/docker.sock" DOCKER_HOST="unix://$DOCKER_SOCK" # export DOCKER_HOST docker-compose will use it export DOCKER_HOST fi # 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}*/docker-compose.yml) 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}/*/docker-compose.yml) fi # Too hard to precompute the number of tests; just spit it out at the end. n_tests=0 # We aren't really TAP 13; this helps logformatter recognize our output as BATS echo "TAP version 13" for t in "${tests_to_run[@]}"; do testdir="$(dirname $t)" testname="$(basename $testdir)" if [ -e $test_dir/SKIP ]; then reason="$(<$test_dir/SKIP)" if [ -n "$reason" ]; then reason=" - $reason" fi _show_ok skip "$testname # skip$reason" continue fi start_service logfile=$WORKDIR/$testname.log ( cd $testdir || die "Cannot cd $testdir" # setup file may be used for creating temporary directories/files. # We source it so that envariables defined in it will get back to us. if [ -e setup.sh ]; then . setup.sh fi if [ -e teardown.sh ]; then trap '. teardown.sh' 0 fi docker-compose up -d &> $logfile docker_compose_rc=$? if [[ $docker_compose_rc -ne 0 ]]; then _show_ok 0 "$testname - up" "[ok]" "status=$docker_compose_rc" sed -e 's/^/# /' <$logfile docker-compose down >>$logfile 2>&1 # No status check here exit 1 fi _show_ok 1 "$testname - up" # Run tests. This is likely to be a series of 'test_port' checks # but may also include podman commands to inspect labels, state if [ -e tests.sh ]; then . tests.sh fi # FIXME: if any tests fail, try 'podman logs' on container? if [ -n "$COMPOSE_WAIT" ]; then echo -n "Pausing due to \$COMPOSE_WAIT. Press ENTER to continue: " read keepgoing fi # Done. Clean up. docker-compose down &>> $logfile rc=$? if [[ $rc -eq 0 ]]; then _show_ok 1 "$testname - down" else _show_ok 0 "$testname - down" "[ok]" "rc=$rc" # FIXME: show error fi ) kill $service_pid wait $service_pid # FIXME: otherwise we get EBUSY if ! is_rootless; then umount $WORKDIR/root/overlay &>/dev/null else $PODMAN_BIN unshare umount $WORKDIR/root/overlay &>/dev/null fi # FIXME: run 'podman ps'? # rm -rf $WORKDIR/${testname} done # END entry handler ############################################################################### # Clean up test_count=$(<$testcounter_file) failure_count=$(<$failures_file) if [ -z "$PODMAN_TESTS_KEEP_WORKDIR" ]; then if ! is_rootless; then rm -rf $WORKDIR else $PODMAN_BIN unshare rm -rf $WORKDIR fi fi echo "1..${test_count}" exit $failure_count