Created
March 27, 2016 07:29
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
#! /bin/sh | |
USAGE="git test-range [--test=NAME] OPTS RANGE [-- [COMMAND]]..." | |
LONG_USAGE="Run COMMAND for each commit in the specified RANGE in reverse order, | |
stopping if the command fails. The return code is that of the last | |
command executed (i.e., 0 only if the command succeeded for every | |
commit in the range). | |
Options: | |
--forget: forget any existing test results for the range. | |
--force | -f: forget any existing test results for the range and test it again. | |
--retest: if any commit in the range is marked as 'bad', try testing it again. | |
--keep-going | -k: if a commit fails the test, continue testing other commits | |
rather than aborting. | |
" | |
SUBDIRECTORY_OK=TRUE | |
notes_ref= | |
. "$(git --exec-path)/git-sh-setup" | |
read_status() { | |
if test -z "$notes_ref" | |
then | |
echo "unknown" | |
return | |
fi | |
local r="$1" | |
local rs="$2" | |
local status | |
if ! status="$(git notes --ref=$notes_ref show "$r^{tree}" 2>/dev/null)" | |
then | |
echo "unknown" | |
return | |
fi | |
case "$status" in | |
good|bad) | |
echo "$status" | |
;; | |
*) | |
echo 1>&2 "fatal: unrecognized status for tree $rs^{tree}!" | |
exit 3 | |
;; | |
esac | |
} | |
write_note() { | |
if test -z "$notes_ref" | |
then | |
return | |
fi | |
r="$1" | |
value="$2" | |
if git notes --ref=$notes_ref add -f "$r^{tree}" -m "$value" | |
then | |
echo "Marked tree $r^{tree} to be $value" | |
else | |
echo 1>&2 "fatal: error adding note to $r^{tree}" | |
exit 3 | |
fi | |
} | |
forget_note() { | |
if test -z "$notes_ref" | |
then | |
return | |
fi | |
r="$1" | |
if ! git notes --ref=$notes_ref remove --ignore-missing "$r^{tree}" | |
then | |
echo 1>&2 "fatal: error removing note from $r^{tree}" | |
exit 3 | |
fi | |
} | |
test_revision() { | |
r="$1" | |
shift | |
git --no-pager log -1 --decorate $r && | |
git co $r && | |
if test -n "$command" | |
then | |
eval "$command" | |
else | |
"$@" | |
fi | |
} | |
test_and_record() { | |
local r="$1" | |
shift | |
test_revision $r "$@" | |
local retcode=$? | |
if test $retcode = 0 | |
then | |
write_note $r "good" | |
else | |
echo | |
echo "*******************************************************************************" | |
echo "FAILED ON COMMIT $r" | |
echo | |
git --no-pager log -1 --decorate $r | |
echo "*******************************************************************************" | |
echo | |
echo "FAILURE!" | |
write_note $r "bad" | |
return $retcode | |
fi | |
} | |
setup_test() { | |
echo "setup_test $*" | |
name="$1" | |
notes_ref="tests/$name" | |
command="$(git config --get "test.$name.command")" | |
if test -z "$command" | |
then | |
echo 1>&2 "fatal: test $name is not defined!" | |
exit 2 | |
fi | |
echo "Using test $name; command: $command" | |
} | |
require_clean_work_tree "test-range" | |
command= | |
force=false | |
forget=false | |
retest=false | |
keep_going=false | |
while test $# != 0 | |
do | |
case "$1" in | |
--test) | |
if test $# -lt 2 | |
then | |
usage | |
exit 2 | |
fi | |
setup_test "$2" | |
shift 2 | |
;; | |
--test=*) | |
setup_test "$(echo "$1" | sed -e 's/^--test=//')" | |
shift | |
;; | |
--force|-f) | |
force=true | |
shift | |
;; | |
--forget) | |
forget=true | |
shift | |
;; | |
--retest) | |
retest=true | |
shift | |
;; | |
--keep-going|-k) | |
keep_going=true | |
shift | |
;; | |
*) | |
break | |
;; | |
esac | |
done | |
if test $# -lt 1 | |
then | |
usage | |
exit 2 | |
fi | |
range= | |
while test $# != 0 | |
do | |
case "$1" in | |
--) | |
shift | |
break | |
;; | |
*) | |
range="$range $1" | |
shift | |
;; | |
esac | |
done | |
if test $# != 0 | |
then | |
# Use the rest of the arguments as the test command line. | |
if test -n "$command" | |
then | |
echo "error: both --test and command specified!" | |
exit 2 | |
fi | |
else | |
if test -z "$name" | |
then | |
setup_test "default" | |
fi | |
fi | |
head=$(git symbolic-ref HEAD 2>/dev/null || git rev-parse HEAD) | |
if $force || $forget | |
then | |
for r in $(git rev-list --reverse $range) | |
do | |
forget_note $r | |
done | |
if $forget | |
then | |
exit 0 | |
fi | |
fi | |
fail_count=0 | |
for r in $(git rev-list --reverse $range) | |
do | |
rs="$(git rev-parse --short "$r")" | |
status="$(read_status $r $rs)" | |
echo "Old status: $status" | |
if test "$status" = "good" | |
then | |
echo "Tree $rs^{tree} is already known to be good." | |
continue | |
fi | |
if test "$status" = "bad" | |
then | |
if $retest | |
then | |
echo "Tree $rs^{tree} was previously tested to be bad; retesting..." | |
status="unknown" | |
# fall through | |
else | |
echo "Tree $rs^{tree} is already known to be bad!" | |
status="failed" | |
retcode=1 | |
fi | |
fi | |
if test "$status" = "unknown" | |
then | |
test_and_record $r "$@" | |
retcode=$? | |
if test $retcode = 0 | |
then | |
continue | |
fi | |
fi | |
# This commit has failed the test. | |
if $keep_going | |
then | |
fail_count=$((fail_count + 1)) | |
continue | |
else | |
exit $retcode | |
fi | |
done | |
git checkout -f ${head#refs/heads/} | |
echo | |
case $fail_count in | |
0) | |
echo "ALL TESTS SUCCESSFUL" | |
exit 0 | |
;; | |
1) | |
echo "!!! $fail_count TEST FAILED !!!" | |
exit 1 | |
;; | |
*) | |
echo "!!! $fail_count TESTS FAILED !!!" | |
exit 1 | |
;; | |
esac | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment