Skip to content

Instantly share code, notes, and snippets.

@fisherevans
Last active July 22, 2024 23:12
Show Gist options
  • Save fisherevans/f24f3ce15bf08e006dc7a9632d3eba35 to your computer and use it in GitHub Desktop.
Save fisherevans/f24f3ce15bf08e006dc7a9632d3eba35 to your computer and use it in GitHub Desktop.
Test local golang changes

gotestbranch

This script tries to make it easy to test changes in a go project by:

  1. Identifying what packages have been changed within your current git branch
  2. Finding all the packages that depend on those packages
  3. Merging those two lists of packages together
  4. Runs go test on those packages

This makes it easy to ensure your changes have not broken any compile-time dependencies, and that all existing tests are passing.

Example

gotestbranch -local -nodeps -skip clang/pkg -args integration

This will:

  • Run tests on all changed packages, based on local changes that are not found in origin of the current branch (-local)
  • Not run tests on packages that might depend on these changes (-nodeps)
  • Skip testing packages that contain clang/pkg in the fully qualified name (-skip)
  • Pass in the integration argument to the go test commend (-args)

Prerequisites

ag is used to find references to package names. You can install is via brew install the_silver_searcher.

#!/bin/bash
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> CHECK PRE-REQS
ag -h > /dev/null 2> /dev/null
if [ $? -ne 0 ] ; then
echo "Missing dependency 'ag' - you can install it via 'brew install the_silver_searcher'"
exit 1
fi
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> CONFIRM WE'RE IN A GIT REPO
root=`git rev-parse --show-toplevel`
if [ $? -ne 0 ] ; then
echo "Not in a git repository!"
exit 1
fi
cd "$root"
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PARSE ARGUMENTS
skipPackages=""
cleanTestCache=""
testArgs=()
local=""
base=""
mode=""
dryRun=""
noDeps=""
for arg in "$@" ; do
case $arg in
# argument modes
-args)
mode="args";;
-skip)
mode="skip";;
-base)
mode="base";;
# toggles
-clean)
cleanTestCache="true"
mode="";;
-local)
local="true"
mode="";;
-dry)
dryRun="true"
mode="";;
-nodeps)
noDeps="true"
mode="";;
# help
-help)
echo "Usage: gotestbranch <arguments>"
echo "Available arguments:"
echo " -help - prints this output"
echo " -args <test-arg-1> <test-arg-n> - supply '-args' to 'go test'"
echo " -skip <substring-1> <substring-n> - skips packages matching the supplied pattern when testing"
echo " -local - calculates the diff based on changes that have not been pushed to origin"
echo " -base <branch/commit> - what to compare the current changes to when detecting packages to test"
echo " -clean - clears the go test cache before running tests"
echo " -dry - detect impacted packages only, do not run tests"
echo " -nodeps - skip detecting dependent packages, only test modified packages"
exit 0;;
*)
# mode argument handling
case $mode in
args)
testArgs+=("$arg");;
skip)
skipPackages+="$arg"$'\n';;
base)
if [ "$base" != "" ] ; then
echo "You can only specify one base"
exit 1
fi
base="$arg";;
*)
echo "Invalid argument '$arg' ($mode). Try --help"
exit 1;;
esac;;
esac
done
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> IDENTIFY MODIFIED PACKAGES
module=`cat go.mod | grep "^module github." | head -n1 | cut -d' ' -f2`
if [ "$base" == "" ] ; then
if [ "$local" ] ; then
base=`git rev-parse --abbrev-ref HEAD`
else
base=`git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'`
fi
echo "Autodetected '$base' as the base branch"
fi
compareAgainst="origin/$base"
if [ "$local" == "" ] ; then
compareAgainst=`git merge-base HEAD $base`
fi
packages=`git diff --name-only "$compareAgainst" | xargs dirname | sort | uniq | sed -e "s~^~$module/~"`
echo "Working directory: `pwd`"
echo "Module: $module"
if [ "$packages" == "" ] ; then
echo "No changes were detected [HEAD > $compareAgainst]"
exit 0
fi
echo "Packages modified: $(echo "$packages" | wc -l) [HEAD > $compareAgainst]"
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> IDENTIFY IMPACTED PACKAGES
testPackages="$packages"
if [ "$noDeps" ] ; then
for package in $packages ; do
echo " - $package"
done
echo "Skipped dependent package detection"
else
dependencies=""
IFS=$'\n'
for package in $packages ; do
localDeps=`ag -l -G '\.go$' "$package" | xargs dirname | sort | uniq`
if [ "$localDeps" == "" ] ; then
echo " - $package (no dependencies)"
continue
fi
echo " - $package ($(echo "$localDeps" | wc -l) dependent packages)"
#echo "$localDeps" && echo && echo # debugging
dependencies+="
$localDeps" # new line intentional
done
dependencies=`echo "$dependencies" | sort | uniq | sed '/^$/d' | sed -e "s~^~$module/~"`
echo "Total upstream dependent packages: `echo "$dependencies" | wc -l`"
for package in $dependencies ; do
echo " - $package"
done
testPackages+="
$dependencies" # new line intentional
testPackages=`echo "$testPackages" | sort | uniq`
echo "Total packages directly impacted by changes: `echo "$testPackages" | wc -l`"
fi
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> FILTER IMPACTED PACKAGES
for skipPackage in $skipPackages ; do
preCount=`echo "$testPackages" | wc -l`
testPackages=`echo "$testPackages" | grep -v -F $skipPackage`
postCount=`echo "$testPackages" | wc -l`
diff=$((preCount-postCount))
if [ "$diff" -ne "0" ] ; then
echo "The '$skipPackage' filter removed $diff package(s) from the testing target"
fi
done
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PRE TEST OPTIONS
if [ "$cleanTestCache" != "" ] ; then
echo "Cleaning test cache..."
go clean -testcache
fi
if [ "$dryRun" != "" ] ; then
echo "In dry run mode, skipping tests"
exit 0
fi
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> RUN TESTS
goTestPackages=()
for package in $testPackages ; do
goTestPackages+=($package)
done
goTestArgs=()
if [ ${#testArgs[@]} -ne 0 ]; then
goTestArgs+=("-args")
goTestArgs="${goTestArgs[@]} ${testArgs[@]}"
fi
echo "Testing `echo "$testPackages" | wc -l` packages..."
echo "==================================================================================================="
exec 5>&1
output=$(eval "go test ${goTestPackages[@]} ${goTestArgs[@]}" | tee /dev/fd/5)
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PARSE TEST RESULTS
failures=`echo "$output" | grep -E "^FAIL\s+$module"`
echo "==================================================================================================="
if [ "$failures" == "" ] ; then
echo "Tests were SUCCESSFUL"
exit 0
fi
goRetestPackages=()
for failure in "$failures" ; do
failedPkg=$(echo "$failure" | sed -E 's/FAIL[[:space:]]+//' | sed -E 's/[[:space:]].*//')
goRetestPackages+=($failedPkg)
done
echo "Tests FAILED!!! To re-run the ${#goRetestPackages[@]} failed packages, execute:"
echo "go test ${goRetestPackages[@]} ${goTestArgs[@]}"
exit 1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment