Run Tests

This script is used in a Jenkins/Kitchen test suite.

Original Version

#!/usr/bin/env bash

KITCHEN=$(which kitchen)
VAGRANT=$(which vagrant)
VBOX=$(which VBoxManage)

function run_tests_cleanup_vms {
  vm_ids=""

  for machine in $($KITCHEN list -b "$1"); do
    vm_ids="$vm_ids $(vagrant global-status | grep $PWD | grep $machine | cut -f 1 -d ' ')"
  done
  $KITCHEN destroy "$1" > /dev/null

  # We then run destroy over any stray VMs
  # That is, VMs that might have failed during create, so were not destroyed
  # by kitchen destroy
  for vm_id in $vm_ids; do
    vagrant destroy --force $vm_id > /dev/null 2>&1
  done
}

if [ -z $KITCHEN ]; then
  echo "Cannot find kitchen binary. Please install to run tests."
  echo "The easiest way to do this is probably by installing ChefDK."
  echo "See the testing-utils README for more information."
  exit 1
fi

if [ -z $VAGRANT ]; then
  echo "Cannot find Vagrant binary. Please install to run tests."
  echo "See the testing-utils README for more information."
  exit 1
fi

if [ -z $VBOX ]; then
  echo "VirtualBox does not seem to be installed. Please install to run tests."
  echo "See the testing-utils README for more information."
  exit 1
fi

if [ -e "$PWD/test/testing_utils_plugin.sh" ]; then
  . "$PWD/test/testing_utils_plugin.sh"
  HAS_PLUGIN=TRUE
else
  HAS_PLUGIN=FALSE
fi

POSITIONAL=()
PID=${$}
logfile=/tmp/run_tests.${PID}.log
exit_value=0

echo "PARAMETERS: $@" > $logfile

while [[ $# -gt 0 ]]; do
  key="$1"

  case $key in
    -e|--use-emerge)
    echo "INFO: Using emerge repos"
    export USE_EMERGE=TRUE
    shift
    ;;
    -h|--help)
    echo "Usage: run_tests [options] [instance pattern]"
    echo "Options:"
    echo "  --use-emerge          Use the emerge repositories"
    echo "  --update [box]        Update the Vagrant boxes and exit. Update single box if specified."
    echo "  --cleanup [instance]  Destroy VMs matching instance pattern. All if not specified."
    echo "                        The instance pattern is passed to kitchen to match the instances upon"
    echo "                        which to run the tests. This can be a string or regex."
    echo "  --with-cleanup        Destroy all instances after testing, even if it has errors. By default,"
    echo "                        if a instance has errors it is not destroyed."

    if [ "$HAS_PLUGIN" == 'TRUE' ]; then
      echo ''
      echo "Options for $(plugin_name) tests"
      show_plugin_help
    fi

    rm -f $logfile
    exit 0
    ;;
    --update)
    echo "Pruning old Vagrant boxes, updating boxes and exiting"
    vagrant box prune
    if [ -z "$2" ]; then
      echo "Checking all boxes"
      for box in $($VAGRANT box outdated --global | grep outdated | cut -f 2 -d "'" | sort ); do
        $VAGRANT box update --box $box
      done
    else
      echo "Updating specified box: $2"
      $VAGRANT box update --box "$2"
    fi
    rm $logfile
    exit 0
    ;;
    --with-cleanup)
    export WITH_CLEANUP=TRUE
    shift
    ;;
    --cleanup)
    echo "Destroying VMs and exiting"
    run_tests_cleanup_vms "$2"
    exit 0
    ;;
    *)
    POSITIONAL+=("$1") # save it in an array for later
    shift
    ;;
  esac
done

set -- "${POSITIONAL[@]}" # restore positional parameters

if [ "$HAS_PLUGIN" == 'TRUE' ]; then
  parse_plugin_command_line "$@"
  set -- "${PLUGIN_POSITIONAL[@]}"
fi

filter_list=$1

run_tests_cleanup_vms "$filter_list"

for machine in $($KITCHEN list -b "${filter_list}"); do
  if $KITCHEN test $machine; then
    echo "PASSED: $machine" >> $logfile
  else
    echo "FAILED: $machine" >> $logfile
    exit_value=1
  fi
done

echo "Testing done"
cat $logfile
rm $logfile

unset USE_EMERGE

if [ "$HAS_PLUGIN" == 'TRUE' ]; then
  plugin_cleanup
fi

if [ "$WITH_CLEANUP" == 'TRUE' ]; then
  unset WITH_CLEANUP
  run_tests_cleanup_vms ${filter_list}
fi

exit $exit_value

Biggest Issues

  • Spaghetti logic (option parsing from start to end)

  • Options provide limited value (mostly for modifying personal directory)

  • Haphazard option parsing and heavy use of environment variables

  • Lots of bugs that require pre-/post-execution tasks

New Version

  • Uses functions to organize logic

  • Fully backward-compatible

  • Supports short and long arguments (long was considered a hard requirement)

  • Lots more built-in documentation

  • Exposes helpful options

  • Lots more logging

#!/usr/bin/env bash
##
# Wrapper script for running integration tests and working with kitchen.
##


##
# Main / Main Operations
##

# Primary entry point for script execution
main() {
	check_packages || die 'Package requisites not met.'
	case "$OPERATION" in
		test) main_test || die 'Some (or all) tests failed.';;
		clean) main_clean || die 'Issue encountered while cleaning up test artifacts.';;
		update) main_update || die 'Issue encountered while updating vagrant boxes.';;
	esac
}

# Update vagrant boxes
main_update() {
	local err=0
	msg "Pruning old Vagrant boxes, updating boxes and exiting."
	vagrant box prune || die 'Issue encountered pruning vagrant boxes.'
	if [[ "$TARGET" == '' ]]; then
		msg 'Updating all boxes.'
		boxes=("$(vagrant box outdated --global | awk -F"'" '/outdated/ { print $2 }')")
		if [[ "${boxes[0]}" != '' ]]; then
			for box in "${boxes[@]}"; do
				vagrant box update --box "$box" || err=1
			done
		fi
	else
		msg 'Updating specified box.'
		vagrant box update --box "$TARGET" || err=1
	fi

	return $err
}

# Clean up test artifacts
main_clean() {
	cleanup || return 1
	return 0
}

# Run integration tests and clean up
main_test() {
	local err=0
	cleanup

	log "PARAMETERS: ${PARAMS[*]}"
	log "PLUGIN ARGS: ${PLUGIN_ARGS[*]}"

	msg 'Testing: start'
	for machine in $(kitchen list -b "$TARGET"); do
		msg "Running tests on: $machine"
		[[ "$KEEPLOGS" ]] && lf="&>'$LOGFILE.$machine'"
		if eval kitchen test "$machine" $lf; then
			log "PASSED: $machine"
		else
			log "FAILED: $machine"
			err=1
		fi
	done
	msg 'Testing: end'


	printf '#- Results: start\n%s\n#- Results: end\n' "$(< "$LOGFILE")"

	if [[ "$KEEPLOGS" ]]; then
		msg "Test logs retained at $LOGFILE and $LOGFILE.{machine}."
	else
		rm "$LOGFILE"*
	fi

	[[ "$CLEANUP" ]] && cleanup

	return $err
}

# Clean up test machines and artifacts
cleanup() {
	local err=0
	msg 'Running clean up tasks.'

	[[ "$HAS_PLUGIN" ]] && plugin_cleanup
	kitchen destroy "$TARGET" &>/dev/null || err=1

	IFS=' ' read -r -a machines <<< "$(kitchen list -b "$TARGET")"
	IFS=$'\n' read -r -a boxes <<< "$(vagrant global-status | grep "$PWD")"

	for machine in "${machines[@]}"; do
		local nl=()
		for box in "${boxes[@]}"; do
			vid="$(awk -v tgt="$machine" '/tgt/ {print $1}' <<<"$box")"
			if [[ "$vid" ]]; then
				vagrant destroy --force "$vid" &> /dev/null || err=1
			else
				nl+=("$box")
			fi
		done
		boxes=("${nl[@]}")
	done

	return $err
}


##
# Helper Functions
##

# Parse options passed to script and load into environment
load_opts() {
	PARAMS=("$@")

	# Defaults
	LOGFILE="/tmp/run_tests.${$}.log"
	OPERATION='test'
	PLUGIN_ARGS=()
	unset HAS_PLUGIN
	unset KEEPLOGS
	unset CLEANUP
	unset TARGET
	unset QUIET


	local OPTIND OPTARG opt
	while getopts ':xukceqdh-:' opt; do
		case "$opt" in
			x) OPERATION='clean';;
			u) OPERATION='update';;
			k) KEEPLOGS='y';;
			c) CLEANUP='y';;
			e) export USE_EMERGE='TRUE';;
			q) QUIET='y';;
			d) set -x;;
			h) show_usage; exit 0;;
			-)
				case "$OPTARG" in
					clean)	OPERATION='clean';;
					update) OPERATION='update';;
					keeplogs) KEEPLOGS='y';;
					cleanup) CLEANUP='y';;
					emerge) export USE_EMERGE='TRUE';;
					quiet) QUIET='y';;
					debug) set -x;;
					help) show_usage; exit 0;;
					# legacy opts
					use-emerge) export USE_EMERGE='TRUE';;
					with-cleanup) CLEANUP='y';;
					*)
						PLUGIN_ARGS+=("--$OPTARG")
						n="${PARAMS[$((OPTIND-1))]}"
						if [[
						    ! "$n" =~ '-' &&
						    ! "$OPTARG" =~ '='
						    ]]; then
							PLUGIN_ARGS+=("$n")
							let 'OPTIND += 1'
						fi
						;;
				esac
				;;
		esac
	done
	TARGET="${PARAMS[$((OPTIND-1))]}"

	if [[ -e "$PWD/test/testing_utils_plugin.sh" ]]; then
		. "$PWD/test/testing_utils_plugin.sh"
		parse_plugin_command_line ${PLUGIN_ARGS[@]}
		HAS_PLUGIN='y'
	fi
}

# Print usage/help information
show_usage() {
	cat <<-EOF
	$0 [options] <target>

	Positional Options:
	  <target>		Restrict execution to instances matching <target> (default: all available)

	Execution Options:
	  <none>		Run integration tests
	  -u	--update	Update vagrant boxes
	  -x	--clean		Clean up test artifacts and exit
	  -h	--help		Show this usage text and exit.

	Runtime Options:
	  -d	--debug		Enable shell debug
	  -q	--quiet		Don't print informational messages

	Test Options:
	  -e	--emerge	Use $Client's Emerge repositories
	  -c	--cleanup	Clean up machines after testing (default: keep failed)
	  -k	--keeplogs	Write logs to per-test files (default: stdout)
	EOF

	if [[ "$HAS_PLUGIN" ]]; then
		printf '\n%s\n' "Options for test plugin:"
		show_plugin_help
	fi

	cat <<-EOF

	Examples:

	    Run all available tests on all supported distros:
	        run_tests.sh

	    Run tests, removing all machines after testng:
	        run_tests.sh -c

	    Run tests on Ubuntu machines:
	        run_tests.sh ubuntu

	    Clean up images from testing; no testing:
	        run_tests.sh -x

	    Typical usage:
	        run_tests.sh -eck ubuntu-1604
	EOF
}

# Verify required packages are installed
check_packages() {
	local err=0
	for pkg in 'kitchen' 'vagrant' 'VBoxManage'; do
		if ! command -v "$pkg" >/dev/null; then
			printf 'Not found in path: %s\n' "$pkg"
			err=1
		fi
	done
	return $err
}

# Write message to log file
log() {
	[[ "$LOGFILE" && "$*" ]] | return 1
	printf '%s\n' "$*" >> "$LOGFILE"
}

# Print a message (if tty/non-quiet)
msg() {
	[[ -t 1 ]] || return 0
	[[ "$QUIET" ]] || printf 'INFO: %s\n' "$*"
}

# Print a message and exit
die() {
	[[ "$*" ]] && printf '*** %s ***\n' "$*"
	exit 2
}


##
# Script Kickoff
##

load_opts "$@"
main